Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76f51c7be7 | |||
| 089fb98782 | |||
| 1739b01d92 | |||
| 87b030d492 | |||
| fbd3b2c643 | |||
| 0a4ef9624d | |||
| 781c81752c | |||
| 8b4df6b15e | |||
| 1c6cda3c10 | |||
| 02bd9b8fd4 | |||
| 5a4be10996 | |||
| da9edaaabc | |||
| 3702073c8c | |||
| ccd019d32a | |||
| 6486d77cb4 | |||
| 9c55859853 | |||
| fd3f002275 | |||
| f893b5e8ca | |||
| fc646ab00b | |||
| 45f58abcf0 | |||
| 3d11ee9748 | |||
| 985114c9bf | |||
| 0cac934480 | |||
| 9b5d71b180 | |||
| 07dada58a9 | |||
| f64959e2c3 | |||
| e892348374 | |||
| 19ed87ce0f | |||
| 44f1204be9 | |||
| e9fd920f7e | |||
| 9435db929d | |||
| f068c910c7 | |||
| 0cfb983506 | |||
| 345e7303c1 | |||
| 1ca1a30b0e | |||
| 21f2e6fee6 | |||
| 5649c6c59d | |||
| 3f7b85ba54 | |||
| 6ec6a75f95 | |||
| a694d17a67 | |||
| 793f792f8b | |||
| 2454dd17e7 | |||
| 7922b8d4d0 | |||
| 3d6a3761e5 | |||
| fece920028 | |||
| be11283ba7 | |||
| 3e7e3d588e | |||
| 6af89c3034 | |||
| 5cf093c3f1 | |||
| f472f5dd80 | |||
| acbbe04a11 | |||
| db9fdf2178 | |||
| 417a44841d | |||
| a305de25a5 | |||
| 5b26415d1e | |||
| 82db248775 | |||
| a5c52cee09 | |||
| a390a05e8c | |||
| 368a4784f7 | |||
| d2ac2fb36d | |||
| b0d22236b0 | |||
| 1759d62600 | |||
| e778ab44b4 | |||
| 59e18acc9c | |||
| 8d33499a37 | |||
| 5a2701fc62 | |||
| 6f46bb543a | |||
| 473f6110bf | |||
| 760fbac328 | |||
| 6eff1dcae8 | |||
| 7730f68dec | |||
| 0e714d2968 | |||
| 3f19738743 | |||
| 2eec23add7 | |||
| 3ac53b7d1f | |||
| 2dc39b7849 | |||
| 2d170a30ff | |||
| cdd253ee5f | |||
| 4072445105 | |||
| 4760ee5e19 | |||
| 4863e422ad | |||
| 127f4cc9d9 | |||
| e7b0e5c8f5 | |||
| b8704f1f95 | |||
| f2ebc54b25 | |||
| b57940a933 | |||
| be043ac220 | |||
| fde3fd4399 | |||
| 5a715bc3e9 | |||
| 28d2f2c086 | |||
| 6aeb8e4179 | |||
| 5e1d6a3659 | |||
| 5bb1ad6ae0 | |||
| 521dc10df5 | |||
| f3db8e4e5c | |||
| df514fb6ec | |||
| 3ec4aaa179 | |||
| 5ceb7da87d | |||
| c1060cddf9 | |||
| 9c1c87b8dd | |||
| 9348443f61 | |||
| 331d192f78 | |||
| 7d372eb5ab | |||
| 9a284c875e | |||
| 7e0a991ce6 | |||
| 25867e2d88 | |||
| 490eecc25e | |||
| bcfa237321 | |||
| f1b8b64668 | |||
| de88846166 | |||
| 3a1d49ccef | |||
| 6afcae6fe1 | |||
| 12647087ef | |||
| a6a0dbf121 | |||
| 565d7923cd | |||
| 2b3793eeaa | |||
| 29a142d3df | |||
| a626fcdd95 | |||
| 4ae268376d | |||
| 19893aaf41 | |||
| 37768c1507 | |||
| d7f4d0b3d3 | |||
| c2ab20d9aa | |||
| 2e64ad537c | |||
| fca26f5b4d | |||
| 31c993c6eb | |||
| ccc6b608dc | |||
| e9272b8b8f | |||
| 1a8931116b | |||
| 8a0d7f2489 | |||
| f93ae798e3 | |||
| 33b4f754b0 | |||
| 6fbf05273e | |||
| 2beca0924a | |||
| 5df4fdc7c3 | |||
| 5274925f2a | |||
| 307676004e | |||
| 50301b61b9 | |||
| 39fa72ba0d | |||
| ac3db0d826 | |||
| 4aa221820d | |||
| 6a835b9efb | |||
| f9d875fc3e | |||
| 954dd25c51 | |||
| 10838779bc | |||
| 59b6ac1e47 | |||
| a04c16552a | |||
| dde6f4b8eb | |||
| 151954bd37 | |||
| 9b51a68731 | |||
| 92af260a79 | |||
| 7cbd8fb878 | |||
| f174c10da5 | |||
| a95cb9e71d | |||
| 780b6a0daa | |||
| 61e85e1402 | |||
| 4c60fd555f | |||
| 9ef1eb6655 | |||
| 77253b5d2b | |||
| 1b0a2c152b | |||
| 813ee6d0b9 | |||
| 455c9c7598 | |||
| 92e65a6431 | |||
| e02d8af3d5 | |||
| 5d06c46fa9 | |||
| 6138b6b874 | |||
| abbe40da01 | |||
| 36f381fc36 | |||
| 9be8fa98f0 | |||
| 08d983800c | |||
| 41e50ccec8 | |||
| 725ad7b459 | |||
| cb68c81a3f | |||
| 6fd917caca | |||
| 8a18187103 | |||
| 0fca6e8a9a | |||
| 038cb5be54 | |||
| 05832603e4 | |||
| c34a472b1c | |||
| 7a073ae047 | |||
| 322f7b1c30 | |||
| 293d66156f | |||
| af71548b0b |
@@ -10,7 +10,8 @@ ADBuildings.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADBuildings.MAX_MARKER_DISTANCE = 40
|
||||
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
|
||||
ADBuildings.MAX_DROP_DISTANCE = 25
|
||||
ADBuildings.CACHE_MS = 1000
|
||||
-- bygninger ændrer sig sjældent; genopbygningen (bygninger x markører) er dyr — 1 s gav et hak hvert sekund
|
||||
ADBuildings.CACHE_MS = 30000
|
||||
|
||||
local cache = {list = nil, farmId = nil, time = -math.huge}
|
||||
|
||||
@@ -30,6 +31,8 @@ local function getNodePosition(node)
|
||||
end
|
||||
|
||||
local function getName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then return tostring(name) end
|
||||
@@ -118,7 +121,10 @@ local function collectNodes(placeable, loadStations, unloadStations)
|
||||
for _, place in ipairs(spawner.spawnPlaces or {}) do
|
||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||
end
|
||||
if #nodes > 0 then table.insert(loadGroups, nodes) end
|
||||
if #nodes > 0 then
|
||||
nodes.isSpawner = true
|
||||
table.insert(loadGroups, nodes)
|
||||
end
|
||||
end
|
||||
-- stald uden aflæsningsnoder i stationen (hønsehus: kun foder-triggeren) -> søg i staldens foderdata
|
||||
if #unloadNodes == 0 and placeable.getHusbandryFillLevel ~= nil then
|
||||
@@ -139,7 +145,10 @@ local function collectNodes(placeable, loadStations, unloadStations)
|
||||
for _, place in ipairs(palletSpawner.spawnPlaces or {}) do
|
||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||
end
|
||||
if #nodes > 0 then table.insert(loadGroups, nodes) end
|
||||
if #nodes > 0 then
|
||||
nodes.isSpawner = true
|
||||
table.insert(loadGroups, nodes)
|
||||
end
|
||||
end
|
||||
end
|
||||
if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then
|
||||
@@ -218,6 +227,82 @@ end
|
||||
-- Aflæsningsmarkør: nærmeste ikke-Wait-markør ≤ MAX_MARKER_DISTANCE til aflæsningsnoderne — men ikke
|
||||
-- bygningens egne LÆSSEmarkører, når der findes en anden (Sugarbeet Crusher: pickup står tættere på
|
||||
-- aflæsningstriggeren end drop). Kun én fælles markør (AIO Load / Unload) bruges til begge.
|
||||
-- Alle ikke-Wait-markører ≤ MAX_MARKER_DISTANCE til nodes, nærmeste først: {{id, name, distance}}.
|
||||
local function markerCandidates(adEnv, nodes)
|
||||
local positions, found = {}, {}
|
||||
for _, node in ipairs(nodes) do
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||
if wayPoint ~= nil then
|
||||
local nearest = nil
|
||||
for _, position in ipairs(positions) do
|
||||
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||
if nearest == nil or distance < nearest then nearest = distance end
|
||||
end
|
||||
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then
|
||||
table.insert(found, {id = marker.id, name = tostring(marker.name), distance = nearest})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||
return found
|
||||
end
|
||||
|
||||
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
||||
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
||||
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
||||
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
||||
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
||||
if not isUnloads then
|
||||
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Diagnose (én gang pr. stald pr. session): hvilke markører ligger nærmest læsse- og aflæsningspunkterne.
|
||||
-- (in-game 27/9: Goat Food fik "Sheep Milk"-markørerne som mål og Goat Milk Pickup "Sheep Food" som kilde)
|
||||
local diagnosed = {}
|
||||
local function closestMarkers(adEnv, nodes, count)
|
||||
local positions, found = {}, {}
|
||||
for _, node in ipairs(nodes) do
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||
if wayPoint ~= nil then
|
||||
local nearest = nil
|
||||
for _, position in ipairs(positions) do
|
||||
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||
if nearest == nil or distance < nearest then nearest = distance end
|
||||
end
|
||||
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then table.insert(found, {name = tostring(marker.name), distance = nearest}) end
|
||||
end
|
||||
end
|
||||
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||
local parts = {}
|
||||
for index = 1, math.min(count, #found) do table.insert(parts, string.format("%s %.0f m", found[index].name, found[index].distance)) end
|
||||
return #parts > 0 and table.concat(parts, ", ") or "ingen markør"
|
||||
end
|
||||
|
||||
local function diagnoseHusbandry(adEnv, id, name, loadGroups, unloadNodes, loadMarkers, unloadMarker)
|
||||
if diagnosed[id] then return end
|
||||
diagnosed[id] = true
|
||||
local groups = {}
|
||||
for index, nodes in ipairs(loadGroups) do table.insert(groups, string.format("læs%d: %s", index, closestMarkers(adEnv, nodes, 2))) end
|
||||
local loads = {}
|
||||
for _, marker in ipairs(loadMarkers) do table.insert(loads, tostring(marker.name)) end
|
||||
Logging.info("ADSmartPickup: stald '%s': %s | aflæsning nærmest: %s | valgt læs=[%s] aflæs=%s", tostring(name),
|
||||
#groups > 0 and table.concat(groups, "; ") or "ingen læssegrupper", closestMarkers(adEnv, unloadNodes, 3),
|
||||
table.concat(loads, ", "), unloadMarker ~= nil and tostring(unloadMarker.name) or "-")
|
||||
end
|
||||
|
||||
local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
if #nodes == 0 then
|
||||
return nil
|
||||
@@ -332,8 +417,9 @@ local function build(adEnv, farmId)
|
||||
local marker = siloLoadMarker(adEnv, farmId, placeable, loadStations)
|
||||
if marker ~= nil then table.insert(loadMarkers, marker) end
|
||||
else
|
||||
local unloadBest = markerCandidates(adEnv, unloadNodes)[1]
|
||||
for _, nodes in ipairs(loadGroups) do
|
||||
local marker = nearestMarker(adEnv, nodes)
|
||||
local marker = ADBuildings.pickLoadMarker(markerCandidates(adEnv, nodes), unloadBest, nodes.isSpawner == true)
|
||||
if marker ~= nil and not seen[marker.id] then
|
||||
seen[marker.id] = true
|
||||
table.insert(loadMarkers, marker)
|
||||
@@ -343,6 +429,7 @@ local function build(adEnv, farmId)
|
||||
end
|
||||
-- siloer og palleskure læsser og aflæsser samme sted: altid nærmeste markør
|
||||
local unloadMarker = unloadMarkerFor(adEnv, unloadNodes, loadMarkers, kind ~= "silo" and kind ~= "objectStorage")
|
||||
if kind == "husbandry" then pcall(diagnoseHusbandry, adEnv, id, getName(placeable, id), loadGroups, unloadNodes, loadMarkers, unloadMarker) end
|
||||
-- silo der kun kan FYLDES (fx drive-on-bunker der fodrer en biogas-fermentor): aflæsning,
|
||||
-- ingen læssestation -> et forsyningsmål, aldrig en kilde
|
||||
local isFillOnlySilo = kind == "silo" and loadMarkers[1] == nil and #loadGroups == 0 and unloadMarker ~= nil
|
||||
@@ -363,6 +450,7 @@ local function build(adEnv, farmId)
|
||||
end
|
||||
|
||||
function ADBuildings.invalidate()
|
||||
if ADCache ~= nil then ADCache.clear() end
|
||||
cache.list, cache.time = nil, -math.huge
|
||||
end
|
||||
|
||||
@@ -372,6 +460,11 @@ function ADBuildings.list(adEnv, farmId)
|
||||
return cache.list
|
||||
end
|
||||
cache.list, cache.farmId, cache.time = build(adEnv, farmId), farmId, time
|
||||
-- nye bygninger uden eget navn navngives (tilføjes navnefilen; eksisterende navne røres ikke)
|
||||
if ADNames ~= nil and ADNames.autoName ~= nil then
|
||||
local ok, err = pcall(ADNames.autoName, adEnv, cache.list)
|
||||
if not ok then Logging.warning("ADSmartPickup: automatiske bygningsnavne fejlede: %s", tostring(err)) end
|
||||
end
|
||||
return cache.list
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- AD Cache
|
||||
-- Fælles korttids-cache for dyre opslag (markør -> station, salgssteder, identify, Wait-punkter, lagre).
|
||||
-- Holdende vogne genplanlægger hvert 5. s og slog de samme ting op igen og igen (markører x stationer x
|
||||
-- triggere) -> hak (in-game 26/9). Verden ændrer sig ikke så hurtigt. Uden spillets tid (tests) caches
|
||||
-- intet, så adfærden er som før. "Intet fundet" gemmes også.
|
||||
|
||||
ADCache = {}
|
||||
|
||||
ADCache.buckets = {}
|
||||
-- tændes af ADSmartPickup når hooksene er installeret (i spillet); tests kører uden cache
|
||||
ADCache.enabled = false
|
||||
local NOTHING = {}
|
||||
|
||||
-- Til moduler der indlæses uden adCache (tests): kald fn direkte.
|
||||
function ADCacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCache == nil or not ADCache.enabled then return fn() end
|
||||
return ADCache.get(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
local function now()
|
||||
local mission = g_currentMission
|
||||
if mission ~= nil and type(mission.time) == "number" then return mission.time end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Resultatet af fn() for (bucket, key), genbrugt i ttlMs.
|
||||
function ADCache.get(bucket, key, ttlMs, fn)
|
||||
local time = now()
|
||||
if not ADCache.enabled or time == nil or key == nil then return fn() end
|
||||
local store = ADCache.buckets[bucket]
|
||||
if store == nil then
|
||||
store = {size = 0, entries = {}}
|
||||
ADCache.buckets[bucket] = store
|
||||
end
|
||||
local entry = store.entries[key]
|
||||
if entry ~= nil and time - entry.at < ttlMs then
|
||||
if entry.value == NOTHING then return nil end
|
||||
return entry.value
|
||||
end
|
||||
local value = fn()
|
||||
if entry == nil then
|
||||
store.size = store.size + 1
|
||||
if store.size > 5000 then store.entries, store.size = {}, 1 end
|
||||
end
|
||||
-- spredt udløb (levetid +0-50 %): poster der blev lavet i samme kald, udløber ikke i samme frame
|
||||
-- (simulering 26/9: 600 markør-opslag udløb samtidig -> hak hvert 10. s)
|
||||
store.entries[key] = {value = value == nil and NOTHING or value, at = time + math.random() * ttlMs * 0.5}
|
||||
return value
|
||||
end
|
||||
|
||||
function ADCache.clear(bucket)
|
||||
if bucket == nil then ADCache.buckets = {} else ADCache.buckets[bucket] = nil end
|
||||
end
|
||||
@@ -0,0 +1,238 @@
|
||||
-- AD Course Cache
|
||||
-- Genbrug af Courseplay-kurser. At lade CP beregne et kursus tager sekunder på store marker (Dakota 16x, 70+ ha)
|
||||
-- og giver hak — men CP gemmer selv hver maskines seneste kursus i savegamet. Registret husker hvad kurset blev
|
||||
-- lavet TIL: mark, bredde, markens form (ADFields.shapeSignature) og indstillingerne der former kurset
|
||||
-- (værktøjsforskydning, multi-tool-sæt/bane, vin-offset/-skip). Passer alt, startes CP på maskinens eget kursus;
|
||||
-- ellers kopieres en anden maskines matchende kursus; ellers beregnes et nyt, som så registreres.
|
||||
-- Rene regler øverst (testet i tests/test_adCourseCache.lua); spil-nær lim nederst (alt pcall'et).
|
||||
|
||||
ADCourseCache = {}
|
||||
|
||||
-- ---- rene regler ----
|
||||
|
||||
local function round1(value)
|
||||
local rounded = math.floor((tonumber(value) or 0) * 10 + 0.5) / 10
|
||||
if rounded == 0 then rounded = 0 end -- ingen "-0.0"
|
||||
return string.format("%.1f", rounded)
|
||||
end
|
||||
|
||||
local function optional(value, format)
|
||||
if value == nil then return "-" end
|
||||
return format(value)
|
||||
end
|
||||
|
||||
-- Indstillingerne der former kurset, som én streng (afrundet, så float-støj ikke tæller).
|
||||
function ADCourseCache.settingsKey(s)
|
||||
s = s or {}
|
||||
return string.format("off=%s;lane=%s;multi=%d;vine=%s;skip=%s",
|
||||
round1(s.toolOffset), optional(s.lane, tostring), tonumber(s.multiTools) or 1,
|
||||
optional(s.vineOffset, round1), optional(s.vineSkip, tostring))
|
||||
end
|
||||
|
||||
-- Ny post (kopi af felterne; nil uden køretøj).
|
||||
function ADCourseCache.newRecord(values)
|
||||
if values == nil or values.vehicle == nil then return nil end
|
||||
return {vehicle = tostring(values.vehicle), field = tonumber(values.field), width = tonumber(values.width),
|
||||
sig = values.sig, settings = values.settings, waypoints = tonumber(values.waypoints)}
|
||||
end
|
||||
|
||||
-- Hvad kurset skal passe til: mark|bredde|form|indstillinger. nil hvis noget er ukendt (så genbruges intet).
|
||||
function ADCourseCache.key(entry)
|
||||
if entry == nil or entry.field == nil or entry.width == nil or entry.sig == nil or entry.settings == nil then return nil end
|
||||
return string.format("%s|%s|%s|%s", tostring(entry.field), round1(entry.width), entry.sig, entry.settings)
|
||||
end
|
||||
|
||||
function ADCourseCache.matches(record, request)
|
||||
local key = ADCourseCache.key(record)
|
||||
return key ~= nil and key == ADCourseCache.key(request)
|
||||
end
|
||||
|
||||
-- Hvorfor maskinens gemte kursus ikke kan bruges (til loggen).
|
||||
function ADCourseCache.mismatchReason(record, request)
|
||||
if request == nil or request.sig == nil then return "markens form ukendt" end
|
||||
if record == nil then return "intet kursus gemt" end
|
||||
if record.field ~= request.field then return "kursus til en anden mark" end
|
||||
if record.sig ~= request.sig then return "markens form ændret" end
|
||||
if record.width == nil or round1(record.width) ~= round1(request.width) then return "ny bredde" end
|
||||
if record.settings ~= request.settings then return "andre indstillinger" end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function copyMap(records)
|
||||
local result = {}
|
||||
for id, record in pairs(records or {}) do result[id] = record end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADCourseCache.withRecord(records, record)
|
||||
local result = copyMap(records)
|
||||
if record ~= nil then result[record.vehicle] = record end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADCourseCache.without(records, vehicleId)
|
||||
local result = copyMap(records)
|
||||
if vehicleId ~= nil then result[tostring(vehicleId)] = nil end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Kursus der kan genbruges: egen maskines først ("own"), ellers en anden maskines ("copy", laveste id først).
|
||||
-- isValid(record) (valgfri) tjekker at kurset stadig sidder på maskinen. Intet: nil + grunden.
|
||||
function ADCourseCache.findReusable(records, request, vehicleId, isValid)
|
||||
records = records or {}
|
||||
local ownId = vehicleId ~= nil and tostring(vehicleId) or nil
|
||||
local own = ownId ~= nil and records[ownId] or nil
|
||||
local anyStale = false
|
||||
local function usable(record)
|
||||
if not ADCourseCache.matches(record, request) then return false end
|
||||
if isValid ~= nil and not isValid(record) then
|
||||
anyStale = true
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
if own ~= nil and usable(own) then return own, "own" end
|
||||
local ids = {}
|
||||
for id in pairs(records) do
|
||||
if id ~= ownId then table.insert(ids, id) end
|
||||
end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
if usable(records[id]) then return records[id], "copy" end
|
||||
end
|
||||
if anyStale then return nil, "kurset på maskinen er ændret" end
|
||||
return nil, ADCourseCache.mismatchReason(own, request)
|
||||
end
|
||||
|
||||
-- Marken har fået en anden form: dens poster (med anden signatur) slettes. Returnerer nyt register + antal.
|
||||
-- Ukendt ny form (nil) sletter intet; intet slettet = samme tabel tilbage.
|
||||
function ADCourseCache.invalidateField(records, fieldId, newSig)
|
||||
if records == nil or newSig == nil then return records or {}, 0 end
|
||||
local result, removed = {}, 0
|
||||
for id, record in pairs(records) do
|
||||
if record.field == fieldId and record.sig ~= newSig then
|
||||
removed = removed + 1
|
||||
else
|
||||
result[id] = record
|
||||
end
|
||||
end
|
||||
if removed == 0 then return records, 0 end
|
||||
return result, removed
|
||||
end
|
||||
|
||||
-- ---- spil-nær lim ----
|
||||
|
||||
local function log(...) ADFieldJobs.log(...) end
|
||||
|
||||
local function getRecords() return ADFieldJobs.state.courseRecords or {} end
|
||||
local function setRecords(records)
|
||||
ADFieldJobs.state.courseRecords = records
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
local function waypointCount(course)
|
||||
if course == nil or course.getNumberOfWaypoints == nil then return nil end
|
||||
local ok, n = pcall(course.getNumberOfWaypoints, course)
|
||||
return ok and tonumber(n) or nil
|
||||
end
|
||||
|
||||
-- Kurset sidder stadig på maskinen og har samme antal punkter som da det blev registreret (spilleren kan
|
||||
-- have lavet et nyt kursus i CP's HUD).
|
||||
local function courseStillThere(vehicle, record)
|
||||
local course = ADCpBridge.getCourse(vehicle)
|
||||
if course == nil then return nil end
|
||||
local n = waypointCount(course)
|
||||
if record.waypoints ~= nil and n ~= record.waypoints then return nil end
|
||||
return course
|
||||
end
|
||||
|
||||
local function vehicleById(adEnv, id)
|
||||
local ok, found = pcall(function()
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
if ADRunsController.getVehicleId(vehicle) == id then return vehicle end
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
return ok and found or nil
|
||||
end
|
||||
|
||||
-- Hvad kurset til denne start skal passe til.
|
||||
function ADCourseCache.requestFor(vehicle, fieldId, info, role, opts)
|
||||
local vineOffset = ADCpBridge.vineOffsetFor(info.fruitName, role)
|
||||
return {
|
||||
field = fieldId, width = ADCpBridge.getWorkWidth(vehicle), sig = info.shapeSig,
|
||||
settings = ADCourseCache.settingsKey({
|
||||
toolOffset = ADCpBridge.getToolOffset(vehicle), lane = opts.lane, multiTools = opts.multiTools or 1,
|
||||
vineOffset = vineOffset, vineSkip = vineOffset ~= nil and ADCpBridge.vineSkipFor(info.fruitName, role) or nil,
|
||||
}),
|
||||
}
|
||||
end
|
||||
|
||||
-- Før ADCpBridge.start: genbrug/kopi/ny beregning. Returnerer (nye) opts + kontekst til afterStart.
|
||||
-- Har opts allerede et kursus (slåmaskinens/rivens, lederens multi-tool-kursus), bruges registret ikke.
|
||||
local function prepareUnsafe(adEnv, field, info, assignment, opts)
|
||||
if opts.course ~= nil then return opts, nil end
|
||||
local vehicle = assignment.vehicle
|
||||
local ok, request = pcall(ADCourseCache.requestFor, vehicle, field.id, info, assignment.role, opts)
|
||||
if not ok then return opts, nil end
|
||||
local vehicles = {}
|
||||
local function isValid(record)
|
||||
local owner = record.vehicle == assignment.vehicleId and vehicle or vehicleById(adEnv, record.vehicle)
|
||||
local course = owner ~= nil and courseStillThere(owner, record) or nil
|
||||
vehicles[record.vehicle] = course ~= nil and {vehicle = owner, course = course} or nil
|
||||
return course ~= nil
|
||||
end
|
||||
local record, how = ADCourseCache.findReusable(getRecords(), request, assignment.vehicleId, isValid)
|
||||
local result = {}
|
||||
for k, v in pairs(opts) do result[k] = v end
|
||||
if how == "own" then
|
||||
result.reuse = true
|
||||
log(field.id, "kursus genbrugt ('%s', %.1f m) — ingen ny beregning", ADRuns.vehicleLabel(vehicle), request.width or 0)
|
||||
elseif how == "copy" then
|
||||
local source = vehicles[record.vehicle]
|
||||
result.course = source.course
|
||||
log(field.id, "kursus kopieret fra '%s' til '%s' — ingen ny beregning", ADRuns.vehicleLabel(source.vehicle), ADRuns.vehicleLabel(vehicle))
|
||||
else
|
||||
log(field.id, "nyt kursus beregnes til '%s' (%s)", ADRuns.vehicleLabel(vehicle), tostring(how))
|
||||
end
|
||||
return result, {request = request, how = how}
|
||||
end
|
||||
|
||||
function ADCourseCache.prepare(adEnv, field, info, assignment, opts)
|
||||
local ok, result, context = pcall(prepareUnsafe, adEnv, field, info, assignment, opts)
|
||||
if ok then return result, context end
|
||||
Logging.warning("ADSmartPickup: kursus-register (Courseplay laver nyt kursus): %s", tostring(result))
|
||||
return opts, nil
|
||||
end
|
||||
|
||||
-- Efter CP er startet: maskinens nuværende kursus registreres (nyt, genbrugt eller kopieret fra registret).
|
||||
-- Uden kontekst (kursus kopieret fra slåmaskinen/lederen) passer det ikke til maskinens egen anmodning: glem.
|
||||
function ADCourseCache.afterStart(vehicle, vehicleId, context)
|
||||
if vehicleId == nil then return end
|
||||
local ok, err = pcall(function()
|
||||
local records = getRecords()
|
||||
local course = context ~= nil and ADCpBridge.getCourse(vehicle) or nil
|
||||
if course == nil or ADCourseCache.key(context.request) == nil then
|
||||
if records[tostring(vehicleId)] ~= nil then setRecords(ADCourseCache.without(records, vehicleId)) end
|
||||
return
|
||||
end
|
||||
local r = context.request
|
||||
local record = ADCourseCache.newRecord({vehicle = vehicleId, field = r.field, width = r.width, sig = r.sig,
|
||||
settings = r.settings, waypoints = waypointCount(course)})
|
||||
local existing = records[record.vehicle]
|
||||
if existing ~= nil and ADCourseCache.matches(existing, r) and existing.waypoints == record.waypoints then return end
|
||||
setRecords(ADCourseCache.withRecord(records, record))
|
||||
end)
|
||||
if not ok then Logging.warning("ADSmartPickup: kursus-register: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Markens form er (måske) ændret: poster med anden signatur glemmes (scanning og markguidens Gem).
|
||||
function ADCourseCache.checkField(fieldId, sig)
|
||||
local records = getRecords()
|
||||
local kept, removed = ADCourseCache.invalidateField(records, fieldId, sig)
|
||||
if removed > 0 then
|
||||
setRecords(kept)
|
||||
log(fieldId, "markens form er ændret — %d gemt(e) kursus(er) glemt, næste start beregner nyt", removed)
|
||||
end
|
||||
return removed
|
||||
end
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
ADCpBridge = {}
|
||||
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine",
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine", "spec_baler",
|
||||
"spec_sowingMachine", "spec_cultivator", "spec_plow", "spec_roller", "spec_weeder", "spec_mulcher"}
|
||||
|
||||
-- Sprøjtens/sprederens fyld -> rolle (tom sprøjte har ingen rolle; den kan ikke sendes ud).
|
||||
@@ -62,16 +62,92 @@ local function collect(vehicle, list)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Sprøjtens/sprederens rolle ud fra det fyld den har på (tom -> nil).
|
||||
-- Hvilke varer der hører til en rolle (påfyldning af en tom spreder/sprøjte), i foretrukken rækkefølge.
|
||||
ADCpBridge.ROLE_FILLTYPES = {
|
||||
fertilizer = {"FERTILIZER", "LIQUIDFERTILIZER", "MANURE", "LIQUIDMANURE", "DIGESTATE"},
|
||||
limer = {"LIME"},
|
||||
weeder = {"HERBICIDE"},
|
||||
}
|
||||
ADCpBridge.ROLE_ORDER = {"fertilizer", "limer", "weeder"}
|
||||
|
||||
-- Sprøjtens/sprederens roller: alle roller dens fyldenhed kan tage (Bredal-spreder: gødning OG kalk).
|
||||
-- Primær rolle = fyldets rolle (har den kalk på, er den kalkspreder), ellers den første den kan tage.
|
||||
-- Returnerer primær, sæt {[rolle] = true}, fyldets rolle (nil = tom); nil hvis ingen roller.
|
||||
function ADCpBridge.sprayRolesFor(filledName, supportedNames)
|
||||
local roles, first = {}, nil
|
||||
for _, role in ipairs(ADCpBridge.ROLE_ORDER) do
|
||||
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role]) do
|
||||
if supportedNames[name] then
|
||||
roles[role] = true
|
||||
first = first or role
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local filledRole = filledName ~= nil and ADCpBridge.sprayRoleForFillType(filledName) or nil
|
||||
if filledRole ~= nil then roles[filledRole] = true end
|
||||
local primary = filledRole or first
|
||||
return primary, primary ~= nil and roles or nil, filledRole
|
||||
end
|
||||
|
||||
local function sprayerNames(object)
|
||||
local unitIndex = object:getSprayerFillUnitIndex()
|
||||
local filledName = nil
|
||||
local level = object:getFillUnitFillLevel(unitIndex) or 0
|
||||
local capacity = object.getFillUnitCapacity ~= nil and (object:getFillUnitCapacity(unitIndex) or 0) or 0
|
||||
if level > 0 then
|
||||
filledName = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unitIndex))
|
||||
end
|
||||
local supported = {}
|
||||
local unit = object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits ~= nil and object.spec_fillUnit.fillUnits[unitIndex] or nil
|
||||
for fillType, isSupported in pairs(unit ~= nil and unit.supportedFillTypes or {}) do
|
||||
if isSupported then
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||
if name ~= nil then supported[name] = true end
|
||||
end
|
||||
end
|
||||
return filledName, supported, level, capacity
|
||||
end
|
||||
|
||||
local function sprayRole(object)
|
||||
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
|
||||
local ok, role = pcall(function()
|
||||
local unit = object:getSprayerFillUnitIndex()
|
||||
if (object:getFillUnitFillLevel(unit) or 0) <= 0 then return nil end
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unit))
|
||||
return ADCpBridge.sprayRoleForFillType(name)
|
||||
local ok, role, roles, filledRole, wrongFillDumpable = pcall(function()
|
||||
local filledName, supported, level, capacity = sprayerNames(object)
|
||||
local r, rs, fr = ADCpBridge.sprayRolesFor(filledName, supported)
|
||||
return r, rs, fr, ADCpBridge.isDumpable(level, capacity)
|
||||
end)
|
||||
return ok and role or nil
|
||||
if not ok then return nil end
|
||||
return role, roles, filledRole, wrongFillDumpable
|
||||
end
|
||||
|
||||
-- Må en forkert vare hældes ud? Kun en lille rest: højst 10 % af tanken og aldrig over 2.000 l
|
||||
-- (in-game 26/9: 100.000 l flydende gødning og 4.000 l sprøjtemiddel blev hældt ud for at skifte opgave).
|
||||
ADCpBridge.MAX_DUMP_LITERS = 2000
|
||||
ADCpBridge.MAX_DUMP_SHARE = 0.10
|
||||
function ADCpBridge.isDumpable(level, capacity)
|
||||
level = level or 0
|
||||
if level <= 0 then return true end
|
||||
if level > ADCpBridge.MAX_DUMP_LITERS then return false end
|
||||
return capacity == nil or capacity <= 0 or level <= capacity * ADCpBridge.MAX_DUMP_SHARE
|
||||
end
|
||||
|
||||
-- Tøm sprøjten/sprederen for en vare der ikke hører til opgaven (kalk på, skal gøde). FS25 kan ikke
|
||||
-- tippe gødning/kalk af, så resten kasseres. Returnerer liter og varenavn (0 hvis intet blev tømt).
|
||||
function ADCpBridge.emptySprayerIfWrong(vehicle, role)
|
||||
local liters, name = 0, nil
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
pcall(function()
|
||||
local filledName, _, level, capacity = sprayerNames(object)
|
||||
if filledName == nil or ADCpBridge.sprayRoleForFillType(filledName) == role then return end
|
||||
if not ADCpBridge.isDumpable(level, capacity) then return end
|
||||
local unit = object:getSprayerFillUnitIndex()
|
||||
object:addFillUnitFillLevel(object:getOwnerFarmId(), unit, -level, object:getFillUnitFillType(unit), ToolType.UNDEFINED, nil)
|
||||
liters, name = liters + level, filledName
|
||||
end)
|
||||
end
|
||||
end
|
||||
return liters, name
|
||||
end
|
||||
|
||||
function ADCpBridge.getSpecs(vehicle)
|
||||
@@ -80,12 +156,16 @@ function ADCpBridge.getSpecs(vehicle)
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if object[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
specs.sprayRole = specs.sprayRole or sprayRole(object)
|
||||
if specs.sprayRole == nil then specs.sprayRole, specs.sprayRoles, specs.filledRole, specs.dumpable = sprayRole(object) end
|
||||
end
|
||||
if vehicle.getCanStartCpCombineUnloader ~= nil then
|
||||
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
|
||||
specs.canUnload = ok and can == true or nil
|
||||
end
|
||||
if vehicle.getCanStartCpBaleFinder ~= nil then
|
||||
local ok, can = pcall(vehicle.getCanStartCpBaleFinder, vehicle)
|
||||
specs.canBaleCollect = ok and can == true or nil
|
||||
end
|
||||
return specs
|
||||
end
|
||||
|
||||
@@ -160,8 +240,9 @@ function ADCpBridge.isPlantableNow(fruitIndex)
|
||||
end
|
||||
|
||||
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||
-- nuværende fyld (gødning, kalk, sprøjtemiddel). nil = intet at fylde på (strigle, plov ...).
|
||||
function ADCpBridge.getRefillFillType(vehicle)
|
||||
-- fyld; en TOM spreder får rollens vare (gødning ved gødskning, kalk ved kalkning), hvis dens fyldenhed
|
||||
-- kan tage den. nil = intet at fylde på (strigle, plov ...).
|
||||
function ADCpBridge.getRefillFillType(vehicle, role)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
||||
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||
@@ -169,7 +250,16 @@ function ADCpBridge.getRefillFillType(vehicle)
|
||||
end
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
local ok, fillType = pcall(function() return object:getFillUnitFillType(object:getSprayerFillUnitIndex()) end)
|
||||
local ok, fillType = pcall(function()
|
||||
local filledName, supported = sprayerNames(object)
|
||||
if filledName ~= nil and (role == nil or ADCpBridge.sprayRoleForFillType(filledName) == role) then
|
||||
return g_fillTypeManager:getFillTypeIndexByName(filledName)
|
||||
end
|
||||
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role] or {}) do
|
||||
if supported[name] then return g_fillTypeManager:getFillTypeIndexByName(name) end
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
||||
end
|
||||
end
|
||||
@@ -189,6 +279,58 @@ function ADCpBridge.getRefillLevel(vehicle, fillType)
|
||||
return total
|
||||
end
|
||||
|
||||
-- Halmskår bag mejetærskeren (Courseplays køretøjsindstilling strawSwath: 1 = fra/hakkes, 2 = til).
|
||||
function ADCpBridge.setStrawSwath(vehicle, on)
|
||||
return (pcall(function() vehicle:getCpSettings().strawSwath:setValue(on and 2 or 1) end))
|
||||
end
|
||||
|
||||
-- Ballesamler: Courseplays BALE_FINDER_CP (ballevogn eller Universal Autoload) samler markens baller.
|
||||
function ADCpBridge.startBaleFinder(vehicle, fieldX, fieldZ, onDone)
|
||||
if AIJobType == nil or AIJobType.BALE_FINDER_CP == nil then
|
||||
return nil, "Courseplay (BALE_FINDER_CP) findes ikke"
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.BALE_FINDER_CP)
|
||||
if job == nil then return nil, "kunne ikke oprette ballesamler-job" end
|
||||
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
|
||||
local params = job.cpJobParameters
|
||||
if params ~= nil and params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
||||
if params ~= nil and params.startPosition ~= nil then
|
||||
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
|
||||
pcall(function() params.startPosition:setPosition(vx, vz) end)
|
||||
end
|
||||
pcall(function() job:setValues() end)
|
||||
local handle = {cancelled = false, fired = false, finished = false}
|
||||
local function finish(success, reason)
|
||||
if handle.finished then return end
|
||||
handle.finished = true
|
||||
if not handle.cancelled then onDone(success, reason) end
|
||||
end
|
||||
local function startJob()
|
||||
if handle.cancelled then return end
|
||||
local valid, err = job:validate(farmId)
|
||||
if not valid then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
if handle.fired then return end
|
||||
handle.fired = true
|
||||
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
|
||||
startJob()
|
||||
end
|
||||
if job.registerFieldBoundaryDetectionCallback ~= nil then
|
||||
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
|
||||
end
|
||||
local isValid, isRunning, message = job:detectFieldBoundary()
|
||||
if isValid and not isRunning then
|
||||
startJob()
|
||||
elseif not isRunning then
|
||||
return nil, "Courseplay: " .. tostring(message)
|
||||
end
|
||||
return handle
|
||||
end
|
||||
|
||||
-- Stop køretøjets AI-job (Courseplay/hjælper).
|
||||
function ADCpBridge.stop(vehicle)
|
||||
pcall(function()
|
||||
@@ -235,8 +377,12 @@ function ADCpBridge.getWorkWidth(vehicle)
|
||||
return best
|
||||
end
|
||||
|
||||
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
|
||||
-- med AD aktiv blev meldt 'Courseplay kører' og ventede på sig selv).
|
||||
function ADCpBridge.isWorking(vehicle)
|
||||
if vehicle.getIsCpFieldWorkActive ~= nil and vehicle:getIsCpFieldWorkActive() then return true end
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if module ~= nil and module.isActive ~= nil and module:isActive() then return false end
|
||||
return vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() or false
|
||||
end
|
||||
|
||||
@@ -309,6 +455,27 @@ function ADCpBridge.getCourse(vehicle)
|
||||
return ok and course or nil
|
||||
end
|
||||
|
||||
-- Forager: nye kurser får altid HEADLANDS runder og starter på forageren, så der ikke køres ind i træer, hegn
|
||||
-- o.l. i hjørnerne (brugerkrav 29/9). Kan kurset ikke laves med forager (smal mark), prøves én gang uden.
|
||||
ADCpBridge.HEADLANDS = 2
|
||||
|
||||
function ADCpBridge.setHeadlands(vehicle, n)
|
||||
if vehicle.getCourseGeneratorSettings == nil then return false end
|
||||
return (pcall(function()
|
||||
local settings = vehicle:getCourseGeneratorSettings()
|
||||
settings.numberOfHeadlands:setValue(n)
|
||||
if settings.startOnHeadland ~= nil then settings.startOnHeadland:setValue(n > 0) end
|
||||
end))
|
||||
end
|
||||
|
||||
-- Har kurset forager? (ukendt API = antag ja, så intet genberegnes unødigt)
|
||||
function ADCpBridge.courseHasHeadlands(course)
|
||||
if course == nil then return false end
|
||||
if course.getNumberOfHeadlands == nil then return true end
|
||||
local ok, n = pcall(course.getNumberOfHeadlands, course)
|
||||
return not ok or type(n) ~= "number" or n > 0
|
||||
end
|
||||
|
||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||
local function setMultiTools(vehicle, n)
|
||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||
@@ -328,7 +495,36 @@ end
|
||||
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
|
||||
-- multiTools = antal sæt når kurset laves her (multi-tool-leder)
|
||||
-- lane = køretøjets bane på et multi-tool-kursus (laneOffset -2..2)
|
||||
-- onDone(ok, reason) kaldes én gang; reason "copied" ved succes med kopieret kursus.
|
||||
-- reuse = start på køretøjets eget nuværende kursus uden ny beregning (ADCourseCache fandt det passende)
|
||||
-- onDone(ok, reason) kaldes én gang; reason "copied"/"reused" ved succes med kopieret/genbrugt kursus.
|
||||
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
||||
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
||||
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
||||
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
|
||||
function ADCpBridge.vineOffsetFor(fruitName, role)
|
||||
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
||||
if role == "harvester" then return 0 end
|
||||
return -1
|
||||
end
|
||||
|
||||
-- Vinrækker der springes over: sprøjter/spredere arbejder på begge sider af gangen på én gang, så de kører kun
|
||||
-- i hver anden gang (1). Andre redskaber (harve, mulcher) arbejder mod én side (0). nil = ikke en vinmark.
|
||||
ADCpBridge.VINE_BOTH_SIDES = {fertilizer = true, weeder = true, limer = true}
|
||||
function ADCpBridge.vineSkipFor(fruitName, role)
|
||||
if ADCpBridge.vineOffsetFor(fruitName, role) == nil then return nil end
|
||||
return ADCpBridge.VINE_BOTH_SIDES[role] and 1 or 0
|
||||
end
|
||||
|
||||
function ADCpBridge.setVineSkip(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||
return (pcall(function() vehicle:getCpVineSettings().vineRowsToSkip:setValue(value) end))
|
||||
end
|
||||
|
||||
function ADCpBridge.setVineOffset(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||
return (pcall(function() vehicle:getCpVineSettings().vineCenterOffset:setValue(value) end))
|
||||
end
|
||||
|
||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
opts = opts or {}
|
||||
if not ADCpBridge.isAvailable() then
|
||||
@@ -342,6 +538,12 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
local params = job.cpJobParameters
|
||||
if params ~= nil and params.fieldPosition ~= nil then params.fieldPosition:setPosition(fieldX, fieldZ) end
|
||||
if params ~= nil and params.startPosition ~= nil then params.startPosition:setPosition(vx, vz) end
|
||||
-- startpunkt i kurset: CP's standard er "first" (forfra) — "nearest"/"last" på en påbegyndt mark
|
||||
local startAtValues = CpFieldWorkJobParameters ~= nil and {first = CpFieldWorkJobParameters.START_AT_FIRST_POINT,
|
||||
nearest = CpFieldWorkJobParameters.START_AT_NEAREST_POINT, last = CpFieldWorkJobParameters.START_AT_LAST_POINT} or {}
|
||||
if opts.startAt ~= nil and startAtValues[opts.startAt] ~= nil and params ~= nil and params.startAt ~= nil then
|
||||
pcall(function() params.startAt:setValue(startAtValues[opts.startAt]) end)
|
||||
end
|
||||
job:setValues()
|
||||
local handle = {cancelled = false, fired = false, finished = false}
|
||||
local function finish(ok, reason)
|
||||
@@ -355,10 +557,16 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
local ok, err = job:validate(farmId)
|
||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true, handle.copiedCourse and "copied" or nil)
|
||||
finish(true, handle.copiedCourse and "copied" or (handle.reusedCourse and "reused" or nil))
|
||||
end
|
||||
local function generateAndStart()
|
||||
if handle.cancelled then return end
|
||||
-- genbrug: maskinens eget kursus (CP's validate kræver kun hasCpCourse) — ingen ny beregning
|
||||
-- eget kursus genbruges kun med forager (ellers nyt kursus med 2 runder)
|
||||
if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil and ADCpBridge.courseHasHeadlands(ADCpBridge.getCourse(vehicle)) then
|
||||
handle.reusedCourse = true
|
||||
return startWithCourse()
|
||||
end
|
||||
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
||||
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
||||
if copied then
|
||||
@@ -368,11 +576,22 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||
end
|
||||
setMultiTools(vehicle, opts.multiTools or 1)
|
||||
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||
if handle.cancelled then return end
|
||||
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
startWithCourse()
|
||||
end)
|
||||
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
|
||||
local function generate(headlands)
|
||||
ADCpBridge.setHeadlands(vehicle, headlands)
|
||||
handle.generating = true
|
||||
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||
handle.generating = false
|
||||
if handle.cancelled then return end
|
||||
if not generated and headlands > 0 then
|
||||
Logging.info("ADSmartPickup: kursus med %d forager-runder kunne ikke laves — prøver uden forager", headlands)
|
||||
return generate(0)
|
||||
end
|
||||
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
startWithCourse()
|
||||
end)
|
||||
end
|
||||
generate(ADCpBridge.HEADLANDS)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
if handle.fired then return end
|
||||
@@ -406,6 +625,21 @@ function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station
|
||||
if params ~= nil then
|
||||
if params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
||||
if params.startPosition ~= nil then pcall(function() params.startPosition:setPosition(waitX, waitZ) end) end
|
||||
-- positioner med retning: CP's applyCurrentState sætter retningen, men fejler den (pcall ovenfor) bliver den
|
||||
-- aldrig sat, og spillet afviser jobbet med "Angle missing" (in-game 27/9 F15, hvert 5. min i en time)
|
||||
pcall(function()
|
||||
local dirX, _, dirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1)
|
||||
local angle = MathUtil.getYRotationFromDirection(dirX, dirZ)
|
||||
for _, position in ipairs({params.startPosition, params.fieldUnloadPosition}) do
|
||||
if position ~= nil and position.getAngle ~= nil and position:getAngle() == nil then
|
||||
if position.getPosition ~= nil and select(1, position:getPosition()) == nil then
|
||||
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
|
||||
position:setPosition(vx, vz)
|
||||
end
|
||||
position:setAngle(angle)
|
||||
end
|
||||
end
|
||||
end)
|
||||
if params.useGiantsUnload ~= nil then pcall(function() params.useGiantsUnload:setValue(true) end) end
|
||||
end
|
||||
pcall(function() job:setValues() end)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
-- AD CP Resume
|
||||
-- Spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (AutoDrive gør selv). Ved hvert gem
|
||||
-- skrives de maskiner hvor CP kører (uden AD) til ADSmartPickup_cpresume.xml; efter indlæsning startes netop
|
||||
-- dem igen fra nærmeste waypoint med CP's egen HUD-start (samme job som spilleren sidst valgte i HUD'en).
|
||||
|
||||
ADCpResume = {}
|
||||
ADCpResume.FILE_NAME = "ADSmartPickup_cpresume.xml"
|
||||
ADCpResume.ROOT = "ADSmartPickupCpResume"
|
||||
ADCpResume.LOG_PREFIX = "ADSmartPickup: CP-genstart: "
|
||||
ADCpResume.pending = nil -- {[vehicleId] = true} fra seneste gem; nil = ingen fil (gammel regel bruges)
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- vehicles = {{id, cpActive, adActive}} -> id'er der skal huskes
|
||||
function ADCpResume.activeIds(vehicles)
|
||||
local ids = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if vehicle.cpActive and not vehicle.adActive then table.insert(ids, vehicle.id) end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
function ADCpResume.fromRecords(ids)
|
||||
local set = {}
|
||||
for _, id in ipairs(ids or {}) do
|
||||
if type(id) == "string" and id ~= "" then set[id] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
-- vehicles = {{id, aiActive, controlled}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste)
|
||||
function ADCpResume.toStart(pending, vehicles)
|
||||
if pending == nil then return nil end
|
||||
local ids = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled then table.insert(ids, vehicle.id) end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function vehicleState(adEnv, vehicle)
|
||||
local ad = vehicle.ad
|
||||
return {
|
||||
id = ADRunsController.getVehicleId(vehicle),
|
||||
cpActive = vehicle.getIsCpActive ~= nil and vehicle:getIsCpActive() == true,
|
||||
adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(),
|
||||
aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true,
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true,
|
||||
}
|
||||
end
|
||||
|
||||
local function farmVehicles(adEnv)
|
||||
local list, byId = {}, {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local ok, state = pcall(vehicleState, adEnv, vehicle)
|
||||
if ok and state.id ~= nil then
|
||||
table.insert(list, state)
|
||||
byId[state.id] = vehicle
|
||||
end
|
||||
end
|
||||
return list, byId
|
||||
end
|
||||
|
||||
-- Starter CP fra nærmeste waypoint (HUD-startpunktet sættes som spillerens "start ved nærmeste"-tast).
|
||||
function ADCpResume.startNearest(vehicle)
|
||||
if vehicle.cpStartStopDriver == nil or (vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive()) then return false end
|
||||
if vehicle.getCpStartingPointSetting ~= nil and CpFieldWorkJobParameters ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse() then
|
||||
local setting = vehicle:getCpStartingPointSetting()
|
||||
if setting ~= nil then setting:setValue(CpFieldWorkJobParameters.START_AT_NEAREST_POINT) end
|
||||
end
|
||||
vehicle:cpStartStopDriver(true)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Id'er der skal startes nu (nil = ingen gemt liste). Listen bruges kun én gang.
|
||||
function ADCpResume.takeToStart(adEnv)
|
||||
local list, byId = farmVehicles(adEnv)
|
||||
local ids = ADCpResume.toStart(ADCpResume.pending, list)
|
||||
ADCpResume.pending = nil
|
||||
return ids, byId
|
||||
end
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADCpResume.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADCpResume.save(adEnv)
|
||||
local path = getPath()
|
||||
if path == nil or adEnv == nil then return false end
|
||||
local ids = ADCpResume.activeIds((farmVehicles(adEnv)))
|
||||
local xmlFile = XMLFile.create("ADSmartPickupCpResume", path, ADCpResume.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
for index, id in ipairs(ids) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", ADCpResume.ROOT, index - 1), id)
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
||||
return true
|
||||
end
|
||||
|
||||
function ADCpResume.load()
|
||||
ADCpResume.pending = nil
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupCpResume", path)
|
||||
if xmlFile == nil then return end
|
||||
local ids = {}
|
||||
xmlFile:iterate(ADCpResume.ROOT .. ".vehicle", function(_, key) table.insert(ids, xmlFile:getString(key .. "#id")) end)
|
||||
xmlFile:delete()
|
||||
ADCpResume.pending = ADCpResume.fromRecords(ids)
|
||||
Logging.info(ADCpResume.LOG_PREFIX .. "%d maskine(r) kørte Courseplay ved sidste gem", #ids)
|
||||
end
|
||||
@@ -8,6 +8,7 @@ ADFieldFleet = {}
|
||||
-- plov, ikke aflæsningsvogn). sprayRole kommer fra sprøjtens/sprederens fyld (gødning/kalk/ukrudt).
|
||||
ADFieldFleet.ROLE_SPECS = {
|
||||
{role = "harvester", spec = "spec_combine"},
|
||||
{role = "baler", spec = "spec_baler"},
|
||||
{role = "collector", spec = "spec_forageWagon"},
|
||||
{role = "rake", spec = "spec_windrower"},
|
||||
{role = "tedder", spec = "spec_tedder"},
|
||||
@@ -17,8 +18,10 @@ ADFieldFleet.ROLE_SPECS = {
|
||||
{role = "weeder", spec = "spec_weeder"},
|
||||
{role = "sprayRole"},
|
||||
{role = "mulcher", spec = "spec_mulcher"},
|
||||
{role = "cultivator", spec = "spec_cultivator"},
|
||||
-- plov før kultivator: FS25-plove har også spec_cultivator (in-game 28/9: POV 5 XL blev "kultivator")
|
||||
{role = "plow", spec = "spec_plow"},
|
||||
{role = "cultivator", spec = "spec_cultivator"},
|
||||
{role = "baleCollector", spec = "canBaleCollect"},
|
||||
{role = "unloader", spec = "canUnload"},
|
||||
}
|
||||
|
||||
@@ -33,12 +36,23 @@ function ADFieldFleet.roleFromSpecs(specs)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Ekstra opgaver en rolle kan tage: pløjning klargør jorden lige så godt som kultivering (ikke omvendt).
|
||||
ADFieldFleet.EXTRA_ROLES = {plow = {plow = true, cultivator = true}}
|
||||
|
||||
function ADFieldFleet.rolesFor(role)
|
||||
local roles = ADFieldFleet.EXTRA_ROLES[role]
|
||||
if roles == nil then return nil end
|
||||
local copy = {}
|
||||
for key, value in pairs(roles) do copy[key] = value end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Kandidater med rollen, sorteret nærmest først, delt i brugbare og afviste (med første grund).
|
||||
local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, sawRole, sawAccepted = {}, false, false
|
||||
local widthReason, widthArgs = nil, nil
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if candidate.role == role and candidate.enabled then
|
||||
if (candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])) and candidate.enabled then
|
||||
sawRole = true
|
||||
if accept == nil or accept(candidate) then
|
||||
sawAccepted = true
|
||||
@@ -46,7 +60,12 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz})
|
||||
-- spreder med en anden vare på skal tømmes først: kun hvis ingen anden kan
|
||||
local wrongFill = candidate.filledRole ~= nil and candidate.filledRole ~= role
|
||||
-- fuld af en anden vare: bruges slet ikke til denne opgave (resten hældes kun ud når den er lille)
|
||||
if not (wrongFill and candidate.dumpable == false) then
|
||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz, wrongFill = wrongFill})
|
||||
end
|
||||
elseif widthReason == nil then
|
||||
widthReason, widthArgs = reason, args
|
||||
end
|
||||
@@ -54,7 +73,10 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(usable, function(a, b) return a.distance < b.distance end)
|
||||
table.sort(usable, function(a, b)
|
||||
if a.wrongFill ~= b.wrongFill then return not a.wrongFill end
|
||||
return a.distance < b.distance
|
||||
end)
|
||||
local reason, args = nil, nil
|
||||
if not sawRole then
|
||||
reason = "noRig"
|
||||
@@ -71,6 +93,43 @@ end
|
||||
-- Nærmeste brugbare sæt. Grund når intet passer: noRig (ingen med flueben og rollen), cannotDo (ingen kan
|
||||
-- tage opgaven, fx afgrøden), allBusy, tooWide/tooNarrow (bredden, med tal fra den første).
|
||||
-- accept (valgfri): candidate -> bool, fx "høsteren kan tage hvede".
|
||||
-- Faste maskiner: rigIdsByField = {[fieldId] = {id, ...}}. En maskine der er fast på en ANDEN mark må
|
||||
-- ikke bruges her. Har marken selv faste maskiner med rollen, bruges kun de (marken venter på sin egen);
|
||||
-- ellers de frie. (in-game 26/9: sprøjten "til F22" blev sendt til F109.)
|
||||
function ADFieldFleet.filterDedicated(candidates, fieldId, role, rigIdsByField)
|
||||
local ownerOf, own = {}, {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
for _, id in ipairs(ids or {}) do
|
||||
ownerOf[id] = otherId
|
||||
if otherId == fieldId then own[id] = true end
|
||||
end
|
||||
end
|
||||
local ownWithRole = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local hasRole = candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])
|
||||
if own[candidate.id] and hasRole then table.insert(ownWithRole, candidate) end
|
||||
end
|
||||
if #ownWithRole > 0 then return ownWithRole end
|
||||
local free = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
if ownerOf[candidate.id] == nil then table.insert(free, candidate) end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
-- Gør en maskine fast på én mark (fjernes fra andre marker). Returnerer ny tabel {[fieldId] = ids}.
|
||||
function ADFieldFleet.dedicate(rigIdsByField, fieldId, vehicleId, isOn)
|
||||
local result = {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
local copy = {}
|
||||
for _, id in ipairs(ids or {}) do if id ~= vehicleId then table.insert(copy, id) end end
|
||||
result[otherId] = copy
|
||||
end
|
||||
result[fieldId] = result[fieldId] or {}
|
||||
if isOn then table.insert(result[fieldId], vehicleId) end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADFieldFleet.pick(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
if usable[1] ~= nil then return usable[1].candidate end
|
||||
@@ -89,9 +148,39 @@ function ADFieldFleet.pickMany(candidates, role, n, x, z, requiredWidth, accept)
|
||||
return nil, reason, args
|
||||
end
|
||||
|
||||
-- Må sættet sendes ud? "all" (standard): alle ledige maskiner; "ticked": kun med flueben.
|
||||
function ADFieldFleet.isEnabled(mode, enabledIds, id)
|
||||
if mode == "ticked" then return (enabledIds or {})[id] == true end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldFleet.toggle(enabledIds, id)
|
||||
local copy = {}
|
||||
for key, value in pairs(enabledIds or {}) do copy[key] = value end
|
||||
copy[id] = (not copy[id]) or nil
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Til menuen: skjul maskiner der er faste på en ANDEN mark end den valgte. Returnerer synlige, antal
|
||||
-- skjulte og de marker de er faste på (sorteret). selectedFieldId nil = vis alle.
|
||||
function ADFieldFleet.visibleForField(rigs, selectedFieldId, rigIdsByField)
|
||||
if selectedFieldId == nil then return rigs, 0, {} end
|
||||
local ownerOf = {}
|
||||
for fieldId, ids in pairs(rigIdsByField or {}) do
|
||||
for _, id in ipairs(ids or {}) do ownerOf[id] = fieldId end
|
||||
end
|
||||
local visible, hidden, fieldSet = {}, 0, {}
|
||||
for _, rig in ipairs(rigs or {}) do
|
||||
local owner = ownerOf[rig.id]
|
||||
if owner ~= nil and owner ~= selectedFieldId then
|
||||
hidden = hidden + 1
|
||||
fieldSet[owner] = true
|
||||
else
|
||||
table.insert(visible, rig)
|
||||
end
|
||||
end
|
||||
local fields = {}
|
||||
for fieldId in pairs(fieldSet) do table.insert(fields, fieldId) end
|
||||
table.sort(fields, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return visible, hidden, fields
|
||||
end
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
-- AD Field Guide
|
||||
-- "Opsæt mark": markguidens trin oven på ADGuide (ren Lua, ingen spil-API). Kladden er en kopi af
|
||||
-- markens plan (+ faste maskiner), så intet ændres før [Gem]. Alt spil-afhængigt kommer i `context`:
|
||||
-- {info, sowable = {{name, title}}, mowable = {[name] = true}, straw = {[name] = true} | nil,
|
||||
-- rigs = {{id, text, role}}, rigIdsByField = {[fieldId] = ids}, fieldSilos = {{id, text}},
|
||||
-- stores = {{id, text}} | nil (nil = markens vare kendes ikke), storeHidden, fields = {{id, text}}}
|
||||
-- Rækker er data: {type = "choice"|"toggle"|"check"|"text"|"link", id, title/label, ...}; GUI'et tegner dem.
|
||||
|
||||
ADFieldGuide = {}
|
||||
|
||||
ADFieldGuide.ORDER = {"field", "crop", "cropChoice", "prep", "care", "machines", "store", "summary"}
|
||||
ADFieldGuide.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
||||
ADFieldGuide.STRAW_VALUES = {"chop", "collect", "bale"}
|
||||
|
||||
local function L(key, args) return {key = key, args = args} end
|
||||
local function T(text) return {text = text} end
|
||||
local YES_NO = {off = L("ui_no"), on = L("ui_yes")}
|
||||
|
||||
-- ---- afledte egenskaber ----
|
||||
|
||||
local function target(draft)
|
||||
return draft.plan ~= nil and draft.plan.targetFruit or ""
|
||||
end
|
||||
|
||||
local function isMowable(context, name)
|
||||
return name ~= nil and name ~= "" and (context.mowable or {})[name] == true
|
||||
end
|
||||
|
||||
-- Ukendt halm-tabel (tom/nil): alle høstafgrøder regnes med halm (som før guiden).
|
||||
local function hasStraw(context, name)
|
||||
if context.straw == nil or next(context.straw) == nil then return true end
|
||||
return name ~= nil and context.straw[name] == true
|
||||
end
|
||||
|
||||
local function info(guide) return guide.context.info or {} end
|
||||
|
||||
local function mowOptions(guide)
|
||||
local t = target(guide.draft)
|
||||
return info(guide).kind == "mowable" or isMowable(guide.context, t)
|
||||
end
|
||||
|
||||
local function strawOptions(guide)
|
||||
local t, i = target(guide.draft), info(guide)
|
||||
if t ~= "" then
|
||||
if isMowable(guide.context, t) then return false end
|
||||
return hasStraw(guide.context, t)
|
||||
end
|
||||
return i.kind == "harvest" and hasStraw(guide.context, i.fruitName)
|
||||
end
|
||||
|
||||
-- Der høstes (mejetærsker + aflæsningsvogne) nu eller senere.
|
||||
local function harvestInvolved(guide)
|
||||
local t = target(guide.draft)
|
||||
return info(guide).kind == "harvest" or (t ~= "" and not isMowable(guide.context, t))
|
||||
end
|
||||
|
||||
local function prepVisible(guide)
|
||||
local t, i = target(guide.draft), info(guide)
|
||||
return i.kind ~= "mowable" or (t ~= "" and t ~= i.fruitName)
|
||||
end
|
||||
|
||||
local function careVisible(guide)
|
||||
return info(guide).kind ~= "bare" or target(guide.draft) ~= ""
|
||||
end
|
||||
|
||||
-- Ukrudt giver kun mening på afgrøder der ikke slås (samme regel som før).
|
||||
local function weedVisible(guide)
|
||||
local t = target(guide.draft)
|
||||
if t ~= "" then return not isMowable(guide.context, t) end
|
||||
return info(guide).kind ~= "mowable"
|
||||
end
|
||||
|
||||
local VISIBLE = {
|
||||
field = function(guide) return guide.draft.pickField == true end,
|
||||
crop = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
cropChoice = function(guide) return guide.draft.fieldId ~= nil and (mowOptions(guide) or strawOptions(guide)) end,
|
||||
prep = function(guide) return guide.draft.fieldId ~= nil and prepVisible(guide) end,
|
||||
care = function(guide) return guide.draft.fieldId ~= nil and careVisible(guide) end,
|
||||
machines = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
store = function(guide) return guide.draft.fieldId ~= nil and guide.context.stores ~= nil and info(guide).kind ~= "bare" end,
|
||||
summary = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
}
|
||||
|
||||
function ADFieldGuide.isVisible(step, guide)
|
||||
local rule = VISIBLE[step]
|
||||
return rule ~= nil and rule(guide) == true
|
||||
end
|
||||
|
||||
ADFieldGuide.DEF = {order = ADFieldGuide.ORDER, isVisible = ADFieldGuide.isVisible}
|
||||
|
||||
function ADFieldGuide.stepKey(step) return "spu_guide_field_step_" .. tostring(step) end
|
||||
function ADFieldGuide.hintKey(step) return "spu_guide_field_hint_" .. tostring(step) end
|
||||
|
||||
-- ---- kladde ----
|
||||
|
||||
local function isPrepAuto(plan)
|
||||
return (plan.soil or "auto") == "auto" and plan.mulch ~= true and plan.lime ~= false and plan.roll ~= true
|
||||
end
|
||||
|
||||
-- Pleje "Automatisk" = al pleje slået til (gødning, ukrudt, kalk). Kalk tæller kun her når den ikke hører
|
||||
-- under klargøring — ellers afgør klargøringens valg den.
|
||||
local function isCareAuto(plan)
|
||||
return plan.fertilize ~= false and plan.weed ~= false and plan.lime ~= false
|
||||
end
|
||||
|
||||
local function storeModeOf(ids, fieldSilos)
|
||||
if #ids == 0 then return "auto" end
|
||||
local silo = {}
|
||||
for _, entry in ipairs(fieldSilos or {}) do silo[entry.id] = true end
|
||||
for _, id in ipairs(ids) do
|
||||
if not silo[id] then return "manual" end
|
||||
end
|
||||
return "fieldSilo"
|
||||
end
|
||||
|
||||
local function draftFrom(field, context)
|
||||
local plan = ADGuide.deepCopy(field.plan or {})
|
||||
plan.storeBuildingIds = plan.storeBuildingIds or {}
|
||||
plan.rigIds = plan.rigIds or {}
|
||||
local mode = storeModeOf(plan.storeBuildingIds, context.fieldSilos)
|
||||
return {
|
||||
fieldId = field.id, plan = plan, originalTarget = plan.targetFruit or "",
|
||||
prepAuto = isPrepAuto(plan), careAuto = isCareAuto(plan), useDedicated = #plan.rigIds > 0,
|
||||
storeMode = mode, storeIds = mode == "manual" and ADGuide.deepCopy(plan.storeBuildingIds) or {},
|
||||
}
|
||||
end
|
||||
|
||||
function ADFieldGuide.open(field, context, options)
|
||||
return ADGuide.new(ADFieldGuide.DEF, draftFrom(field, context), context, options)
|
||||
end
|
||||
|
||||
-- Guiden åbnet uden valgt mark: trin 1 "Mark".
|
||||
function ADFieldGuide.openPicker(context)
|
||||
return ADGuide.new(ADFieldGuide.DEF, {pickField = true}, context, {step = "field"})
|
||||
end
|
||||
|
||||
-- Mark valgt i trin 1: kladden bygges fra markens plan; trin 1 tæller stadig med.
|
||||
function ADFieldGuide.pickField(guide, field, context)
|
||||
local draft = draftFrom(field, context)
|
||||
draft.pickField = true
|
||||
local picked = ADGuide.withContext(ADGuide.withDraft(guide, draft), context)
|
||||
return (ADGuide.next(picked))
|
||||
end
|
||||
|
||||
-- ---- valg ----
|
||||
|
||||
local PLAN_VALUES = {
|
||||
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
|
||||
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
|
||||
}
|
||||
|
||||
function ADFieldGuide.choose(guide, id, value)
|
||||
local draft = ADGuide.deepCopy(guide.draft)
|
||||
if PLAN_VALUES[id] then
|
||||
draft.plan[id] = value
|
||||
elseif id == "product" then
|
||||
draft.plan.product = value and "hay" or "silage"
|
||||
elseif id == "prepAuto" or id == "careAuto" or id == "useDedicated" then
|
||||
draft[id] = value == true
|
||||
elseif id == "rigIds" then
|
||||
draft.plan.rigIds = ADGuide.toggleItem(draft.plan.rigIds, value.item, value.on)
|
||||
elseif id == "useFieldSilo" then
|
||||
draft.storeMode = value and "fieldSilo" or "auto"
|
||||
elseif id == "storeAuto" then
|
||||
draft.storeMode = value and "auto" or "manual"
|
||||
elseif id == "storeBuildingIds" then
|
||||
draft.storeIds = ADGuide.toggleItem(draft.storeIds, value.item, value.on)
|
||||
else
|
||||
return guide
|
||||
end
|
||||
return ADGuide.withDraft(guide, draft)
|
||||
end
|
||||
|
||||
-- ---- rækker pr. trin ----
|
||||
|
||||
local function numberOptions(from, to)
|
||||
local options = {}
|
||||
for value = from, to do table.insert(options, {label = T(tostring(value)), value = value}) end
|
||||
return options
|
||||
end
|
||||
|
||||
local function choice(id, title, options, value)
|
||||
return {type = "choice", id = id, title = title, options = options, value = value}
|
||||
end
|
||||
|
||||
local function toggle(id, title, value, off, on)
|
||||
return {type = "toggle", id = id, title = title, off = off or YES_NO.off, on = on or YES_NO.on, value = value == true}
|
||||
end
|
||||
|
||||
local function rowsField(guide)
|
||||
local rows = {}
|
||||
for _, entry in ipairs(guide.context.fields or {}) do
|
||||
table.insert(rows, {type = "link", id = "field", value = entry.id, label = T(entry.text)})
|
||||
end
|
||||
if #rows == 0 then table.insert(rows, {type = "text", label = L("spu_field_scanning")}) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCrop(guide)
|
||||
local plan = guide.draft.plan
|
||||
local options = {{label = L("spu_guide_crop_keep"), value = ""}}
|
||||
for _, fruit in ipairs(guide.context.sowable or {}) do
|
||||
table.insert(options, {label = T(fruit.title), value = fruit.name})
|
||||
end
|
||||
local rows = {choice("targetFruit", L("spu_guide_crop"), options, plan.targetFruit or "")}
|
||||
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
|
||||
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCropChoice(guide)
|
||||
local plan, rows = guide.draft.plan, {}
|
||||
if mowOptions(guide) then
|
||||
table.insert(rows, toggle("product", L("spu_field_plan"), ADFieldModel.product(plan, "mowable") == "hay",
|
||||
L("spu_field_plan_silage"), L("spu_field_plan_hay")))
|
||||
table.insert(rows, toggle("rake", L("spu_field_rake"), plan.rake))
|
||||
end
|
||||
if strawOptions(guide) then
|
||||
local options = {}
|
||||
for _, value in ipairs(ADFieldGuide.STRAW_VALUES) do
|
||||
table.insert(options, {label = L("spu_field_straw_" .. value), value = value})
|
||||
end
|
||||
table.insert(rows, choice("straw", L("spu_field_straw"), options, plan.straw or "chop"))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsPrep(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("prepAuto", L("spu_guide_prep"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.prepAuto)}
|
||||
if draft.prepAuto then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_prep_autoText")})
|
||||
return rows
|
||||
end
|
||||
local soil = {}
|
||||
for _, value in ipairs(ADFieldGuide.SOIL_VALUES) do
|
||||
table.insert(soil, {label = L("spu_field_soil_" .. value), value = value})
|
||||
end
|
||||
table.insert(rows, choice("soil", L("spu_field_soil"), soil, plan.soil or "auto"))
|
||||
table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false))
|
||||
table.insert(rows, toggle("mulch", L("spu_field_mulch"), plan.mulch))
|
||||
if target(draft) ~= "" or plan.sow then
|
||||
table.insert(rows, toggle("roll", L("spu_field_roll"), plan.roll))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCare(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("careAuto", L("spu_guide_care"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.careAuto)}
|
||||
if draft.careAuto then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_care_autoText")})
|
||||
return rows
|
||||
end
|
||||
table.insert(rows, toggle("fertilize", L("spu_field_fertilize"), plan.fertilize ~= false))
|
||||
if weedVisible(guide) then table.insert(rows, toggle("weed", L("spu_field_weed"), plan.weed ~= false)) end
|
||||
if not prepVisible(guide) then table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false)) end
|
||||
return rows
|
||||
end
|
||||
|
||||
-- Faste maskiner: kun ledige og dem der allerede er faste her; faste på andre marker skjules med en linje.
|
||||
local function dedicatedRows(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local byField = {}
|
||||
for fieldId, ids in pairs(guide.context.rigIdsByField or {}) do byField[fieldId] = ids end
|
||||
byField[draft.fieldId] = draft.plan.rigIds
|
||||
local own = {}
|
||||
for _, id in ipairs(draft.plan.rigIds) do own[id] = true end
|
||||
local visible, hidden, fields = ADFieldFleet.visibleForField(guide.context.rigs or {}, draft.fieldId, byField)
|
||||
local sorted = {}
|
||||
for _, rig in ipairs(visible) do table.insert(sorted, rig) end
|
||||
table.sort(sorted, function(a, b)
|
||||
if (own[a.id] == true) ~= (own[b.id] == true) then return own[a.id] == true end
|
||||
if (a.role or "") ~= (b.role or "") then return (a.role or "") < (b.role or "") end
|
||||
return ADFlowView.naturalLess(tostring(a.text), tostring(b.text))
|
||||
end)
|
||||
for _, rig in ipairs(sorted) do
|
||||
table.insert(rows, {type = "check", id = "rigIds", item = rig.id, title = T(rig.text), value = own[rig.id] == true})
|
||||
end
|
||||
if #sorted == 0 then table.insert(rows, {type = "text", label = L("spu_field_noRigs")}) end
|
||||
if hidden > 0 then
|
||||
local names = {}
|
||||
for _, fieldId in ipairs(fields) do table.insert(names, "F" .. tostring(fieldId)) end
|
||||
table.insert(rows, {type = "text", label = L("spu_field_hiddenDedicated", {hidden, table.concat(names, ", ")})})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsMachines(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("rigs", L("spu_guide_rigs"), numberOptions(1, ADFieldModel.MAX_RIGS), plan.rigs or 1)}
|
||||
if harvestInvolved(guide) then
|
||||
table.insert(rows, choice("unloaders", L("spu_field_unloaders"), numberOptions(1, ADFieldModel.MAX_UNLOADERS), plan.unloaders or 1))
|
||||
end
|
||||
table.insert(rows, toggle("useDedicated", L("spu_guide_dedicated"), draft.useDedicated))
|
||||
if draft.useDedicated then
|
||||
for _, row in ipairs(dedicatedRows(guide)) do table.insert(rows, row) end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsStore(guide)
|
||||
local draft, context, rows = guide.draft, guide.context, {}
|
||||
local silos = context.fieldSilos or {}
|
||||
if #silos > 0 then
|
||||
local names = {}
|
||||
for _, silo in ipairs(silos) do table.insert(names, silo.text) end
|
||||
table.insert(rows, toggle("useFieldSilo", L("spu_guide_useFieldSilo"), draft.storeMode == "fieldSilo"))
|
||||
table.insert(rows, {type = "text", label = T(" " .. table.concat(names, ", "))})
|
||||
if draft.storeMode == "fieldSilo" then return rows end
|
||||
end
|
||||
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeMode ~= "manual"))
|
||||
if draft.storeMode ~= "manual" then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_store_autoText")})
|
||||
return rows
|
||||
end
|
||||
local chosen = {}
|
||||
for _, id in ipairs(draft.storeIds) do chosen[id] = true end
|
||||
for _, store in ipairs(context.stores or {}) do
|
||||
table.insert(rows, {type = "check", id = "storeBuildingIds", item = store.id, title = T(store.text), value = chosen[store.id] == true})
|
||||
end
|
||||
if #(context.stores or {}) == 0 then table.insert(rows, {type = "text", label = L("spu_field_noStore")}) end
|
||||
if (context.storeHidden or 0) > 0 then
|
||||
table.insert(rows, {type = "text", label = L("spu_field_storeHidden", {context.storeHidden})})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsSummary(guide)
|
||||
local rows = {}
|
||||
for _, line in ipairs(ADFieldGuide.summary(guide)) do
|
||||
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local ROWS = {
|
||||
field = rowsField, crop = rowsCrop, cropChoice = rowsCropChoice, prep = rowsPrep, care = rowsCare,
|
||||
machines = rowsMachines, store = rowsStore, summary = rowsSummary,
|
||||
}
|
||||
|
||||
function ADFieldGuide.rows(guide)
|
||||
local builder = ROWS[guide.step]
|
||||
return builder ~= nil and builder(guide) or {}
|
||||
end
|
||||
|
||||
-- ---- resumé ----
|
||||
|
||||
local function fruitTitle(context, name)
|
||||
for _, fruit in ipairs(context.sowable or {}) do
|
||||
if fruit.name == name then return fruit.title end
|
||||
end
|
||||
return tostring(name)
|
||||
end
|
||||
|
||||
local function storeText(guide)
|
||||
local draft, context = guide.draft, guide.context
|
||||
if draft.storeMode == "fieldSilo" then return {L("spu_guide_sum_fieldSilo")} end
|
||||
if draft.storeMode ~= "manual" or #draft.storeIds == 0 then return {L("spu_guide_sum_auto")} end
|
||||
local names = {}
|
||||
for _, id in ipairs(draft.storeIds) do
|
||||
for _, store in ipairs(context.stores or {}) do
|
||||
if store.id == id then table.insert(names, store.text) end
|
||||
end
|
||||
end
|
||||
if #names == 0 or #names > 2 or #names ~= #draft.storeIds then
|
||||
return {L("spu_guide_sum_stores", {#draft.storeIds})}
|
||||
end
|
||||
return {T(table.concat(names, " + "))}
|
||||
end
|
||||
|
||||
local SUMMARY = {
|
||||
crop = function(guide)
|
||||
local plan, t = guide.draft.plan, target(guide.draft)
|
||||
if t ~= "" then return {L("spu_guide_sum_target", {fruitTitle(guide.context, t)})} end
|
||||
local parts = {L("spu_guide_crop_keep")}
|
||||
if plan.sow and info(guide).kind ~= "mowable" then table.insert(parts, L("spu_guide_sum_sowAgain")) end
|
||||
return parts
|
||||
end,
|
||||
cropChoice = function(guide)
|
||||
local plan, parts = guide.draft.plan, {}
|
||||
if mowOptions(guide) then
|
||||
table.insert(parts, L(ADFieldModel.product(plan, "mowable") == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage"))
|
||||
table.insert(parts, L(plan.rake and "spu_guide_sum_rake" or "spu_guide_sum_noRake"))
|
||||
end
|
||||
if strawOptions(guide) then table.insert(parts, L("spu_guide_sum_straw_" .. (plan.straw or "chop"))) end
|
||||
return parts
|
||||
end,
|
||||
prep = function(guide)
|
||||
local plan = guide.draft.plan
|
||||
if guide.draft.prepAuto then return {L("spu_guide_sum_auto")} end
|
||||
local parts = {L("spu_field_soil_" .. (plan.soil or "auto"))}
|
||||
if plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
|
||||
if plan.mulch then table.insert(parts, L("spu_guide_sum_mulch")) end
|
||||
if plan.roll and (plan.sow or target(guide.draft) ~= "") then table.insert(parts, L("spu_guide_sum_roll")) end
|
||||
return parts
|
||||
end,
|
||||
care = function(guide)
|
||||
if guide.draft.careAuto then return {L("spu_guide_sum_auto")} end
|
||||
local plan, parts = guide.draft.plan, {}
|
||||
if plan.fertilize ~= false then table.insert(parts, L("spu_guide_sum_fert")) end
|
||||
if weedVisible(guide) and plan.weed ~= false then table.insert(parts, L("spu_guide_sum_weed")) end
|
||||
if not prepVisible(guide) and plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
|
||||
if #parts == 0 then parts = {L("spu_guide_sum_noCare")} end
|
||||
return parts
|
||||
end,
|
||||
machines = function(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local parts = {}
|
||||
if draft.useDedicated and #plan.rigIds > 0 then
|
||||
table.insert(parts, L("spu_guide_sum_dedicated", {#plan.rigIds}))
|
||||
else
|
||||
table.insert(parts, L("spu_guide_sum_freeRigs"))
|
||||
end
|
||||
table.insert(parts, L("spu_guide_sum_rigs", {plan.rigs or 1}))
|
||||
if harvestInvolved(guide) then table.insert(parts, L("spu_guide_sum_unloaders", {plan.unloaders or 1})) end
|
||||
return parts
|
||||
end,
|
||||
store = storeText,
|
||||
}
|
||||
|
||||
-- Resumé-linjer {step, title, parts} for de synlige trin (ikke "Mark" og "Oversigt").
|
||||
function ADFieldGuide.summary(guide)
|
||||
local lines = {}
|
||||
for _, step in ipairs(ADGuide.visibleSteps(guide)) do
|
||||
local build = SUMMARY[step]
|
||||
if build ~= nil then
|
||||
table.insert(lines, {step = step, title = L(ADFieldGuide.stepKey(step)), parts = build(guide)})
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- gem ----
|
||||
|
||||
-- start: true = [Kør automatisk], false = [Gem uden at starte], nil = [Ret] (tilstanden bevares).
|
||||
-- Mål-afgrøde kører altid automatisk (ADFieldModel.setTarget).
|
||||
function ADFieldGuide.result(guide, start)
|
||||
local draft = guide.draft
|
||||
local plan = ADGuide.deepCopy(draft.plan)
|
||||
local t = plan.targetFruit or ""
|
||||
if t ~= "" then
|
||||
plan.mode = "auto"
|
||||
elseif start ~= nil then
|
||||
plan.mode = start and "auto" or "manual"
|
||||
end
|
||||
if draft.prepAuto and ADFieldGuide.isVisible("prep", guide) then
|
||||
plan.soil, plan.mulch, plan.lime, plan.roll = "auto", false, true, false
|
||||
end
|
||||
if draft.careAuto and ADFieldGuide.isVisible("care", guide) then
|
||||
plan.fertilize, plan.weed = true, true
|
||||
if not prepVisible(guide) then plan.lime = true end
|
||||
end
|
||||
if ADFieldGuide.isVisible("store", guide) then
|
||||
if draft.storeMode == "fieldSilo" then
|
||||
local ids = {}
|
||||
for _, silo in ipairs(guide.context.fieldSilos or {}) do table.insert(ids, silo.id) end
|
||||
plan.storeBuildingIds = ids
|
||||
elseif draft.storeMode == "manual" then
|
||||
plan.storeBuildingIds = ADGuide.deepCopy(draft.storeIds)
|
||||
else
|
||||
plan.storeBuildingIds = {}
|
||||
end
|
||||
end
|
||||
local rigIds = draft.useDedicated and ADGuide.deepCopy(plan.rigIds) or {}
|
||||
plan.rigIds = nil
|
||||
return {fieldId = draft.fieldId, targetFruit = t, targetChanged = t ~= (draft.originalTarget or ""), plan = plan, rigIds = rigIds}
|
||||
end
|
||||
|
||||
-- Faste maskiner for alle marker efter gem: marken får rigIds, og de fjernes fra andre marker.
|
||||
function ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds)
|
||||
local mine = {}
|
||||
for _, id in ipairs(rigIds or {}) do mine[id] = true end
|
||||
local result = {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
if otherId ~= fieldId then
|
||||
local copy = {}
|
||||
for _, id in ipairs(ids or {}) do if not mine[id] then table.insert(copy, id) end end
|
||||
result[otherId] = copy
|
||||
end
|
||||
end
|
||||
result[fieldId] = ADGuide.deepCopy(rigIds or {})
|
||||
return result
|
||||
end
|
||||
@@ -7,28 +7,139 @@ ADFieldJobs = {}
|
||||
|
||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADFieldJobs.TICK_MS = 1000
|
||||
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
||||
ADFieldJobs.PASSIVE_SLICES = 10
|
||||
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
||||
ADFieldJobs.SLOW_TICK_MS = 3
|
||||
ADFieldJobs.tickCount = 0
|
||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
||||
ADFieldJobs.MAX_PASSES = 2
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
||||
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
||||
-- courseRecords: vehicleId -> post i kursus-registret (ADCourseCache)
|
||||
-- manual: {[vehicleId] = true} = høstere spilleren selv kører til marken (for brede til vejene)
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}, manual = {}}
|
||||
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
||||
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||
ADFieldJobs.courses = {}
|
||||
ADFieldJobs.finished = {}
|
||||
ADFieldJobs.timer = 0
|
||||
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
|
||||
ADFieldJobs.timer = 500
|
||||
ADFieldJobs.now = 0
|
||||
ADFieldJobs.loaded = false
|
||||
|
||||
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
||||
-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang)
|
||||
ADFieldJobs.ACTIVE_PHASES = {fetching = true, driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true}
|
||||
|
||||
-- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente.
|
||||
ADFieldJobs.REGROW_RATIO = 0.1
|
||||
|
||||
-- Trin efter høsten der ødelægger/ignorerer en stående afgrøde.
|
||||
-- Klargøring der kan måles på jorden: trin -> info-felt (andel af marken). Kultivering er gjort når jorden er
|
||||
-- klargjort (kultiveret/pløjet/såbed/stubbearbejdet); pløjning først når den er pløjet.
|
||||
ADFieldJobs.GROUND_PREP_STAGES = {cultivating = "prepared", plowing = "plowed"}
|
||||
ADFieldJobs.PREPARED_DONE_RATIO = 0.95
|
||||
|
||||
ADFieldJobs.PREP_STAGES = {strawCollecting = true, baling = true, baleCollecting = true, mulching = true, liming = true,
|
||||
plowing = true, cultivating = true, sowing = true, rolling = true}
|
||||
|
||||
-- ---- ren beslutning ----
|
||||
|
||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar, eller under 5 % af HELE
|
||||
-- marken står (rester i hjørnerne). readyRatio regner kun med den afgrøde der er tilbage, så en halvt høstet mark
|
||||
-- lignede "væk" (in-game 27/9 F111 gik til kultivering med halvdelen stående).
|
||||
ADFieldJobs.HARVEST_DONE_RATIO = 0.05
|
||||
local function cropGone(stage, info)
|
||||
if info == nil then return false end
|
||||
if stage == "harvesting" and info.kind == "bare" then return true end
|
||||
if stage == "harvesting" and type(info.standingRatio) == "number" then
|
||||
return info.standingRatio < ADFieldJobs.HARVEST_DONE_RATIO
|
||||
end
|
||||
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
||||
end
|
||||
|
||||
-- Ligger (x, z) inden for omridset? (lige-ulige-reglen; kanten tæller som inde)
|
||||
function ADFieldJobs.insidePolygon(x, z, verts)
|
||||
local inside, j = false, #verts
|
||||
for i = 1, #verts do
|
||||
local a, b = verts[i], verts[j]
|
||||
if (a.z > z) ~= (b.z > z) and x <= (b.x - a.x) * (z - a.z) / (b.z - a.z) + a.x then inside = not inside end
|
||||
j = i
|
||||
end
|
||||
return inside
|
||||
end
|
||||
|
||||
-- Efter genindlæsning: marken en CP-maskine står på, hvis markens aktuelle trin er maskinens rolle (ellers nil).
|
||||
-- fields = {{id, stage, verts = {{x, z}}}}
|
||||
function ADFieldJobs.resumeField(x, z, role, fields)
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if #(field.verts or {}) >= 3 and ADFieldModel.roleFor(field.stage) == role and ADFieldJobs.insidePolygon(x, z, field.verts) then
|
||||
return field.id
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvor Courseplay skal starte i kurset: "last" (eget kursus + husket waypoint = fortsæt præcis), "nearest"
|
||||
-- (marken er allerede delvist høstet/slået), ellers "first". CP's eget standardvalg er "first" — også på en
|
||||
-- halvt høstet mark (in-game 27/9: mejetærskeren startede F111 forfra).
|
||||
-- s = {reuse, remembered, stage, standingRatio}
|
||||
ADFieldJobs.PARTLY_DONE_RATIO = 0.95
|
||||
function ADFieldJobs.startAtFor(s)
|
||||
if s.reuse and s.remembered then return "last" end
|
||||
local cropStage = s.stage == "harvesting" or s.stage == "mowing"
|
||||
if cropStage and type(s.standingRatio) == "number" and s.standingRatio < ADFieldJobs.PARTLY_DONE_RATIO then return "nearest" end
|
||||
return "first"
|
||||
end
|
||||
|
||||
-- Skal en maskine med husket CP-waypoint genstartes efter indlæsning? Kun hvis den stadig er midt i jobbet: den
|
||||
-- står på en mark der er i gang med dens trin (fieldId), eller den står lige ved sit huskede waypoint. Et husket
|
||||
-- waypoint fra et gammelt job sendte ellers maskinen ud ad vejene (in-game 27/9: to ROPA'er). s = {fieldId, distance}
|
||||
ADFieldJobs.RESUME_NEAR_M = 30
|
||||
function ADFieldJobs.shouldResume(s)
|
||||
if s.fieldId ~= nil then return true end
|
||||
return type(s.distance) == "number" and s.distance <= ADFieldJobs.RESUME_NEAR_M
|
||||
end
|
||||
|
||||
-- Højst én maskine sendt ud pr. DISPATCH_GAP_MS på tværs af alle marker (in-game 27/9: 6 maskiner kørte fra
|
||||
-- parkeringen i samme sekund efter Start alle).
|
||||
ADFieldJobs.DISPATCH_GAP_MS = 10000
|
||||
function ADFieldJobs.mayDispatch(now, lastAt)
|
||||
return lastAt == nil or now - lastAt >= ADFieldJobs.DISPATCH_GAP_MS
|
||||
end
|
||||
|
||||
function ADFieldJobs.assignmentCount()
|
||||
local count = 0
|
||||
for _ in pairs(ADFieldJobs.state.assignments) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Har Courseplay-starten taget for lang tid? Mens CP beregner kursus (store marker), må det tage op til
|
||||
-- COURSE_GENERATE_MS; ellers START_MS (in-game 27/9 F90: 140 ha tog over 60 s, og moden opgav midt i beregningen).
|
||||
ADFieldJobs.START_MS = 60000
|
||||
ADFieldJobs.COURSE_GENERATE_MS = 300000
|
||||
function ADFieldJobs.startTimedOut(elapsed, generating)
|
||||
return elapsed > (generating and ADFieldJobs.COURSE_GENERATE_MS or ADFieldJobs.START_MS)
|
||||
end
|
||||
|
||||
-- Høsten er færdig, men høsteren har korn i tanken: tøm den på marken før marken går videre (én gang pr. mark).
|
||||
-- s = {fill, near, busy, controlled, tried}
|
||||
ADFieldJobs.emptyAttempts = {}
|
||||
function ADFieldJobs.shouldEmptyHarvester(s)
|
||||
return (s.fill or 0) > 0 and s.near == true and not s.busy and not s.controlled and not s.tried
|
||||
end
|
||||
|
||||
-- Markens punkt nærmest (x, z) — CP bruger det som kursets startsted. Uden position: første punkt.
|
||||
function ADFieldJobs.nearestPoint(points, x, z)
|
||||
if points == nil or points[1] == nil then return nil end
|
||||
if x == nil or z == nil then return points[1] end
|
||||
local best, bestD = nil, nil
|
||||
for _, point in ipairs(points) do
|
||||
local d = (point.x - x) ^ 2 + (point.z - z) ^ 2
|
||||
if bestD == nil or d < bestD then best, bestD = point, d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
||||
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
||||
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
||||
@@ -46,6 +157,20 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
end
|
||||
return {action = "start"}
|
||||
end
|
||||
-- mål-afgrøde på en bar mark: klargør straks; såningen venter selv på såtiden
|
||||
if info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
||||
if ADFieldModel.shouldStartBare(field, "bare") then
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
return {action = "start"}
|
||||
end
|
||||
end
|
||||
-- klargøring på en mark hvor afgrøden står uhøstet: tilbage til klar (in-game 27/9 F16: kartofler
|
||||
-- blev regnet som høstet, og marken stod til kultivering med afgrøden i jorden)
|
||||
local standing = info ~= nil and type(info.standingRatio) == "number" and info.standingRatio >= ADFieldJobs.HARVEST_DONE_RATIO
|
||||
if ADFieldJobs.PREP_STAGES[field.stage] and info ~= nil and info.kind == "harvest" and info.phase == "ready"
|
||||
and (standing or (info.readyRatio or 0) >= ADFieldModel.READY_RATIO) then
|
||||
return {action = "rewind"}
|
||||
end
|
||||
if not ADFieldModel.isWorking(field) then
|
||||
local care = ADFieldModel.careNeed(field, info)
|
||||
if care == nil then return {action = "none"} end
|
||||
@@ -53,7 +178,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if not hasMarker then return {action = "wait", care = care} end
|
||||
return {action = "care", stage = care}
|
||||
end
|
||||
local active, finished, helpers = 0, 0, 0
|
||||
local active, finished, helpers, awaiting = 0, 0, 0, 0
|
||||
for _, assignment in ipairs(assignments) do
|
||||
if assignment.stage == nil or assignment.stage == field.stage then
|
||||
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||
@@ -61,6 +186,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if isActive then helpers = helpers + 1 end
|
||||
else
|
||||
if isActive then active = active + 1 end
|
||||
if assignment.phase == "awaitingPlayer" then awaiting = awaiting + 1 end
|
||||
if assignment.phase == "finished" then finished = finished + 1 end
|
||||
end
|
||||
end
|
||||
@@ -69,10 +195,21 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if active == 0 then
|
||||
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
||||
if isCropStage and cropGone(field.stage, info) then return {action = "advance"} end
|
||||
-- klargøringen er allerede gjort (målt på jorden): videre — også efter genindlæsning, hvor
|
||||
-- opgaven ikke er husket (in-game 28/9: F17 blev kultiveret 3 gange)
|
||||
local groundKey = ADFieldJobs.GROUND_PREP_STAGES[field.stage]
|
||||
if groundKey ~= nil and info ~= nil and type(info[groundKey]) == "number" and info[groundKey] >= ADFieldJobs.PREPARED_DONE_RATIO then
|
||||
return {action = "advance"}
|
||||
end
|
||||
if finished > 0 then
|
||||
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
||||
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
||||
end
|
||||
-- slåning uden maskine på en halvt slået mark: vent til genvæksten er fuldt udvokset igen
|
||||
-- (et hjørne Courseplay efterlod i samme omgang, finished > 0, slås straks)
|
||||
if field.stage == "mowing" and finished == 0 and info ~= nil and (info.immatureRatio or 0) > ADFieldJobs.REGROW_RATIO then
|
||||
return {action = "wait", reason = "regrowing"}
|
||||
end
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
||||
@@ -82,7 +219,8 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
end
|
||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||
-- tømmevogne først når en høster faktisk er i gang (ikke mens spilleren kører den derud)
|
||||
if helperRole ~= nil and active > awaiting and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
||||
end
|
||||
return {action = "none"}
|
||||
@@ -124,6 +262,10 @@ function ADFieldJobs.load()
|
||||
local ok, loaded = pcall(ADFieldStorage.load)
|
||||
if ok then
|
||||
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
||||
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
||||
ADFieldJobs.state.courseRecords = loaded.courseRecords or {}
|
||||
ADFieldJobs.state.manual = loaded.manual or {}
|
||||
ADFieldJobs.state.paused = loaded.paused == true
|
||||
else
|
||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
||||
end
|
||||
@@ -135,6 +277,30 @@ function ADFieldJobs.setPlan(id, changes)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- "Start nu" på en mark hvor moden venter på spilleren (manuel transport): overtag uden at vente
|
||||
ADFieldJobs.takeoverRequested = {}
|
||||
function ADFieldJobs.requestTakeover(id)
|
||||
ADFieldJobs.takeoverRequested[id] = true
|
||||
end
|
||||
|
||||
-- Venter moden på at spilleren kører en høster til marken? (assignment eller nil)
|
||||
function ADFieldJobs.awaitingPlayer(fieldId)
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
if assignment.fieldId == fieldId and assignment.phase == "awaitingPlayer" then return assignment end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Under global pause: kun denne mark genoptages (de andre venter på Start alle). nil = pause igen.
|
||||
function ADFieldJobs.setFieldResumed(id, isOn)
|
||||
ADFieldJobs.state.resumed = ADFieldJobs.state.resumed or {}
|
||||
ADFieldJobs.state.resumed[id] = isOn ~= false or nil
|
||||
end
|
||||
|
||||
function ADFieldJobs.isFieldResumed(id)
|
||||
return (ADFieldJobs.state.resumed or {})[id] == true
|
||||
end
|
||||
|
||||
function ADFieldJobs.requestStart(id)
|
||||
ADFieldJobs.note(id, {startRequested = true})
|
||||
end
|
||||
@@ -143,6 +309,30 @@ function ADFieldJobs.forceReady(id)
|
||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
end
|
||||
|
||||
function ADFieldJobs.setTarget(id, fruitName)
|
||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
||||
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.setFleetMode(mode)
|
||||
ADFieldJobs.state.fleetMode = mode
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- Pause (Stop alt og parkér): intet nyt sendes ud, ingen genstart efter indlæsning. Gemmes.
|
||||
function ADFieldJobs.setPaused(value)
|
||||
ADFieldJobs.state.paused = value == true
|
||||
ADFieldJobs.state.resumed = {}
|
||||
ADFieldJobs.save()
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde %s", value and "på pause" or "genoptaget")
|
||||
end
|
||||
|
||||
function ADFieldJobs.toggleManual(vehicleId)
|
||||
ADFieldJobs.state.manual = ADFieldFleet.toggle(ADFieldJobs.state.manual or {}, vehicleId)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.toggleFleet(vehicleId)
|
||||
ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
|
||||
ADFieldJobs.save()
|
||||
@@ -192,32 +382,56 @@ function ADFieldJobs.environment()
|
||||
local weather = g_currentMission.environment.weather
|
||||
return weather ~= nil and weather.getIsRaining ~= nil and weather:getIsRaining() or false
|
||||
end)
|
||||
return {raining = ok and raining == true}
|
||||
return {
|
||||
raining = ok and raining == true,
|
||||
-- kan afgrøden (navn) sås i den aktuelle periode?
|
||||
plantable = function(name)
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and ADCpBridge.isPlantableNow(fruit.index)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||
ADFieldJobs.lastDecision = {}
|
||||
|
||||
local function stepField(adEnv, info)
|
||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
||||
ADFieldJobs.setField(field)
|
||||
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||
ADFieldJobs.waitingSince = {}
|
||||
|
||||
local function stepField(adEnv, info, env)
|
||||
local current = ADFieldJobs.getField(info.id)
|
||||
local field = ADFieldModel.onReadiness(current, info.readyRatio or 0)
|
||||
if field ~= current then ADFieldJobs.setField(field) end
|
||||
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment())
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
|
||||
ADFieldJobs.lastDecision[info.id] = decision
|
||||
if decision.action == "start" then
|
||||
local started = ADFieldModel.start(field, info.kind, info.needs)
|
||||
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
|
||||
started.lastFruit = info.kind == "harvest" and info.fruitName or field.lastFruit
|
||||
ADFieldJobs.setField(started)
|
||||
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "rewind" then
|
||||
ADFieldJobs.setField(ADFieldModel.with(field, {stage = "ready", retryAt = 0, reason = false}))
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||
ADFieldJobs.log(info.id, "%s -> ready: afgrøden står stadig uhøstet (%.0f %% klar)", field.stage, (info.readyRatio or 0) * 100)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "care" then
|
||||
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
||||
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "advance" then
|
||||
-- høsteren har stadig korn i tanken: CP genstartes og venter ved kursets ende, tømmevognene kommer
|
||||
-- (marken bliver på høst til tanken er tom) — in-game 27/9 F15
|
||||
if field.stage == "harvesting" and ADFieldWork.tryEmptyHarvester ~= nil then
|
||||
local okEmpty, emptying = pcall(ADFieldWork.tryEmptyHarvester, adEnv, field, info)
|
||||
if okEmpty and emptying then return end
|
||||
end
|
||||
local nextField = ADFieldModel.advance(field, info.needs)
|
||||
ADFieldJobs.setField(nextField)
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
ADFieldJobs.forgetFinished(info.id)
|
||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||
@@ -225,23 +439,58 @@ local function stepField(adEnv, info)
|
||||
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
||||
end
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "wait" and decision.reason == "rain" then
|
||||
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
||||
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
||||
elseif decision.action == "wait" and decision.reason ~= nil then
|
||||
local key = "spu_field_reason_" .. decision.reason
|
||||
if field.reason == nil or field.reason.key ~= key then
|
||||
ADFieldJobs.note(info.id, {reason = {key = key}})
|
||||
end
|
||||
elseif decision.action == "dispatch" then
|
||||
-- én udsendelse ad gangen på tværs af markerne: marken prøver igen ved næste tick
|
||||
if not ADFieldJobs.mayDispatch(ADFieldJobs.now, ADFieldJobs.lastDispatchAt) then return end
|
||||
local before = ADFieldJobs.assignmentCount()
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
if ADFieldJobs.assignmentCount() > before then
|
||||
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
elseif ADFieldJobs.waitingSince[info.id] == nil then
|
||||
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Realtid i ms: netGetTime (bruges overalt i FS), ellers getTimeSec.
|
||||
local function clockMs()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
local okSec, seconds = pcall(function() return getTimeSec() end)
|
||||
return okSec and type(seconds) == "number" and seconds * 1000 or nil
|
||||
end
|
||||
|
||||
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
|
||||
local function reportSlow(total, scan, follow, steps, stepped)
|
||||
if total == nil or total <= ADFieldJobs.SLOW_TICK_MS then return end
|
||||
if ADFieldJobs.lastSlowReport ~= nil and ADFieldJobs.now - ADFieldJobs.lastSlowReport < 60000 then return end
|
||||
ADFieldJobs.lastSlowReport = ADFieldJobs.now
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde-tick %.1f ms (scan %.1f, sæt %.1f, marker %.1f for %d marker)",
|
||||
total, scan or 0, follow or 0, steps or 0, stepped)
|
||||
end
|
||||
|
||||
ADFieldJobs.STARTUP_GRACE_MS = 120000
|
||||
|
||||
function ADFieldJobs.tick(adEnv, dt)
|
||||
ADFieldJobs.now = ADFieldJobs.now + dt
|
||||
ADFieldJobs.timer = ADFieldJobs.timer + dt
|
||||
if ADFieldJobs.loaded then
|
||||
-- markscanningen kører et lille stykke HVERT frame (arbejdskø), aldrig en hel mark i ét frame
|
||||
local okScan, errScan = pcall(ADFields.scanStep, adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
|
||||
if not okScan then Logging.warning(ADFieldJobs.LOG_PREFIX .. "markscanning: %s", tostring(errScan)) end
|
||||
end
|
||||
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
||||
ADFieldJobs.timer = 0
|
||||
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
||||
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
ADFields.scanNext(adEnv, farmId)
|
||||
local t0 = clockMs()
|
||||
local t1 = t0
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||
if not ok then
|
||||
@@ -249,8 +498,34 @@ function ADFieldJobs.tick(adEnv, dt)
|
||||
ADFieldWork.release(assignment)
|
||||
end
|
||||
end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
local ok, err = pcall(stepField, adEnv, info)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
local t2 = clockMs()
|
||||
-- de første minutter efter indlæsning sendes intet ud: Courseplay/AutoDrive genoptager deres egne jobs
|
||||
-- lidt efter start, og indtil da ligner de maskiner ledige (in-game 26/9: kultivatoren blev taget midt
|
||||
-- i spillerens eget CP-job 45 s efter indlæsning)
|
||||
if ADFieldJobs.now < ADFieldJobs.STARTUP_GRACE_MS then return end
|
||||
-- Stop alt og parkér: intet sendes ud og intet genstartes, før spilleren trykker Start alle — undtagen
|
||||
-- marker spilleren har genoptaget enkeltvis ([Start] i Fields-listen, ADFieldJobs.setFieldResumed)
|
||||
local paused, resumed = ADFieldJobs.state.paused, ADFieldJobs.state.resumed or {}
|
||||
if paused and next(resumed) == nil then return end
|
||||
if not paused and not ADFieldJobs.resumedAfterLoad then
|
||||
-- én gang, før noget nyt sendes ud: CP-maskiner der var midt i et job, fortsætter (og tæller som aktive)
|
||||
ADFieldJobs.resumedAfterLoad = true
|
||||
local okResume, errResume = pcall(ADFieldWork.resumeAfterLoad, adEnv)
|
||||
if not okResume then Logging.warning(ADFieldJobs.LOG_PREFIX .. "genstart efter indlæsning fejlede: %s", tostring(errResume)) end
|
||||
end
|
||||
local env = ADFieldJobs.environment()
|
||||
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
|
||||
local stepped = 0
|
||||
local infos = ADFields.list()
|
||||
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
||||
for index, info in ipairs(infos) do
|
||||
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
|
||||
if (not paused or resumed[info.id]) and (not ADFieldModel.isPassive(ADFieldJobs.getField(info.id)) or index % ADFieldJobs.PASSIVE_SLICES == slice) then
|
||||
stepped = stepped + 1
|
||||
local ok, err = pcall(stepField, adEnv, info, env)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
end
|
||||
end
|
||||
local t3 = clockMs()
|
||||
if t0 ~= nil and t3 ~= nil then reportSlow(t3 - t0, t1 - t0, t2 - t1, t3 - t2, stepped) end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ ADFieldModel.ROLES = {
|
||||
mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector",
|
||||
harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow",
|
||||
cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder",
|
||||
fertilizing = "fertilizer",
|
||||
fertilizing = "fertilizer", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector",
|
||||
}
|
||||
ADFieldModel.HELPERS = {harvesting = "unloader"}
|
||||
ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true}
|
||||
@@ -42,8 +42,11 @@ KNOWN_STAGES.careLiming = true
|
||||
local DEFAULT_PLAN = {
|
||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||
fertilize = true, weed = true,
|
||||
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
||||
}
|
||||
local STRAW_VALUES = {chop = true, collect = true, bale = true}
|
||||
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
||||
local STRAW_STAGES = {collect = {"strawCollecting"}, bale = {"baling", "baleCollecting"}}
|
||||
local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true}
|
||||
local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true}
|
||||
|
||||
@@ -58,6 +61,8 @@ local function copyPlan(plan)
|
||||
for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end
|
||||
for key, value in pairs(plan or {}) do copy[key] = value end
|
||||
copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil)
|
||||
-- faste maskiner (køretøjs-id'er) til marken; en fast maskine bruges kun her
|
||||
copy.rigIds = copyList(plan ~= nil and plan.rigIds or nil)
|
||||
return copy
|
||||
end
|
||||
|
||||
@@ -84,6 +89,8 @@ end
|
||||
|
||||
-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst.
|
||||
function ADFieldModel.product(plan, kind)
|
||||
-- silage/hø gælder kun slåafgrøder: et gemt produkt fra en fejlklassificeret mark (vinmark F22) ignoreres
|
||||
if kind ~= nil and kind ~= "mowable" then return "harvest" end
|
||||
if plan.product ~= nil and plan.product ~= "auto" then return plan.product end
|
||||
return kind == "mowable" and "silage" or "harvest"
|
||||
end
|
||||
@@ -116,6 +123,7 @@ function ADFieldModel.workStages(plan, kind, needs)
|
||||
local product = ADFieldModel.product(plan, kind)
|
||||
if product == "harvest" then
|
||||
local stages = {"harvesting"}
|
||||
for _, stage in ipairs(STRAW_STAGES[plan.straw or "chop"] or {}) do table.insert(stages, stage) end
|
||||
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
|
||||
return stages
|
||||
end
|
||||
@@ -123,12 +131,58 @@ function ADFieldModel.workStages(plan, kind, needs)
|
||||
if product == "hay" then table.insert(stages, "tedding") end
|
||||
if plan.rake then table.insert(stages, "raking") end
|
||||
table.insert(stages, "collecting")
|
||||
-- mål-afgrøde på en slåmark med en anden afgrøde: efter sidste slæt klargøres og sås målet
|
||||
if plan.prepAfterMow then
|
||||
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
|
||||
end
|
||||
return stages
|
||||
end
|
||||
|
||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner.
|
||||
-- Mål-afgrøde ("vælg mark + afgrøde, glem den"): planen udfyldes ud fra hvad der står på marken nu.
|
||||
-- Står målet der allerede som slåafgrøde (græs -> græs): bare slå igen og igen. Ellers: høst/slå det der
|
||||
-- står, klargør og så målet. Kører altid automatisk. Uden mål returneres planen uændret.
|
||||
function ADFieldModel.applyTarget(plan, kind, currentFruit)
|
||||
local copy = copyPlan(plan)
|
||||
if copy.targetFruit == nil or copy.targetFruit == "" then return copy end
|
||||
copy.mode = "auto"
|
||||
copy.sowFruit = copy.targetFruit
|
||||
if kind == "mowable" then
|
||||
copy.prepAfterMow = currentFruit ~= copy.targetFruit
|
||||
copy.sow = copy.prepAfterMow
|
||||
else
|
||||
copy.prepAfterMow = false
|
||||
copy.sow = true
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
|
||||
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
|
||||
function ADFieldModel.setTarget(field, fruitName)
|
||||
local copy = copyField(field)
|
||||
copy.plan.targetFruit = fruitName or ""
|
||||
copy.held = nil
|
||||
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
||||
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
||||
copy.stage = "idle"
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Bar mark med mål-afgrøde starter selv klargøring + såning med det samme: klargøringen (kalk, plov,
|
||||
-- kultivator) kører uanset årstid, og kun såningen venter på såtiden (preflight: notPlantable). Med mange
|
||||
-- marker nås det ellers ikke inden såvinduet lukker (brugerønske 28/9).
|
||||
function ADFieldModel.shouldStartBare(field, kind)
|
||||
if field.plan.targetFruit == nil or field.plan.targetFruit == "" then return false end
|
||||
-- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning
|
||||
if field.held and not field.startRequested then return false end
|
||||
return kind == "bare" and field.stage == "idle"
|
||||
end
|
||||
|
||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
|
||||
-- høsterens spor — der er breddekravet ligegyldigt).
|
||||
function ADFieldModel.requiredWidth(field, role)
|
||||
if role == "collector" and not field.plan.rake then
|
||||
if role == "collector" and not field.plan.rake and field.stage ~= "strawCollecting" then
|
||||
return field.mowWidth
|
||||
end
|
||||
return nil
|
||||
@@ -136,6 +190,8 @@ end
|
||||
|
||||
-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget.
|
||||
function ADFieldModel.courseSource(field, role)
|
||||
-- halm: læssevogn og presser kører høsterens kursus (skåret ligger i mejetærskerens spor)
|
||||
if field.stage == "strawCollecting" or role == "baler" then return "harvester" end
|
||||
if role ~= "collector" then return nil end
|
||||
return field.plan.rake and "rake" or "mower"
|
||||
end
|
||||
@@ -174,16 +230,27 @@ function ADFieldModel.lanePositions(n)
|
||||
end
|
||||
|
||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os.
|
||||
-- Uændret -> samme tabel (kaldes for hver mark hvert sekund; nye kopier giver affald og GC-hak).
|
||||
function ADFieldModel.onReadiness(field, ratio)
|
||||
local copy = copyField(field)
|
||||
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
||||
local copy = copyField(field)
|
||||
copy.stage = "ready"
|
||||
return copy
|
||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
|
||||
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
|
||||
local copy = copyField(field)
|
||||
copy.stage = "idle"
|
||||
copy.startRequested = false
|
||||
return copy
|
||||
end
|
||||
return copy
|
||||
return field
|
||||
end
|
||||
|
||||
-- Passiv mark: manuel, uden mål, venter/færdig, ingen Start nu — der sker intet uden brugeren.
|
||||
function ADFieldModel.isPassive(field)
|
||||
local plan = field.plan
|
||||
return plan.mode ~= "auto" and (plan.targetFruit == nil or plan.targetFruit == "") and not field.startRequested
|
||||
and (field.stage == "idle" or field.stage == "done")
|
||||
end
|
||||
|
||||
function ADFieldModel.shouldStart(field)
|
||||
@@ -205,14 +272,18 @@ local function freshCycle(copy)
|
||||
copy.mowWidth = nil
|
||||
copy.mowVehicleId = nil
|
||||
copy.rakeVehicleId = nil
|
||||
copy.harvestVehicleId = nil
|
||||
copy.windrowOffset = nil
|
||||
copy.careRuns = {}
|
||||
end
|
||||
|
||||
-- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig.
|
||||
function ADFieldModel.start(field, kind, needs)
|
||||
-- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den.
|
||||
function ADFieldModel.start(field, kind, needs, currentFruit)
|
||||
local copy = copyField(field)
|
||||
local stages = ADFieldModel.workStages(field.plan, kind, needs)
|
||||
copy.held = nil
|
||||
copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit)
|
||||
local stages = ADFieldModel.workStages(copy.plan, kind, needs)
|
||||
copy.stage = stages[1] or "done"
|
||||
copy.kind = kind or "mowable"
|
||||
freshCycle(copy)
|
||||
@@ -232,7 +303,7 @@ function ADFieldModel.advance(field, needs)
|
||||
local stages = ADFieldModel.workStages(field.plan, field.kind, needs)
|
||||
copy.stage = "done"
|
||||
local passed = false
|
||||
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
|
||||
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "strawCollecting", "baling", "baleCollecting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
|
||||
local rank = {}
|
||||
for index, stage in ipairs(order) do rank[stage] = index end
|
||||
for _, stage in ipairs(stages) do
|
||||
@@ -254,12 +325,14 @@ function ADFieldModel.stop(field)
|
||||
end
|
||||
copy.startRequested = false
|
||||
copy.reason = nil
|
||||
-- stoppet af spilleren: en bar mark med mål-afgrøde starter ikke selv igen (Start eller nyt mål slipper)
|
||||
copy.held = true
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling).
|
||||
-- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn.
|
||||
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true}
|
||||
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true, strawCollecting = true, baling = true}
|
||||
function ADFieldModel.isWeatherSensitive(stage, plan, kind)
|
||||
if WEATHER_STAGES[stage] then return true end
|
||||
return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay"
|
||||
@@ -297,7 +370,16 @@ function ADFieldModel.startCare(field, stage)
|
||||
end
|
||||
|
||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde.
|
||||
-- Under dette er bredden ikke læst rigtigt (spreder uden AI-markører gav 0,5 m): Courseplay laver så et
|
||||
-- kursus med rækker hver halve meter — på en 72 ha mark fryser spillet (in-game 26/9, F108).
|
||||
ADFieldModel.MIN_WORK_WIDTH = 1.5
|
||||
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
||||
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
||||
|
||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
||||
return false, "widthUnknown", {width}
|
||||
end
|
||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||
end
|
||||
@@ -376,13 +458,16 @@ function ADFieldModel.toRecord(field)
|
||||
return {
|
||||
id = field.id, stage = field.stage, mode = plan.mode, product = plan.product,
|
||||
rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "),
|
||||
dedicated = table.concat(plan.rigIds or {}, " "),
|
||||
rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime),
|
||||
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
||||
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||
mowWidth = field.mowWidth or 0,
|
||||
mowVehicle = field.mowVehicleId or "",
|
||||
rakeVehicle = field.rakeVehicleId or "",
|
||||
harvestVehicle = field.harvestVehicleId or "",
|
||||
windrowOffset = field.windrowOffset ~= nil and string.format("%.2f", field.windrowOffset) or "",
|
||||
}
|
||||
end
|
||||
@@ -405,6 +490,7 @@ function ADFieldModel.fromRecord(record)
|
||||
plan.product = PRODUCTS[record.product] and record.product or "auto"
|
||||
plan.rake = readFlag(record.rake, false)
|
||||
for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end
|
||||
for id in string.gmatch(record.dedicated or "", "%S+") do table.insert(plan.rigIds, id) end
|
||||
plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||
plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||
plan.mulch = readFlag(record.mulch, false)
|
||||
@@ -415,12 +501,16 @@ function ADFieldModel.fromRecord(record)
|
||||
plan.roll = readFlag(record.roll, false)
|
||||
plan.fertilize = readFlag(record.fertilize, true)
|
||||
plan.weed = readFlag(record.weed, true)
|
||||
plan.targetFruit = record.targetFruit or ""
|
||||
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
||||
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
||||
field.kind = readText(record.kind)
|
||||
field.lastFruit = readText(record.lastFruit)
|
||||
for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end
|
||||
field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil
|
||||
field.mowVehicleId = readText(record.mowVehicle)
|
||||
field.rakeVehicleId = readText(record.rakeVehicle)
|
||||
field.harvestVehicleId = readText(record.harvestVehicle)
|
||||
field.windrowOffset = tonumber(record.windrowOffset)
|
||||
return field
|
||||
end
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
-- AD Field Silo Config
|
||||
-- Marksilo-flowets opsætning som ren model (ny tabel ud, input muteres ikke) + flade records til XML-laget.
|
||||
|
||||
ADFieldSiloConfig = {}
|
||||
|
||||
ADFieldSiloConfig.MAX_VEHICLES = 10
|
||||
ADFieldSiloConfig.DEFAULT_MAX_VEHICLES = 2
|
||||
ADFieldSiloConfig.MIN_LITERS_CHOICES = {5000, 10000, 25000, 50000}
|
||||
ADFieldSiloConfig.DEFAULT_MIN_LITERS = 10000
|
||||
-- foderreserve: staldenes forbrug i så mange døgn bliver på gården, før overskud køres til fabrik
|
||||
ADFieldSiloConfig.RESERVE_DAYS_CHOICES = {3, 7, 14, 30}
|
||||
ADFieldSiloConfig.DEFAULT_RESERVE_DAYS = 7
|
||||
|
||||
local function copy(cfg)
|
||||
local ids = {}
|
||||
for index, id in ipairs(cfg.storeIds or {}) do ids[index] = id end
|
||||
return {enabled = cfg.enabled == true, storeIds = ids, maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters,
|
||||
toProduction = cfg.toProduction == true, reserveDays = cfg.reserveDays}
|
||||
end
|
||||
|
||||
local function clampVehicles(value)
|
||||
value = tonumber(value) or ADFieldSiloConfig.DEFAULT_MAX_VEHICLES
|
||||
return math.max(1, math.min(ADFieldSiloConfig.MAX_VEHICLES, math.floor(value)))
|
||||
end
|
||||
|
||||
local function snap(value, choices, default)
|
||||
value = tonumber(value)
|
||||
if value == nil then return default end
|
||||
local best, bestDiff = nil, nil
|
||||
for _, choice in ipairs(choices) do
|
||||
local diff = math.abs(choice - value)
|
||||
if bestDiff == nil or diff < bestDiff then best, bestDiff = choice, diff end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function snapLiters(value)
|
||||
return snap(value, ADFieldSiloConfig.MIN_LITERS_CHOICES, ADFieldSiloConfig.DEFAULT_MIN_LITERS)
|
||||
end
|
||||
|
||||
local function snapDays(value)
|
||||
return snap(value, ADFieldSiloConfig.RESERVE_DAYS_CHOICES, ADFieldSiloConfig.DEFAULT_RESERVE_DAYS)
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.new()
|
||||
return {enabled = false, storeIds = {}, maxVehicles = ADFieldSiloConfig.DEFAULT_MAX_VEHICLES, minLiters = ADFieldSiloConfig.DEFAULT_MIN_LITERS,
|
||||
toProduction = false, reserveDays = ADFieldSiloConfig.DEFAULT_RESERVE_DAYS}
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.with(cfg, changes)
|
||||
local new = copy(cfg)
|
||||
if changes.enabled ~= nil then new.enabled = changes.enabled == true end
|
||||
if changes.maxVehicles ~= nil then new.maxVehicles = clampVehicles(changes.maxVehicles) end
|
||||
if changes.minLiters ~= nil then new.minLiters = snapLiters(changes.minLiters) end
|
||||
if changes.toProduction ~= nil then new.toProduction = changes.toProduction == true end
|
||||
if changes.reserveDays ~= nil then new.reserveDays = snapDays(changes.reserveDays) end
|
||||
return new
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.hasStore(cfg, id)
|
||||
for _, storeId in ipairs(cfg.storeIds or {}) do
|
||||
if storeId == id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.toggleStore(cfg, id)
|
||||
local new = copy(cfg)
|
||||
local kept, found = {}, false
|
||||
for _, storeId in ipairs(new.storeIds) do
|
||||
if storeId == id then found = true else table.insert(kept, storeId) end
|
||||
end
|
||||
if not found then table.insert(kept, id) end
|
||||
new.storeIds = kept
|
||||
return new
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.toRecord(cfg)
|
||||
return {enabled = cfg.enabled and 1 or 0, stores = table.concat(cfg.storeIds or {}, " "),
|
||||
maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters, toProduction = cfg.toProduction and 1 or 0,
|
||||
reserveDays = cfg.reserveDays}
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.fromRecord(record)
|
||||
local cfg = ADFieldSiloConfig.new()
|
||||
if record == nil then return cfg end
|
||||
for token in string.gmatch(tostring(record.stores or ""), "%S+") do table.insert(cfg.storeIds, token) end
|
||||
cfg.enabled = tonumber(record.enabled) == 1
|
||||
cfg.maxVehicles = clampVehicles(record.maxVehicles)
|
||||
cfg.minLiters = snapLiters(record.minLiters)
|
||||
cfg.toProduction = tonumber(record.toProduction) == 1
|
||||
cfg.reserveDays = snapDays(record.reserveDays)
|
||||
return cfg
|
||||
end
|
||||
@@ -0,0 +1,195 @@
|
||||
-- AD Field Silo Planner
|
||||
-- Ren planlægning for marksilo-flowet: hvilke (silo, vare) skal hjem, hvor mange lånte vogne sendes, og til
|
||||
-- hvilket gårdlager eller (med "kør til produktion") hvilken fabrik. Ingen spil-kald; input muteres ikke.
|
||||
|
||||
ADFieldSiloPlanner = {}
|
||||
|
||||
function ADFieldSiloPlanner.key(siloId, fillType)
|
||||
return tostring(siloId) .. ":" .. tostring(fillType)
|
||||
end
|
||||
local key = ADFieldSiloPlanner.key
|
||||
|
||||
local function dist2(a, b)
|
||||
return ((a.x or 0) - (b.x or 0)) ^ 2 + ((a.z or 0) - (b.z or 0)) ^ 2
|
||||
end
|
||||
|
||||
local function freeIn(store, fillType, reserved)
|
||||
return ((store.free or {})[fillType] or 0) - (reserved[store.id .. ":" .. tostring(fillType)] or 0)
|
||||
end
|
||||
|
||||
local function anyStoreTakes(stores, fillType, minLiters, reserved)
|
||||
for _, store in ipairs(stores or {}) do
|
||||
if store.markerId ~= nil and freeIn(store, fillType, reserved) >= minLiters then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function isProduction(store)
|
||||
return store ~= nil and store.kind == "production"
|
||||
end
|
||||
|
||||
-- Nærmeste lager af den ønskede slags (fabrik/gård) med plads til hele turen; ellers det med mest plads.
|
||||
local function chooseStore(stores, silo, fillType, liters, minLiters, reserved, production)
|
||||
local best, bestD, most, mostFree = nil, nil, nil, nil
|
||||
for _, store in ipairs(stores or {}) do
|
||||
local free = freeIn(store, fillType, reserved)
|
||||
if store.markerId ~= nil and free >= minLiters and isProduction(store) == production then
|
||||
local d = dist2(store, silo)
|
||||
if free >= liters and (bestD == nil or d < bestD) then best, bestD = store, d end
|
||||
if mostFree == nil or free > mostFree then most, mostFree = store, free end
|
||||
end
|
||||
end
|
||||
-- fabrikker kun med plads til hele læsset: en fabrik tager kun det den forbruger, så en vogn med resten
|
||||
-- holder ved den i timevis (in-game 28/9: 252.000 l kartofler ved en fuld snackfabrik)
|
||||
if production then return best end
|
||||
return best or most
|
||||
end
|
||||
|
||||
-- Tom silo: en lånt vogn med last, der har stået stille ved en tom marksilo i REST_IDLE_MS, kører resten videre
|
||||
-- (AutoDrive venter ellers til vognen er fuld). s = {atSilo, stillMs, siloStock, cargo}
|
||||
ADFieldSiloPlanner.REST_IDLE_MS = 30000
|
||||
ADFieldSiloPlanner.REST_MIN_LITERS = 100
|
||||
function ADFieldSiloPlanner.restDue(s)
|
||||
return s.atSilo == true and (s.stillMs or 0) >= ADFieldSiloPlanner.REST_IDLE_MS
|
||||
and (s.cargo or 0) >= ADFieldSiloPlanner.REST_MIN_LITERS and (s.siloStock or 0) < ADFieldSiloPlanner.REST_MIN_LITERS
|
||||
end
|
||||
|
||||
-- Fuld fabrik/lager: vognen med last har holdt ved sit lager i STORE_FULL_MS, og lageret har ikke plads.
|
||||
-- s = {atStore, stillMs, cargo, storeFree}
|
||||
ADFieldSiloPlanner.STORE_FULL_MS = 60000
|
||||
function ADFieldSiloPlanner.storeFullDue(s)
|
||||
return s.atStore == true and (s.stillMs or 0) >= ADFieldSiloPlanner.STORE_FULL_MS
|
||||
and (s.cargo or 0) >= ADFieldSiloPlanner.REST_MIN_LITERS and (s.storeFree or 0) < ADFieldSiloPlanner.REST_MIN_LITERS
|
||||
end
|
||||
|
||||
-- Lager til en rest: fabrik kun med plads til hele resten, ellers gårdlager (nærmeste med plads, ellers mest plads).
|
||||
-- excludeId: lageret vognen holder fast ved (fuldt).
|
||||
function ADFieldSiloPlanner.restStore(stores, silo, fillType, liters, excludeId)
|
||||
local candidates = {}
|
||||
for _, store in ipairs(stores or {}) do
|
||||
if store.id ~= excludeId then table.insert(candidates, store) end
|
||||
end
|
||||
return chooseStore(candidates, silo, fillType, liters, 1, {}, true) or chooseStore(candidates, silo, fillType, liters, 1, {}, false)
|
||||
end
|
||||
|
||||
local function canCarry(vehicle, fillType)
|
||||
if (vehicle.capacity or 0) <= 0 then return false end
|
||||
if vehicle.cargo ~= nil and vehicle.cargo ~= fillType then return false end
|
||||
return (vehicle.carries or {})[fillType] == true
|
||||
end
|
||||
|
||||
local function nearestVehicle(vehicles, used, silo, fillType)
|
||||
local best, bestD = nil, nil
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not used[vehicle.id] and canCarry(vehicle, fillType) then
|
||||
local d = dist2(vehicle, silo)
|
||||
if bestD == nil or d < bestD then best, bestD = vehicle, d end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Foder først: mangler gården stadig foderreserve for varen (reserve − beholdning − læs på vej hjem), køres til
|
||||
-- gårdlager; ellers til fabrik. Findes kun den ene slags, bruges den.
|
||||
local function pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||||
local fillType = task.fillType
|
||||
local short = ((input.feedReserve or {})[fillType] or 0) - ((input.farmStock or {})[fillType] or 0) - (farmIncoming[fillType] or 0)
|
||||
local wantProduction = short <= 0
|
||||
return chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, wantProduction)
|
||||
or chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, not wantProduction)
|
||||
end
|
||||
|
||||
-- Høsten først: for hvert behov {fillType, count} (høstere i arbejde, der mangler tømmevogne) holdes så mange
|
||||
-- vogne der kan bære afgrøden tilbage; resten må marksilo-flowet låne. Ny liste; input muteres ikke.
|
||||
function ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
|
||||
local held = {}
|
||||
for _, need in ipairs(needs or {}) do
|
||||
local left = need.count or 0
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if left <= 0 then break end
|
||||
if not held[vehicle.id] and canCarry(vehicle, need.fillType) then
|
||||
held[vehicle.id] = true
|
||||
left = left - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local free = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not held[vehicle.id] then table.insert(free, vehicle) end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
function ADFieldSiloPlanner.plan(input)
|
||||
local minLiters = input.minLiters or 10000
|
||||
local storesById = {}
|
||||
for _, store in ipairs(input.stores or {}) do storesById[store.id] = store end
|
||||
local reserved, onWay, farmIncoming = {}, {}, {}
|
||||
for _, loan in ipairs(input.loans or {}) do
|
||||
local k = key(loan.siloId, loan.fillType)
|
||||
onWay[k] = (onWay[k] or 0) + (loan.liters or 0)
|
||||
if loan.storeId ~= nil and not isProduction(storesById[loan.storeId]) then
|
||||
farmIncoming[loan.fillType] = (farmIncoming[loan.fillType] or 0) + (loan.liters or 0)
|
||||
end
|
||||
-- læs på vej til et lager optager pladsen, til de er aflæsset (også i senere runder)
|
||||
if loan.storeId ~= nil then
|
||||
local reserveKey = loan.storeId .. ":" .. tostring(loan.fillType)
|
||||
reserved[reserveKey] = (reserved[reserveKey] or 0) + (loan.liters or 0)
|
||||
end
|
||||
end
|
||||
local status, tasks, statusByKey = {}, {}, {}
|
||||
for _, silo in ipairs(input.silos or {}) do
|
||||
for fillType, liters in pairs(silo.stock or {}) do
|
||||
if (liters or 0) >= minLiters then
|
||||
local k = key(silo.id, fillType)
|
||||
local entry = {siloId = silo.id, siloName = silo.name, fillType = fillType, liters = liters, onWay = onWay[k] or 0}
|
||||
if silo.markerId == nil then
|
||||
entry.state = "noMarker"
|
||||
elseif not anyStoreTakes(input.stores, fillType, minLiters, reserved) then
|
||||
entry.state = "noFarmStore"
|
||||
elseif (input.paused or {})[k] then
|
||||
entry.state = "paused"
|
||||
else
|
||||
entry.state = "ok"
|
||||
local rest = liters - entry.onWay
|
||||
if rest >= minLiters then table.insert(tasks, {silo = silo, fillType = fillType, rest = rest}) end
|
||||
end
|
||||
table.insert(status, entry)
|
||||
statusByKey[k] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(tasks, function(a, b)
|
||||
if a.rest ~= b.rest then return a.rest > b.rest end
|
||||
if tostring(a.silo.id) ~= tostring(b.silo.id) then return tostring(a.silo.id) < tostring(b.silo.id) end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
table.sort(status, function(a, b)
|
||||
if tostring(a.siloId) ~= tostring(b.siloId) then return tostring(a.siloId) < tostring(b.siloId) end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
local freeSlots = math.max(0, (input.maxVehicles or 2) - #(input.loans or {}))
|
||||
local used, dispatch = {}, {}
|
||||
for _, task in ipairs(tasks) do
|
||||
while freeSlots > 0 and task.rest >= minLiters do
|
||||
local vehicle = nearestVehicle(input.vehicles, used, task.silo, task.fillType)
|
||||
if vehicle == nil then break end
|
||||
local liters = math.min(vehicle.capacity, task.rest)
|
||||
local store = pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||||
if store == nil then break end
|
||||
if isProduction(store) then
|
||||
statusByKey[key(task.silo.id, task.fillType)].toProduction = true
|
||||
else
|
||||
farmIncoming[task.fillType] = (farmIncoming[task.fillType] or 0) + liters
|
||||
end
|
||||
used[vehicle.id] = true
|
||||
local reserveKey = store.id .. ":" .. tostring(task.fillType)
|
||||
reserved[reserveKey] = (reserved[reserveKey] or 0) + liters
|
||||
table.insert(dispatch, {vehicleId = vehicle.id, siloId = task.silo.id, siloMarkerId = task.silo.markerId,
|
||||
fillType = task.fillType, storeId = store.id, storeMarkerId = store.markerId, liters = liters})
|
||||
task.rest = task.rest - liters
|
||||
freeSlots = freeSlots - 1
|
||||
end
|
||||
end
|
||||
return {dispatch = dispatch, status = status, freeSlots = freeSlots}
|
||||
end
|
||||
@@ -0,0 +1,720 @@
|
||||
-- AD Field Silos
|
||||
-- Marksilo-flowet i spillet: samler marksiloer, gårdlagre og ledige vogne, lader ADFieldSiloPlanner vælge,
|
||||
-- med "kør til produktion" går overskud ud over gårdens foderreserve til gårdens fabrikker;
|
||||
-- låner vogne (AutoDrive "Hent og aflever", én tur, parkering bagefter) og frigiver dem igen.
|
||||
-- Lån ligger kun i hukommelsen; opsætningen gemmes i ADSmartPickup_fieldsilos.xml.
|
||||
|
||||
ADFieldSilos = {}
|
||||
|
||||
ADFieldSilos.PLAN_INTERVAL_MS = 60000
|
||||
ADFieldSilos.CHECK_INTERVAL_MS = 2000
|
||||
ADFieldSilos.STUCK_MS = 120000
|
||||
ADFieldSilos.PAUSE_MS = 600000
|
||||
ADFieldSilos.FAIL_MS = 30000 -- AD slukket inden for 30 s uden fremdrift = turen kunne ikke køres
|
||||
ADFieldSilos.CARGO_MIN_LITERS = 100 -- rest under dette tæller ikke som last
|
||||
-- Silo så tæt på et gårdlager er en del af gården (Farma-siloer deler lager inden for 50 m): ikke marksilo.
|
||||
-- (in-game 27/9: sukkerroer blev kørt fra én Farma-silo til nabosiloen i samme klynge)
|
||||
ADFieldSilos.FARM_RADIUS = 100
|
||||
-- Siloer højst så langt fra en nabo hører til samme klynge (Farma-rækker: 15 m mellem siloer, 18 m mellem rækker)
|
||||
ADFieldSilos.CLUSTER_LINK = 60
|
||||
ADFieldSilos.REST_SILO_DISTANCE = 40 -- m fra marksiloens markør: vognen holder ved siloen
|
||||
ADFieldSilos.FETCH_TIMEOUT_MS = 300000 -- så længe må en traktor være om at hente en puljevogn
|
||||
ADFieldSilos.MOVING_SPEED = 0.00028 -- m/ms, samme grænse som AutoDrive
|
||||
ADFieldSilos.FILE_NAME = "ADSmartPickup_fieldsilos.xml"
|
||||
ADFieldSilos.ROOT = "ADSmartPickupFieldSilos"
|
||||
ADFieldSilos.LOG_PREFIX = "ADSmartPickup: marksilo: "
|
||||
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
ADFieldSilos.lastPlan = nil
|
||||
local loans = setmetatable({}, {__mode = "k"}) -- vehicle -> {siloId, siloName, fillType, liters, startedMs, lastProgressMs, lastFill}
|
||||
local paused = {} -- key -> untilMs
|
||||
local planTimerMs, checkTimerMs, nowMs = ADFieldSilos.PLAN_INTERVAL_MS, 0, 0
|
||||
|
||||
-- ---- ren frigivelsesregel ----
|
||||
|
||||
-- vehicleState = {adActive, controlled, fill, moving}. Fremdrift = kører, eller fyldningen ændrer sig (læsser
|
||||
-- eller aflæsser). Vagten gælder kun før turen er kommet i gang — kø ved siloen/lageret er normalt.
|
||||
function ADFieldSilos.releaseReason(loan, vehicleState, now)
|
||||
if vehicleState.controlled then return "player" end
|
||||
local progressing = vehicleState.moving or (vehicleState.fill or 0) ~= (loan.lastFill or 0)
|
||||
local hasProgressed = loan.hasProgressed or progressing
|
||||
local age = now - (loan.startedMs or 0)
|
||||
if not vehicleState.adActive then
|
||||
return (not hasProgressed and age < ADFieldSilos.FAIL_MS) and "failed" or "done"
|
||||
end
|
||||
if hasProgressed then return nil end
|
||||
if age > ADFieldSilos.STUCK_MS then return "stuck" end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ---- opsætning ----
|
||||
|
||||
function ADFieldSilos.setConfig(cfg)
|
||||
local wasEnabled = ADFieldSilos.config.enabled
|
||||
ADFieldSilos.config = cfg
|
||||
if wasEnabled and not cfg.enabled then ADFieldSilos.stopAll(ADFieldSilos.adEnv) end
|
||||
planTimerMs = ADFieldSilos.PLAN_INTERVAL_MS -- planlæg straks ved næste tick
|
||||
end
|
||||
|
||||
function ADFieldSilos.isLoaned(vehicle)
|
||||
return loans[vehicle] ~= nil
|
||||
end
|
||||
|
||||
function ADFieldSilos.loanCount()
|
||||
local n = 0
|
||||
for _ in pairs(loans) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
-- ---- spil-input ----
|
||||
|
||||
local function farmId()
|
||||
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||||
end
|
||||
|
||||
local function position(node)
|
||||
if node == nil then return 0, 0 end
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function siloStock(placeable)
|
||||
local stock = {}
|
||||
local spec = placeable ~= nil and placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
for fillType, level in pairs(storage.fillLevels or {}) do
|
||||
if (level or 0) > 0 then stock[fillType] = (stock[fillType] or 0) + level end
|
||||
end
|
||||
end
|
||||
return stock
|
||||
end
|
||||
|
||||
local function siloFree(placeable)
|
||||
local free = {}
|
||||
local spec = placeable ~= nil and placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
for fillType in pairs(storage.fillTypes or {}) do
|
||||
local amount = storage.getFreeCapacity ~= nil and (storage:getFreeCapacity(fillType) or 0) or 0
|
||||
free[fillType] = (free[fillType] or 0) + amount
|
||||
end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
local function firstMarker(building, role)
|
||||
local markers = ADBuildings.getMarkers(building, role)
|
||||
return markers[1] ~= nil and markers[1].id or nil
|
||||
end
|
||||
|
||||
-- Bygninger der er kilde, mål eller lager i et flow: dem tømmer marksilo-flowet ikke (de hører til den drift).
|
||||
local function runBuildingIds()
|
||||
local ids = {}
|
||||
local store = ADRunsController ~= nil and ADRunsController.store or nil
|
||||
for _, run in ipairs(store ~= nil and store.runs or {}) do
|
||||
for _, list in ipairs({run.sourceBuildingIds or {}, run.targetBuildingIds or {}, run.storeBuildingIds or {}}) do
|
||||
for _, id in ipairs(list) do ids[id] = true end
|
||||
end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- Alle siloer i samme klynge som et gårdlager (kæde af naboer ≤ CLUSTER_LINK): gårdens, aldrig marksiloer.
|
||||
-- (in-game 27/9: gårdens Farma-klynge er ~300 m lang; siloer over 100 m fra et valgt lager blev marksiloer)
|
||||
local function farmCluster(allSilos, storeIds)
|
||||
local farm, queue = {}, {}
|
||||
for _, silo in ipairs(allSilos) do
|
||||
if storeIds[silo.id] then farm[silo.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
local link2 = ADFieldSilos.CLUSTER_LINK ^ 2
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue)
|
||||
for _, silo in ipairs(allSilos) do
|
||||
if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= link2 then
|
||||
farm[silo.id] = true
|
||||
table.insert(queue, silo)
|
||||
end
|
||||
end
|
||||
end
|
||||
return farm
|
||||
end
|
||||
|
||||
local function nearStore(stores, x, z)
|
||||
for _, store in ipairs(stores) do
|
||||
if (store.x - x) ^ 2 + (store.z - z) ^ 2 <= ADFieldSilos.FARM_RADIUS ^ 2 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Fabrikkens input-varer (dem aflæsningen tager imod) med fri plads i dens lager.
|
||||
local function factoryFree(placeable)
|
||||
local spec = placeable ~= nil and placeable.spec_productionPoint or nil
|
||||
local point = spec ~= nil and spec.productionPoint or nil
|
||||
local storage = point ~= nil and point.storage or nil
|
||||
if storage == nil or storage.getFreeCapacity == nil then return {} end
|
||||
local inputs = {}
|
||||
for fillType in pairs(point.inputFillTypeIds or {}) do inputs[fillType] = true end
|
||||
for _, production in ipairs(point.productions or {}) do
|
||||
for _, input in ipairs(production.inputs or {}) do inputs[input.type] = true end
|
||||
end
|
||||
local free = {}
|
||||
for fillType in pairs(inputs) do free[fillType] = storage:getFreeCapacity(fillType) or 0 end
|
||||
return free
|
||||
end
|
||||
|
||||
-- "Kør til produktion": gårdens fabrikker med aflæsningsmarkør bliver mål ved siden af gårdlagrene.
|
||||
local function addFactories(stores, buildings)
|
||||
for _, building in ipairs(buildings) do
|
||||
local markerId = building.kind == "factory" and firstMarker(building, "unload") or nil
|
||||
if markerId ~= nil then
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(stores, {id = building.id, name = building.name, kind = "production", markerId = markerId,
|
||||
x = x, z = z, free = factoryFree(building.placeable)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Foderreserve (staldenes døgnforbrug × døgn) og gårdlagrenes beholdning pr. vare.
|
||||
function ADFieldSilos.collectFeed(stores, byId, fid, reserveDays)
|
||||
local reserve, stock = {}, {}
|
||||
local rates = ADSources ~= nil and ADSources.getFarmRates ~= nil and ADSources.getFarmRates(ADFieldSilos.adEnv, fid, true) or {}
|
||||
for fillType, rate in pairs(rates) do reserve[fillType] = (rate.consumption or 0) * reserveDays end
|
||||
local counted = {} -- Farma-siloer i samme klynge deler lager: tæl hvert lager én gang
|
||||
for _, store in ipairs(stores) do
|
||||
local building = byId[store.id]
|
||||
local spec = store.kind ~= "production" and building ~= nil and building.placeable ~= nil and building.placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
if not counted[storage] then
|
||||
counted[storage] = true
|
||||
for fillType, level in pairs(storage.fillLevels or {}) do stock[fillType] = (stock[fillType] or 0) + (level or 0) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return reserve, stock
|
||||
end
|
||||
|
||||
-- Gårdlagre fra opsætningen + marksiloer (alle markers marksiloer, uden dubletter og uden gårdlagre).
|
||||
function ADFieldSilos.collectBuildings(adEnv)
|
||||
local fid = farmId()
|
||||
local stores, byId, allSilos, storeIds = {}, {}, {}, {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, fid)) do
|
||||
byId[building.id] = building
|
||||
if building.kind == "silo" then
|
||||
local sx, sz = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(allSilos, {id = building.id, x = sx, z = sz})
|
||||
if ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then storeIds[building.id] = true end
|
||||
end
|
||||
if building.kind == "silo" and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(stores, {id = building.id, name = building.name, markerId = firstMarker(building, "unload"),
|
||||
x = x, z = z, free = siloFree(building.placeable)})
|
||||
end
|
||||
end
|
||||
if ADFieldSilos.config.toProduction then addFactories(stores, ADBuildings.list(adEnv, fid)) end
|
||||
local silos, seen = {}, runBuildingIds()
|
||||
for id in pairs(farmCluster(allSilos, storeIds)) do seen[id] = true end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, fid, info)) do
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
if not seen[building.id] and not ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) and not nearStore(stores, x, z) then
|
||||
seen[building.id] = true
|
||||
-- marknummeret først: navnet skæres af i fanen, og mange siloer hedder ens ("Farma 500 …")
|
||||
-- marknummeret først (medmindre navnet allerede starter med det, fx "F22 marksilo")
|
||||
local siloName = tostring(building.name)
|
||||
if not siloName:match("^F" .. tostring(info.id) .. "%f[%D]") then siloName = string.format("F%s %s", tostring(info.id), siloName) end
|
||||
table.insert(silos, {id = building.id, name = siloName, markerId = firstMarker(building, "load"),
|
||||
x = x, z = z, stock = siloStock(building.placeable)})
|
||||
end
|
||||
end
|
||||
end
|
||||
return silos, stores, byId
|
||||
end
|
||||
|
||||
-- Marksiloernes placeables som sæt (til at udelukke dem som tømmested — de er til markens høst).
|
||||
function ADFieldSilos.fieldSiloPlaceableSet(adEnv)
|
||||
local set = {}
|
||||
local silos, _, byId = ADFieldSilos.collectBuildings(adEnv)
|
||||
for _, silo in ipairs(silos or {}) do
|
||||
local building = byId[silo.id]
|
||||
if building ~= nil and building.placeable ~= nil then set[building.placeable] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
local function trailerInfo(adEnv, vehicle)
|
||||
local capacity, carries, cargo = 0, {}, nil
|
||||
for _, unit in ipairs(adEnv.AutoDrive.getAllDischargeableUnits(vehicle) or {}) do
|
||||
local object, index = unit.object, unit.fillUnitIndex
|
||||
if object ~= nil and object.getFillUnitCapacity ~= nil then
|
||||
capacity = capacity + (object:getFillUnitCapacity(index) or 0)
|
||||
for fillType in pairs(object.getFillUnitSupportedFillTypes ~= nil and object:getFillUnitSupportedFillTypes(index) or {}) do
|
||||
carries[fillType] = true
|
||||
end
|
||||
if (object:getFillUnitFillLevel(index) or 0) >= ADFieldSilos.CARGO_MIN_LITERS then cargo = object:getFillUnitFillType(index) end
|
||||
end
|
||||
end
|
||||
return capacity, carries, cargo
|
||||
end
|
||||
|
||||
local function fillLevel(adEnv, vehicle)
|
||||
local ok, level = pcall(function() return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle))) end)
|
||||
return ok and tonumber(level) or 0
|
||||
end
|
||||
|
||||
function ADFieldSilos.collectVehicles(adEnv)
|
||||
local vehicles, byId = {}, {}
|
||||
-- hvorfor tømmevogne ikke kan lånes (til fanens statuslinje)
|
||||
local why = {notEnabled = 0, busy = 0, controlled = 0, noPark = 0}
|
||||
local rigs = ADFieldWork.getRigs(adEnv)
|
||||
for _, rig in ipairs(rigs) do
|
||||
-- uden AD-parkeringspunkt bliver vognen holdende på lagerets markør og spærrer for de næste
|
||||
local canPark = ADFieldWork.parkMarkerId == nil or ADFieldWork.parkMarkerId(adEnv, rig.vehicle) ~= nil
|
||||
if rig.role == "unloader" then
|
||||
if not rig.enabled then why.notEnabled = why.notEnabled + 1
|
||||
elseif rig.controlled then why.controlled = why.controlled + 1
|
||||
elseif rig.busy then why.busy = why.busy + 1
|
||||
elseif not canPark then why.noPark = why.noPark + 1 end
|
||||
end
|
||||
if rig.role == "unloader" and rig.enabled and not rig.busy and not rig.controlled and canPark then
|
||||
local capacity, carries, cargo = trailerInfo(adEnv, rig.vehicle)
|
||||
table.insert(vehicles, {id = rig.id, x = rig.x, z = rig.z, capacity = capacity, carries = carries, cargo = cargo})
|
||||
byId[rig.id] = rig.vehicle
|
||||
end
|
||||
end
|
||||
-- puljevogne: en ledig solo-traktor henter vognen først (maskinpulje etape 3)
|
||||
local poolById = {}
|
||||
if ADPool ~= nil then
|
||||
local okPool, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
||||
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
||||
end)
|
||||
if okPool then
|
||||
for _, pair in ipairs(ADPool.pairAll("unloader", implements, tractors, ADPool.blocked(ADFieldJobs.now or 0))) do
|
||||
local entryId = "pool:" .. tostring(pair.implement.id)
|
||||
table.insert(vehicles, {id = entryId, x = pair.implement.x, z = pair.implement.z,
|
||||
capacity = pair.implement.capacity or 0, carries = pair.implement.carries or {}})
|
||||
byId[entryId] = pair.tractor.vehicle
|
||||
poolById[entryId] = pair
|
||||
end
|
||||
else
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "maskinpuljen kunne ikke læses: %s", tostring(implements))
|
||||
end
|
||||
end
|
||||
local ok, needs = pcall(ADFieldSilos.harvestNeeds, adEnv, rigs)
|
||||
if ok and #needs > 0 then
|
||||
local before = #vehicles
|
||||
vehicles = ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
|
||||
why.harvest = before - #vehicles
|
||||
elseif not ok then
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "høstreserve kunne ikke beregnes: %s", tostring(needs))
|
||||
end
|
||||
why.eligible = #vehicles
|
||||
ADFieldSilos.lastVehicleReasons = why
|
||||
return vehicles, byId, poolById
|
||||
end
|
||||
|
||||
-- Høsten først: pr. høster der arbejder (CP kører) holdes dens planlagte antal tømmevogne tilbage, minus dem
|
||||
-- markarbejdet allerede har sendt til marken. (in-game 28/9: marksilo-flowet havde lånt begge kartoffelvogne,
|
||||
-- og ingen kom til roeoptagerne.)
|
||||
function ADFieldSilos.harvestNeeds(adEnv, rigs)
|
||||
local assignments = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.assignments or {}
|
||||
local sent = {}
|
||||
for _, assignment in pairs(assignments) do
|
||||
if assignment.role == "unloader" and assignment.fieldId ~= nil then sent[assignment.fieldId] = (sent[assignment.fieldId] or 0) + 1 end
|
||||
end
|
||||
local needs = {}
|
||||
for _, rig in ipairs(rigs) do
|
||||
if rig.role == "harvester" and ADCpBridge.isWorking(rig.vehicle) then
|
||||
local assignment = assignments[rig.id]
|
||||
local fieldId = assignment ~= nil and assignment.fieldId or nil
|
||||
local field = fieldId ~= nil and ADFieldJobs.getField(fieldId) or nil
|
||||
local info = fieldId ~= nil and ADFields.getInfo(fieldId) or nil
|
||||
local count = field ~= nil and ADFieldModel.maxHelpers("harvesting", field.plan) or 1
|
||||
count = count - (fieldId ~= nil and (sent[fieldId] or 0) or 0)
|
||||
local fillTypes = {}
|
||||
if info ~= nil and info.harvestFillType ~= nil then
|
||||
fillTypes[info.harvestFillType] = true
|
||||
else
|
||||
for fruitIndex in pairs(rig.fruits or {}) do
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
|
||||
if fruit ~= nil and fruit.fillType ~= nil then fillTypes[fruit.fillType.index] = true end
|
||||
end
|
||||
end
|
||||
for fillType in pairs(fillTypes) do
|
||||
if count > 0 then table.insert(needs, {fillType = fillType, count = count}) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return needs
|
||||
end
|
||||
|
||||
-- ---- lån ----
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
local desc = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||||
return desc ~= nil and (desc.title or desc.name) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function dispatchOne(adEnv, order, vehicle, silosById, storesById, isPool)
|
||||
local module = vehicle.ad.stateModule
|
||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||
module:setFirstMarkerByWayPointId(order.siloMarkerId)
|
||||
module:setSecondMarkerByWayPointId(order.storeMarkerId)
|
||||
module.selectedFillTypes = {order.fillType}
|
||||
module:setFillType(order.fillType)
|
||||
module.loopCounter = 1
|
||||
if module.setLoopsDone ~= nil then module:setLoopsDone(0) end
|
||||
-- puljevogn: ingen AD-parkering — ved turens slut sætter returnHome vognen af og kører traktoren hjem
|
||||
-- (med AD-parkering kørte den først hjem med vognen og så tilbage efter den)
|
||||
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
|
||||
-- rotation fra: ellers kan modens egne pickup-hooks sende vognen til en anden silo i mappen
|
||||
if ADRunsController ~= nil and ADRunsController.setSetting ~= nil then
|
||||
ADRunsController.setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_NONE)
|
||||
end
|
||||
module:raiseDirtyFlag()
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_PICKUPANDDELIVER, order.siloMarkerId, order.storeMarkerId, order.fillType)
|
||||
local silo, store = silosById[order.siloId], storesById[order.storeId]
|
||||
loans[vehicle] = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId, fillType = order.fillType,
|
||||
liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle)}
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter %d l %s i %s -> %s", tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId),
|
||||
tostring(store ~= nil and store.name or order.storeId))
|
||||
end
|
||||
|
||||
-- Puljevogn: traktoren henter vognen ved "Connect X" og kører derefter turen som en almindelig tømmevogn.
|
||||
local function dispatchPool(adEnv, order, pair, silosById, storesById)
|
||||
local tractor = pair.tractor.vehicle
|
||||
local silo = silosById[order.siloId]
|
||||
local owner = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId,
|
||||
fillType = order.fillType, liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = 0, fetching = true}
|
||||
loans[tractor] = owner
|
||||
ADHitch.fetch(adEnv, tractor, pair.implement.connectId, function(_, ok, detail)
|
||||
ADPool.release(owner)
|
||||
if loans[tractor] ~= owner then return end -- lånet blev afbrudt imens (Stop alt / flow slået fra)
|
||||
if not ok then
|
||||
loans[tractor] = nil
|
||||
local blamed = ADPool.failureBlames(detail) == "tractor" and pair.tractor.id or pair.implement.id
|
||||
ADPool.quarantine(blamed, ADFieldJobs.now or 0)
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke hente puljevognen '%s' (%s) — prøver en anden om lidt",
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(pair.implement.name), tostring(detail))
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, tractor)
|
||||
if park ~= nil then ADFieldWork.startAd(adEnv, tractor, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
return
|
||||
end
|
||||
pcall(ADHitch.foldForTransport, tractor)
|
||||
local okDispatch, err = pcall(dispatchOne, adEnv, order, tractor, silosById, storesById, true)
|
||||
if not okDispatch then
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke starte turen med puljevognen: %s", tostring(ADRuns.vehicleLabel(tractor)), tostring(err))
|
||||
ADFieldSilos.releaseVehicle(adEnv, tractor, "failed")
|
||||
end
|
||||
end)
|
||||
-- reservationen først når hentningen er sat i gang (fejler fetch, er der intet at frigive)
|
||||
ADPool.reserve(owner, {pair.implement.id, pair.tractor.id})
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter puljevognen '%s' til %d l %s i %s", tostring(ADRuns.vehicleLabel(tractor)),
|
||||
tostring(pair.implement.name), math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId))
|
||||
end
|
||||
|
||||
local function release(adEnv, vehicle, reason)
|
||||
local loan = loans[vehicle]
|
||||
loans[vehicle] = nil
|
||||
if loan == nil then return end
|
||||
if loan.fetching then
|
||||
-- henter stadig puljevognen: afbryd koblingen og frigiv reservationen
|
||||
if ADHitch ~= nil then ADHitch.cancel(vehicle) end
|
||||
if ADPool ~= nil then ADPool.release(loan) end
|
||||
end
|
||||
if reason == "stuck" or reason == "failed" then
|
||||
local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType)
|
||||
paused[k] = nowMs + ADFieldSilos.PAUSE_MS
|
||||
pcall(vehicle.stopAutoDrive, vehicle)
|
||||
local why = reason == "stuck" and "kom ikke i gang på 2 min — stoppet" or "AutoDrive stoppede turen med det samme (rute/læssested?)"
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' ved %s (%s): %s; siloen springes over i 10 min",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(loan.siloName), fillTypeName(loan.fillType), why)
|
||||
elseif reason == "stopped" then
|
||||
pcall(vehicle.stopAutoDrive, vehicle)
|
||||
end
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' frigivet (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(reason))
|
||||
-- puljevogn bag traktoren: sæt den af på sin plads og kør hjem (ikke når spilleren har overtaget)
|
||||
if reason ~= "player" and ADHitch ~= nil then
|
||||
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
|
||||
if not (okHome and pooled) and loan.fetching then
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldSilos.releaseVehicle(adEnv, vehicle, reason)
|
||||
release(adEnv, vehicle, reason)
|
||||
end
|
||||
|
||||
function ADFieldSilos.stopAll(adEnv)
|
||||
local list = {}
|
||||
for vehicle in pairs(loans) do table.insert(list, vehicle) end
|
||||
for _, vehicle in ipairs(list) do release(adEnv, vehicle, "stopped") end
|
||||
end
|
||||
|
||||
-- Efter genindlæsning genoptager AutoDrive selv "Hent og aflever" fra marksiloerne, men lånene er glemt: sådanne
|
||||
-- vogne overtages igen som lån (tæller i "højst x ude", og tom silo-vagten gælder).
|
||||
function ADFieldSilos.adoptOrphans(adEnv, silos)
|
||||
local byMarker = {}
|
||||
for _, silo in ipairs(silos or {}) do
|
||||
if silo.markerId ~= nil then byMarker[silo.markerId] = silo end
|
||||
end
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if loans[vehicle] == nil and module ~= nil and module:isActive() and module:getMode() == adEnv.AutoDrive.MODE_PICKUPANDDELIVER then
|
||||
local first = module:getFirstMarker()
|
||||
local silo = first ~= nil and byMarker[first.id] or nil
|
||||
if silo ~= nil then
|
||||
local fillType = module.getFillType ~= nil and module:getFillType() or nil
|
||||
local second = module.getSecondMarker ~= nil and module:getSecondMarker() or nil
|
||||
local storeId = nil
|
||||
for _, store in ipairs(ADFieldSilos.lastStores or {}) do
|
||||
if second ~= nil and store.markerId == second.id then storeId = store.id end
|
||||
end
|
||||
loans[vehicle] = {siloId = silo.id, storeId = storeId, siloName = silo.name, fillType = fillType, liters = 0, startedMs = nowMs,
|
||||
lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle), hasProgressed = true, adopted = true}
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' overtaget igen (AutoDrive kører fra %s efter genindlæsning)",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(silo.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Tom silo-vagt: en lånt vogn med last, der står stille ved en tom marksilo, kører resten videre — til en fabrik
|
||||
-- med plads til hele resten, ellers til et gårdlager (AutoDrive venter ellers til vognen er fuld).
|
||||
local warnedNoRestStore = {}
|
||||
local function isNear(adEnv, vehicle, markerId)
|
||||
local wayPoint = markerId ~= nil and adEnv.ADGraphManager:getWayPointById(markerId) or nil
|
||||
if wayPoint == nil or vehicle.rootNode == nil then return false end
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return (x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2 <= ADFieldSilos.REST_SILO_DISTANCE ^ 2
|
||||
end
|
||||
|
||||
local function findById(list, id)
|
||||
for _, item in ipairs(list or {}) do
|
||||
if item.id == id then return item end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function checkRest(adEnv, vehicle, loan, moving, fill)
|
||||
if loan.fetching then return end
|
||||
local silo = findById(ADFieldSilos.lastSilos, loan.siloId)
|
||||
if silo == nil then return end
|
||||
if moving then loan.stillSince = nil return end
|
||||
loan.stillSince = loan.stillSince or nowMs
|
||||
local stillMs = nowMs - loan.stillSince
|
||||
local fullStore = nil
|
||||
if not loan.delivering and isNear(adEnv, vehicle, silo.markerId) then
|
||||
-- tom marksilo med last på
|
||||
local state = {atSilo = true, stillMs = stillMs, siloStock = (silo.stock or {})[loan.fillType] or 0, cargo = fill}
|
||||
if not ADFieldSiloPlanner.restDue(state) then return end
|
||||
else
|
||||
-- fuldt lager (fabrik der kun tager sit forbrug) med last på
|
||||
local current = findById(ADFieldSilos.lastStores, loan.storeId)
|
||||
if current == nil or not isNear(adEnv, vehicle, current.markerId) then return end
|
||||
local state = {atStore = true, stillMs = stillMs, cargo = fill, storeFree = (current.free or {})[loan.fillType] or 0}
|
||||
if not ADFieldSiloPlanner.storeFullDue(state) then return end
|
||||
fullStore = current
|
||||
end
|
||||
local store = ADFieldSiloPlanner.restStore(ADFieldSilos.lastStores, silo, loan.fillType, fill, fullStore ~= nil and fullStore.id or nil)
|
||||
if store == nil then
|
||||
if not warnedNoRestStore[vehicle] then
|
||||
warnedNoRestStore[vehicle] = true
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' har %d l %s fra tom %s, men intet lager har plads",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(fill), fillTypeName(loan.fillType), tostring(silo.name))
|
||||
end
|
||||
return
|
||||
end
|
||||
warnedNoRestStore[vehicle] = nil
|
||||
loan.delivering, loan.storeId, loan.stillSince = true, store.id, nil
|
||||
loan.startedMs, loan.hasProgressed = nowMs, true
|
||||
local isPool = ADHitch ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, store.markerId, nil, loan.fillType)
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s': %s — kører resten (%d l %s) til %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), fullStore ~= nil and (tostring(fullStore.name) .. " er fuld") or (tostring(silo.name) .. " er tom"),
|
||||
math.floor(fill), fillTypeName(loan.fillType), tostring(store.name))
|
||||
end
|
||||
|
||||
local function checkLoans(adEnv)
|
||||
local list = {}
|
||||
for vehicle in pairs(loans) do table.insert(list, vehicle) end
|
||||
for _, vehicle in ipairs(list) do
|
||||
if vehicle.isDeleted then
|
||||
loans[vehicle] = nil -- solgt/slettet mens den var lånt
|
||||
else
|
||||
local ok, err = pcall(function()
|
||||
local loan = loans[vehicle]
|
||||
-- henter puljevognen: AD holder stille under koblingen — ikke "færdig"; men spilleren kan overtage,
|
||||
-- og en hentning der ikke bliver færdig (trafik, rute), opgives
|
||||
if loan.fetching then
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false
|
||||
if controlled then
|
||||
release(adEnv, vehicle, "player")
|
||||
elseif nowMs - (loan.startedMs or nowMs) > ADFieldSilos.FETCH_TIMEOUT_MS then
|
||||
release(adEnv, vehicle, "stuck")
|
||||
end
|
||||
return
|
||||
end
|
||||
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local fill = fillLevel(adEnv, vehicle)
|
||||
local vehicleState = {
|
||||
adActive = stateModule ~= nil and stateModule:isActive(),
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false,
|
||||
fill = fill, moving = (vehicle.lastSpeedReal or 0) >= ADFieldSilos.MOVING_SPEED,
|
||||
}
|
||||
checkRest(adEnv, vehicle, loan, vehicleState.moving, fill)
|
||||
if loans[vehicle] ~= loan then return end
|
||||
vehicleState.adActive = stateModule ~= nil and stateModule:isActive()
|
||||
local reason = ADFieldSilos.releaseReason(loan, vehicleState, nowMs)
|
||||
if vehicleState.moving or fill ~= (loan.lastFill or 0) then loan.hasProgressed = true end
|
||||
loan.lastFill = fill
|
||||
if reason ~= nil then release(adEnv, vehicle, reason) end
|
||||
end)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "lån opgivet (fejl): %s", tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function currentPaused()
|
||||
local result = {}
|
||||
for k, untilMs in pairs(paused) do
|
||||
if untilMs > nowMs then result[k] = true else paused[k] = nil end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function loanList()
|
||||
local list = {}
|
||||
for _, loan in pairs(loans) do table.insert(list, {siloId = loan.siloId, storeId = loan.storeId, fillType = loan.fillType, liters = loan.liters}) end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Rute fra vognen til siloen og videre til lageret (samme opslag som Wait-punkterne); én advarsel pr. kombination.
|
||||
local warnedRoute = {}
|
||||
local function hasRoutes(adEnv, vehicle, order)
|
||||
if ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil then return true end
|
||||
local ok = ADUnloadWait.hasRouteTo(adEnv, vehicle, order.siloMarkerId) and ADUnloadWait.hasRouteTo(adEnv, vehicle, order.storeMarkerId)
|
||||
local warnKey = tostring(order.vehicleId) .. ">" .. tostring(order.siloMarkerId)
|
||||
if not ok and not warnedRoute[warnKey] then
|
||||
warnedRoute[warnKey] = true
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "ingen AutoDrive-rute for '%s' til marksilo-markør %s eller gårdlager-markør %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(order.siloMarkerId), tostring(order.storeMarkerId))
|
||||
end
|
||||
return ok
|
||||
end
|
||||
|
||||
function ADFieldSilos.planNow(adEnv)
|
||||
local cfg = ADFieldSilos.config
|
||||
local silos, stores, byId = ADFieldSilos.collectBuildings(adEnv)
|
||||
ADFieldSilos.lastSilos, ADFieldSilos.lastStores = silos, stores
|
||||
if cfg.enabled then
|
||||
local okAdopt, errAdopt = pcall(ADFieldSilos.adoptOrphans, adEnv, silos)
|
||||
if not okAdopt then Logging.warning(ADFieldSilos.LOG_PREFIX .. "overtagelse efter genindlæsning fejlede: %s", tostring(errAdopt)) end
|
||||
end
|
||||
local vehicles, vehiclesById, poolById = ADFieldSilos.collectVehicles(adEnv)
|
||||
local feedReserve, farmStock = nil, nil
|
||||
if cfg.toProduction then
|
||||
local ok, reserve, stock = pcall(ADFieldSilos.collectFeed, stores, byId, farmId(), cfg.reserveDays)
|
||||
if ok then feedReserve, farmStock = reserve, stock
|
||||
else Logging.warning(ADFieldSilos.LOG_PREFIX .. "foderreserve kunne ikke beregnes: %s", tostring(reserve)) end
|
||||
end
|
||||
local plan = ADFieldSiloPlanner.plan({silos = silos, stores = stores, loans = loanList(), vehicles = vehicles,
|
||||
paused = currentPaused(), maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters,
|
||||
feedReserve = feedReserve, farmStock = farmStock})
|
||||
ADFieldSilos.lastFeed = {reserve = feedReserve, stock = farmStock}
|
||||
ADFieldSilos.lastPlan = plan
|
||||
local paused = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused
|
||||
if not cfg.enabled or paused then return plan end
|
||||
local silosById, storesById = {}, {}
|
||||
for _, silo in ipairs(silos) do silosById[silo.id] = silo end
|
||||
for _, store in ipairs(stores) do storesById[store.id] = store end
|
||||
for _, order in ipairs(plan.dispatch) do
|
||||
local vehicle = vehiclesById[order.vehicleId]
|
||||
if vehicle ~= nil and not hasRoutes(adEnv, vehicle, order) then
|
||||
vehicle = nil
|
||||
end
|
||||
local pair = poolById ~= nil and poolById[order.vehicleId] or nil
|
||||
if vehicle ~= nil and pair ~= nil then
|
||||
local ok, err = pcall(dispatchPool, adEnv, order, pair, silosById, storesById)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s' efter puljevogn: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
elseif vehicle ~= nil then
|
||||
local ok, err = pcall(dispatchOne, adEnv, order, vehicle, silosById, storesById)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s': %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
return plan
|
||||
end
|
||||
|
||||
function ADFieldSilos.tick(adEnv, dt)
|
||||
if adEnv == nil then return end
|
||||
ADFieldSilos.adEnv = adEnv
|
||||
nowMs = nowMs + (dt or 0)
|
||||
checkTimerMs = checkTimerMs + (dt or 0)
|
||||
if checkTimerMs >= ADFieldSilos.CHECK_INTERVAL_MS then
|
||||
checkTimerMs = 0
|
||||
checkLoans(adEnv)
|
||||
end
|
||||
planTimerMs = planTimerMs + (dt or 0)
|
||||
if planTimerMs >= ADFieldSilos.PLAN_INTERVAL_MS then
|
||||
planTimerMs = 0
|
||||
ADFieldSilos.planNow(adEnv)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- gem / indlæs (samme sti-opslag som runs) ----
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADFieldSilos.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADFieldSilos.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupFieldSilos", path, ADFieldSilos.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local record = ADFieldSiloConfig.toRecord(ADFieldSilos.config)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#enabled", record.enabled)
|
||||
xmlFile:setString(ADFieldSilos.ROOT .. "#stores", record.stores)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#maxVehicles", record.maxVehicles)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#minLiters", record.minLiters)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#toProduction", record.toProduction)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#reserveDays", record.reserveDays)
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldSilos.load()
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
return
|
||||
end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupFieldSilos", path)
|
||||
if xmlFile == nil then
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
return
|
||||
end
|
||||
ADFieldSilos.config = ADFieldSiloConfig.fromRecord({
|
||||
enabled = xmlFile:getInt(ADFieldSilos.ROOT .. "#enabled"),
|
||||
stores = xmlFile:getString(ADFieldSilos.ROOT .. "#stores"),
|
||||
maxVehicles = xmlFile:getInt(ADFieldSilos.ROOT .. "#maxVehicles"),
|
||||
minLiters = xmlFile:getInt(ADFieldSilos.ROOT .. "#minLiters"),
|
||||
toProduction = xmlFile:getInt(ADFieldSilos.ROOT .. "#toProduction"),
|
||||
reserveDays = xmlFile:getInt(ADFieldSilos.ROOT .. "#reserveDays"),
|
||||
})
|
||||
xmlFile:delete()
|
||||
end
|
||||
@@ -11,8 +11,14 @@ ADFieldStorage.FIELD_TYPES = {
|
||||
id = "Int", stage = "String", mode = "String", product = "String", rake = "Int", store = "String",
|
||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||
kind = "String", lastFruit = "String", care = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", windrowOffset = "String",
|
||||
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
|
||||
dedicated = "String",
|
||||
}
|
||||
|
||||
-- Kursus-registret (ADCourseCache): hvilken mark/bredde/form/indstillinger hver maskines CP-kursus er lavet til.
|
||||
ADFieldStorage.COURSE_TYPES = {
|
||||
vehicle = "String", field = "Int", width = "Float", sig = "String", settings = "String", waypoints = "Int",
|
||||
}
|
||||
|
||||
function ADFieldStorage.getPath()
|
||||
@@ -36,6 +42,21 @@ function ADFieldStorage.readRecord(xmlFile, key)
|
||||
return record
|
||||
end
|
||||
|
||||
function ADFieldStorage.writeCourseRecord(xmlFile, key, record)
|
||||
for name, kind in pairs(ADFieldStorage.COURSE_TYPES) do
|
||||
local value = record[name]
|
||||
if value ~= nil then xmlFile["set" .. kind](xmlFile, key .. "#" .. name, value) end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldStorage.readCourseRecord(xmlFile, key)
|
||||
local record = {}
|
||||
for name, kind in pairs(ADFieldStorage.COURSE_TYPES) do
|
||||
record[name] = xmlFile["get" .. kind](xmlFile, key .. "#" .. name)
|
||||
end
|
||||
return record
|
||||
end
|
||||
|
||||
function ADFieldStorage.save(state)
|
||||
local path = ADFieldStorage.getPath()
|
||||
if path == nil then return false end
|
||||
@@ -46,22 +67,36 @@ function ADFieldStorage.save(state)
|
||||
ADFieldStorage.writeRecord(xmlFile, string.format("%s.field(%d)", ADFieldStorage.ROOT, index), ADFieldModel.toRecord(field))
|
||||
index = index + 1
|
||||
end
|
||||
xmlFile:setString(ADFieldStorage.ROOT .. "#fleetMode", state.fleetMode or "all")
|
||||
xmlFile:setInt(ADFieldStorage.ROOT .. "#paused", state.paused == true and 1 or 0)
|
||||
local vehicleIndex = 0
|
||||
for vehicleId in pairs(state.fleet) do
|
||||
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
|
||||
vehicleIndex = vehicleIndex + 1
|
||||
end
|
||||
local manualIndex = 0
|
||||
for vehicleId in pairs(state.manual or {}) do
|
||||
xmlFile:setString(string.format("%s.manual.vehicle(%d)#id", ADFieldStorage.ROOT, manualIndex), vehicleId)
|
||||
manualIndex = manualIndex + 1
|
||||
end
|
||||
local courseIndex = 0
|
||||
for _, record in pairs(state.courseRecords or {}) do
|
||||
ADFieldStorage.writeCourseRecord(xmlFile, string.format("%s.courses.course(%d)", ADFieldStorage.ROOT, courseIndex), record)
|
||||
courseIndex = courseIndex + 1
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldStorage.load()
|
||||
local result = {fields = {}, fleet = {}}
|
||||
local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}, manual = {}, paused = false}
|
||||
local path = ADFieldStorage.getPath()
|
||||
if path == nil or not fileExists(path) then return result end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
||||
if xmlFile == nil then return result end
|
||||
result.fleetMode = xmlFile:getString(ADFieldStorage.ROOT .. "#fleetMode") == "ticked" and "ticked" or "all"
|
||||
result.paused = xmlFile:getInt(ADFieldStorage.ROOT .. "#paused") == 1
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".field", function(_, key)
|
||||
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
||||
if field.id ~= nil then result.fields[field.id] = field end
|
||||
@@ -70,6 +105,14 @@ function ADFieldStorage.load()
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".manual.vehicle", function(_, key)
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil and id ~= "" then result.manual[id] = true end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".courses.course", function(_, key)
|
||||
local record = ADCourseCache ~= nil and ADCourseCache.newRecord(ADFieldStorage.readCourseRecord(xmlFile, key)) or nil
|
||||
if record ~= nil then result.courseRecords[record.vehicle] = record end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
return result
|
||||
end
|
||||
|
||||
@@ -37,3 +37,75 @@ function ADFieldView.soilParts(info)
|
||||
if (info.weed or 0) >= ADFieldModel.WEED_MIN then table.insert(parts, "weed") end
|
||||
return parts
|
||||
end
|
||||
|
||||
-- Afgrødekolonnen: høstet mark = "stub (Hvede)", bar mark = "bar jord", ellers afgrøden.
|
||||
function ADFieldView.cropText(info, getText)
|
||||
if info.kind == "bare" and info.stubbleOf ~= nil then
|
||||
return string.format(getText("spu_field_stubble"), tostring(info.stubbleOf))
|
||||
end
|
||||
if info.kind == "bare" then return getText("spu_field_bare") end
|
||||
return tostring(info.cropTitle or "-")
|
||||
end
|
||||
|
||||
-- Lagervalget for en mark: marksilo først, så gæringssiloer, så efter afstand. Valgte, marksiloer og
|
||||
-- gæringssiloer vises altid; af resten kun de `limit` nærmeste (34 ens "Farma 500"-rækker var ulæselige).
|
||||
-- entries = {{id, distance, fieldSilo, fermenter, chosen}}.
|
||||
function ADFieldView.storeRows(entries, limit)
|
||||
local sorted = {}
|
||||
for _, entry in ipairs(entries or {}) do table.insert(sorted, entry) end
|
||||
local function rank(e) return e.fieldSilo and 0 or (e.fermenter and 1 or 2) end
|
||||
table.sort(sorted, function(a, b)
|
||||
if rank(a) ~= rank(b) then return rank(a) < rank(b) end
|
||||
if (a.distance or math.huge) ~= (b.distance or math.huge) then return (a.distance or math.huge) < (b.distance or math.huge) end
|
||||
return tostring(a.id) < tostring(b.id)
|
||||
end)
|
||||
local rows, others, hidden = {}, 0, 0
|
||||
for _, entry in ipairs(sorted) do
|
||||
if entry.chosen or entry.fieldSilo or entry.fermenter then
|
||||
table.insert(rows, entry)
|
||||
elseif others < limit then
|
||||
others = others + 1
|
||||
table.insert(rows, entry)
|
||||
else
|
||||
hidden = hidden + 1
|
||||
end
|
||||
end
|
||||
return rows, hidden
|
||||
end
|
||||
|
||||
-- "90 m" / "1,4 km"
|
||||
function ADFieldView.distanceText(meters)
|
||||
if meters == nil then return "?" end
|
||||
if meters < 1000 then return string.format("%d m", math.floor(meters + 0.5)) end
|
||||
return (string.gsub(string.format("%.1f km", meters / 1000), "%.", ","))
|
||||
end
|
||||
|
||||
-- ---- status pr. mark (Fields-listen) ----
|
||||
|
||||
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
|
||||
ADFieldView.ERROR_REASONS = {
|
||||
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
|
||||
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
|
||||
}
|
||||
|
||||
-- Arbejder marken reelt? Under global pause (Stop alt) kun hvis spilleren har genoptaget netop den.
|
||||
-- s = {working, paused, resumed}
|
||||
function ADFieldView.isActiveWork(s)
|
||||
return s.working == true and (not s.paused or s.resumed == true)
|
||||
end
|
||||
|
||||
-- s = {hasMarker, working, paused, resumed, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
|
||||
function ADFieldView.fieldStatus(s)
|
||||
if not s.hasMarker then return "error" end
|
||||
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
|
||||
if s.paused and not s.resumed then return "stopped" end
|
||||
if s.awaiting or s.startRequested then return "waiting" end
|
||||
if ADFieldView.isActiveWork(s) then return s.reasonKey ~= nil and "waiting" or "running" end
|
||||
if s.reasonKey ~= nil then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
|
||||
-- Må [Start]/[Stop] trykkes? Stop altid når marken arbejder; Start kræver en markør.
|
||||
function ADFieldView.canStart(s)
|
||||
return s.working == true or s.hasMarker == true
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+420
-125
@@ -11,6 +11,11 @@ ADFields.cache = {}
|
||||
ADFields.order = {}
|
||||
ADFields.scanIndex = 0
|
||||
ADFields.mowable = nil
|
||||
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
|
||||
ADFields.OWNED_REFRESH_MS = 30000
|
||||
ADFields.MARKER_REFRESH_MS = 30000
|
||||
ADFields.ownedAt = nil
|
||||
ADFields.now = 0
|
||||
|
||||
function ADFields.parseMarkerName(name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
@@ -45,6 +50,7 @@ end
|
||||
function ADFields.growthPhase(state, fruit)
|
||||
if state == nil or fruit == nil then return nil end
|
||||
if state == 0 then return "none" end
|
||||
if ADFields.isPreparable(state, fruit) then return "ready" end
|
||||
if fruit.cutState ~= nil and fruit.cutState ~= 0 and state == fruit.cutState then return "cut" end
|
||||
if fruit.witheredState ~= nil and state == fruit.witheredState then return "withered" end
|
||||
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
||||
@@ -54,6 +60,33 @@ function ADFields.growthPhase(state, fruit)
|
||||
return "cut"
|
||||
end
|
||||
|
||||
-- Fuldt udvokset stadie for en slåafgrøde: græs er "høstbart" allerede i mellemhøjde (halv ydelse,
|
||||
-- Dakota-græs: greenMiddle 3 < harvestReady 4 < harvestReady2 5), så slåning venter til stadiet efter
|
||||
-- det første høstbare, når afgrøden har flere. nil = ukendte grænser (ingen ekstra krav).
|
||||
function ADFields.primeState(fruit)
|
||||
if fruit == nil then return nil end
|
||||
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
||||
if minReady == nil then return nil end
|
||||
local maxReady = fruit.maxHarvestingGrowthState or fruit.maxHarvestGrowthState or minReady
|
||||
if maxReady > minReady then return minReady + 1 end
|
||||
return minReady
|
||||
end
|
||||
|
||||
-- Kan stadiet "forberedes" (toppes)? Dakota-kartofler: "harvestReady" er kun forberedelig — først det
|
||||
-- toppede stadie er høstklart, men skærebord med topper (Ropa rrXL9) tager begge i ét drag.
|
||||
function ADFields.isPreparable(state, fruit)
|
||||
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
|
||||
local minPrep, maxPrep = fruit.minPreparingGrowthState, fruit.maxPreparingGrowthState
|
||||
if type(minPrep) ~= "number" or minPrep <= 0 then return false end
|
||||
return state >= minPrep and state <= (type(maxPrep) == "number" and maxPrep >= minPrep and maxPrep or minPrep)
|
||||
end
|
||||
|
||||
-- Tæller punktets klar-pixels? Kun når stadiet (4. værdi fra getFruitArea) er fuldt udvokset.
|
||||
function ADFields.isPrime(growthState, prime)
|
||||
if prime == nil or type(growthState) ~= "number" or growthState <= 0 then return true end
|
||||
return growthState >= prime
|
||||
end
|
||||
|
||||
-- Vokser marken? Fasen vinder; ukendt fase -> der er voksende afgrøde (getFruitArea) og den er ikke klar.
|
||||
function ADFields.isGrowing(phase, anyPixels, readyRatio)
|
||||
if phase ~= nil then return phase == "growing" end
|
||||
@@ -69,6 +102,16 @@ function ADFields.soilNeeds(levels, settings)
|
||||
}
|
||||
end
|
||||
|
||||
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen
|
||||
-- (in-game 29/9: F22 hvide druer fik "Silage + så" og en skårlægger). Samme liste som ADCpBridge.VINE_FRUITS.
|
||||
ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
|
||||
|
||||
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
|
||||
function ADFields.isMowableFruit(fruitName, mowableEntry)
|
||||
if mowableEntry == nil then return false end
|
||||
return not ADFields.VINE_FRUITS[string.upper(tostring(fruitName or ""))]
|
||||
end
|
||||
|
||||
function ADFields.getMowable()
|
||||
if ADFields.mowable ~= nil then return ADFields.mowable end
|
||||
local ok, result = pcall(function()
|
||||
@@ -122,15 +165,6 @@ local function readWeed(x, z)
|
||||
return value
|
||||
end
|
||||
|
||||
local function average(points, reader, channel)
|
||||
local sum, count = 0, 0
|
||||
for _, p in ipairs(points) do
|
||||
local value = reader(channel, p.x, p.z)
|
||||
if value ~= nil then sum, count = sum + value, count + 1 end
|
||||
end
|
||||
return count > 0 and sum / count or nil
|
||||
end
|
||||
|
||||
local function missionSettings()
|
||||
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
||||
local function flag(value) return value == nil or value == true end
|
||||
@@ -141,22 +175,6 @@ local function missionSettings()
|
||||
}
|
||||
end
|
||||
|
||||
-- Jord og pleje: gødning (og max), kalk, pløjning, ukrudt — gennemsnit over prøvepunkterne.
|
||||
local function readSoil(info)
|
||||
if type(FieldDensityMap) ~= "table" or #info.points == 0 then return end
|
||||
local settings = missionSettings()
|
||||
info.fert = average(info.points, readDensity, FieldDensityMap.SPRAY_LEVEL)
|
||||
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
|
||||
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
|
||||
info.lime = average(info.points, readDensity, FieldDensityMap.LIME_LEVEL)
|
||||
info.plowLevel = average(info.points, readDensity, FieldDensityMap.PLOW_LEVEL)
|
||||
if settings.weedsEnabled then
|
||||
info.weed = average(info.points, function(_, x, z) return readWeed(x, z) end, nil)
|
||||
end
|
||||
info.needs = ADFields.soilNeeds(info, settings)
|
||||
if not settings.limeRequired then info.lime = nil end
|
||||
end
|
||||
|
||||
local function fieldOwner(field)
|
||||
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
||||
if farmland == nil then return nil end
|
||||
@@ -171,82 +189,141 @@ local function isOnField(x, z)
|
||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
||||
end
|
||||
|
||||
-- Centroid + halvvejs mod op til 8 hjørner; kun punkter der stadig er mark (bygninger fjerner areal).
|
||||
local function samplePoints(field)
|
||||
ADFields.SAMPLE_DIRECTIONS = 16
|
||||
|
||||
-- field.fieldState (FS25, bekræftet i AI-Bonde via aifProbe): {fruitIndex|nil, growthState} eller nil
|
||||
-- når spillet ikke har tilstanden (så bruges punkt-afstemningen).
|
||||
function ADFields.readFieldState(field)
|
||||
local state = field ~= nil and field.fieldState or nil
|
||||
if type(state) ~= "table" or type(state.fruitTypeIndex) ~= "number" then return nil end
|
||||
local fruitIndex = state.fruitTypeIndex > 0 and state.fruitTypeIndex or nil
|
||||
local groundNone = type(FieldGroundType) == "table" and FieldGroundType.NONE or 0
|
||||
return {fruitIndex = fruitIndex, growthState = fruitIndex ~= nil and state.growthState or nil,
|
||||
noFieldGround = type(state.groundType) == "number" and state.groundType == groundNone}
|
||||
end
|
||||
|
||||
-- Jordtyper der tæller som klargjort til såning (stubbearbejdet, kultiveret, såbed, pløjet).
|
||||
ADFields.PREPARED_GROUNDS = {"STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}
|
||||
|
||||
-- value = jordtypens tæthedsværdi i et punkt; values = {[typenavn] = værdi}. -> klargjort, pløjet (nil: ukendt)
|
||||
function ADFields.classifyGround(value, values)
|
||||
if value == nil or values == nil then return nil, nil end
|
||||
-- ingen markjord (bygget over / uden for marken): tæller ikke med i andelen
|
||||
if values.NONE ~= nil and values.NONE == value then return nil, nil end
|
||||
local prepared = false
|
||||
for _, name in ipairs(ADFields.PREPARED_GROUNDS) do
|
||||
if values[name] ~= nil and values[name] == value then prepared = true end
|
||||
end
|
||||
return prepared, values.PLOWED ~= nil and values.PLOWED == value
|
||||
end
|
||||
|
||||
-- Spillets tæthedsværdier for jordtyperne (FieldGroundType -> værdi i GROUND_TYPE-kanalen).
|
||||
function ADFields.groundValues()
|
||||
if ADFields.groundValueCache ~= nil then return ADFields.groundValueCache or nil end
|
||||
local system = g_currentMission ~= nil and g_currentMission.fieldGroundSystem or nil
|
||||
if type(FieldGroundType) ~= "table" or system == nil then return nil end
|
||||
local values = {}
|
||||
for _, name in ipairs({"NONE", "STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}) do
|
||||
local groundType = FieldGroundType[name]
|
||||
if groundType ~= nil then
|
||||
local ok, value = pcall(function() return system:getFieldGroundValue(groundType) end)
|
||||
values[name] = ok and type(value) == "number" and value or groundType
|
||||
end
|
||||
end
|
||||
ADFields.groundValueCache = values
|
||||
return values
|
||||
end
|
||||
|
||||
-- Kan marken dyrkes? Nej hvis spillet siger "ingen markjord" (groundType NONE: bygget over — gårdsplads,
|
||||
-- stald, biogasanlæg), eller hvis intet prøvepunkt ligger på markjord. En markensilo i et hjørne ændrer
|
||||
-- intet: resten af marken er stadig markjord.
|
||||
function ADFields.isCultivable(state, onFieldPoints)
|
||||
if state ~= nil and state.noFieldGround then return false end
|
||||
return (onFieldPoints or 0) > 0
|
||||
end
|
||||
|
||||
-- Markens omrids som {{x, z}, ...}. FS25: field:getDensityMapPolygon():getVerticesList() (flad liste
|
||||
-- x1, z1, x2, z2 ... — som Courseplay); ældre: polygonPoints (noder). Tom liste hvis intet findes.
|
||||
function ADFields.readPolygon(field)
|
||||
local verts = {}
|
||||
local okList, list = pcall(function()
|
||||
return field.getDensityMapPolygon ~= nil and field:getDensityMapPolygon():getVerticesList() or nil
|
||||
end)
|
||||
if okList and type(list) == "table" and #list >= 6 then
|
||||
for index = 1, #list - 1, 2 do
|
||||
table.insert(verts, {x = list[index], z = list[index + 1]})
|
||||
end
|
||||
return verts
|
||||
end
|
||||
for _, node in ipairs(field.polygonPoints or {}) do
|
||||
if type(node) == "number" then
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
table.insert(verts, {x = x, z = z})
|
||||
end
|
||||
end
|
||||
local cx, cz = field.posX, field.posZ
|
||||
if (cx == nil or cz == nil) and #verts > 0 then
|
||||
cx, cz = 0, 0
|
||||
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
|
||||
cx, cz = cx / #verts, cz / #verts
|
||||
end
|
||||
if cx == nil then return {}, verts end
|
||||
local candidates = {{x = cx, z = cz}}
|
||||
local step = math.max(1, math.floor(#verts / 8))
|
||||
for index = 1, #verts, step do
|
||||
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
|
||||
end
|
||||
local points = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local ok, onField = pcall(isOnField, p.x, p.z)
|
||||
if ok and onField then table.insert(points, p) end
|
||||
end
|
||||
return points, verts
|
||||
return verts
|
||||
end
|
||||
|
||||
local function fruitAt(points)
|
||||
local votes, best, bestVotes = {}, nil, 0
|
||||
for _, p in ipairs(points) do
|
||||
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, p.x, p.z)
|
||||
if ok and type(index) == "number" and index > 0 then
|
||||
votes[index] = (votes[index] or 0) + 1
|
||||
if votes[index] > bestVotes then best, bestVotes = index, votes[index] end
|
||||
end
|
||||
-- Markens form-fingeraftryk: antal hjørner, areal (shoelace, nærmeste 10 m²) og tyngdepunkt (nærmeste meter).
|
||||
-- Samme mark giver samme streng trods små float-forskelle; en ændret mark (hjørne/kant) giver en ny.
|
||||
-- Bruges til at genbruge Courseplay-kurser (ADCourseCache). nil uden en flade (under 3 hjørner).
|
||||
ADFields.SHAPE_AREA_STEP = 10
|
||||
local function roundTo(value, step)
|
||||
return math.floor(value / step + 0.5) * step
|
||||
end
|
||||
function ADFields.shapeSignature(verts)
|
||||
if type(verts) ~= "table" or #verts < 3 then return nil end
|
||||
local n = #verts
|
||||
local twiceArea, sx, sz = 0, 0, 0
|
||||
for index = 1, n do
|
||||
local a, b = verts[index], verts[index % n + 1]
|
||||
local cross = a.x * b.z - b.x * a.z
|
||||
twiceArea = twiceArea + cross
|
||||
sx, sz = sx + (a.x + b.x) * cross, sz + (a.z + b.z) * cross
|
||||
end
|
||||
local cx, cz
|
||||
if math.abs(twiceArea) > 1e-6 then
|
||||
cx, cz = sx / (3 * twiceArea), sz / (3 * twiceArea)
|
||||
else
|
||||
cx, cz = ADFields.polygonCenter(verts)
|
||||
end
|
||||
return string.format("n=%d;a=%d;c=%d,%d", n, roundTo(math.abs(twiceArea) / 2, ADFields.SHAPE_AREA_STEP),
|
||||
roundTo(cx, 1), roundTo(cz, 1))
|
||||
end
|
||||
|
||||
-- Hjørnernes gennemsnit (nil uden hjørner).
|
||||
function ADFields.polygonCenter(verts)
|
||||
if verts == nil or #verts == 0 then return nil end
|
||||
local cx, cz = 0, 0
|
||||
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
|
||||
return cx / #verts, cz / #verts
|
||||
end
|
||||
|
||||
-- Afstand fra (x, z) til omridsets nærmeste kant; nil uden omrids.
|
||||
function ADFields.distanceToPolygon(verts, x, z)
|
||||
if verts == nil or #verts == 0 then return nil end
|
||||
local best = math.huge
|
||||
for index = 1, #verts do
|
||||
local a, b = verts[index], verts[index % #verts + 1]
|
||||
local dx, dz = b.x - a.x, b.z - a.z
|
||||
local len2 = dx * dx + dz * dz
|
||||
local t = len2 > 0 and math.max(0, math.min(1, ((x - a.x) * dx + (z - a.z) * dz) / len2)) or 0
|
||||
local d = math.sqrt((a.x + dx * t - x) ^ 2 + (a.z + dz * t - z) ^ 2)
|
||||
if d < best then best = d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Andel af prøvefladen i høstvinduet (samme kald som Arbejdsseddel: any = true,true; klar = false,false).
|
||||
local function readyRatio(fruitIndex, points)
|
||||
local anySum, readySum = 0, 0
|
||||
for _, p in ipairs(points) do
|
||||
local okA, _, anyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, true, true)
|
||||
if okA and anyPixels then anySum = anySum + anyPixels end
|
||||
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, false, false)
|
||||
if okR and readyPixels then readySum = readySum + readyPixels end
|
||||
end
|
||||
return anySum > 0 and readySum / anySum or 0, anySum
|
||||
end
|
||||
|
||||
-- Vækststadiet på prøvepunkterne (flertal): getFieldDataAtWorldPosition's anden returværdi.
|
||||
local function growthStateAt(points)
|
||||
local votes, best, bestVotes = {}, nil, 0
|
||||
for _, p in ipairs(points) do
|
||||
local ok, _, state = pcall(function()
|
||||
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, p.x, 1, p.z)
|
||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(p.x, y, p.z)
|
||||
end)
|
||||
if ok and type(state) == "number" then
|
||||
votes[state] = (votes[state] or 0) + 1
|
||||
if votes[state] > bestVotes then best, bestVotes = state, votes[state] end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function findMarker(adEnv, fieldId, x, z)
|
||||
-- Markøren "F<nr>" nærmest marken: afstand til omridset (markører står ved kanten; midten kan
|
||||
-- ligge 900 m væk på store kort), ellers til midten.
|
||||
local function findMarker(adEnv, fieldId, x, z, verts)
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if ADFields.parseMarkerName(marker.name) == fieldId then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
if wayPoint ~= nil then
|
||||
local distance = math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2)
|
||||
local distance = ADFields.distanceToPolygon(verts, wayPoint.x, wayPoint.z)
|
||||
or math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2)
|
||||
if distance < bestDistance then best, bestDistance = marker, distance end
|
||||
end
|
||||
end
|
||||
@@ -257,84 +334,302 @@ local function findMarker(adEnv, fieldId, x, z)
|
||||
return nil, best ~= nil and bestDistance or nil
|
||||
end
|
||||
|
||||
local function scanField(adEnv, field, fieldId)
|
||||
local points, verts = samplePoints(field)
|
||||
local info = {id = fieldId, field = field, points = points, verts = verts, mowable = false, readyRatio = 0}
|
||||
info.x, info.z = field.posX, field.posZ
|
||||
if (info.x == nil or info.z == nil) and points[1] ~= nil then info.x, info.z = points[1].x, points[1].z end
|
||||
info.fruitIndex = #points > 0 and fruitAt(points) or nil
|
||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
||||
if info.fruitIndex ~= nil then
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
||||
-- ---- scanning som arbejdskø ----
|
||||
-- En marks scanning er en kø af små opgaver (ét prøvepunkt pr. opgave), og scanStep kører én opgave pr.
|
||||
-- frame. In-game 26/9 gav en hel mark i ét frame et hak (flere opslag gennemgår alle kortets ~40
|
||||
-- afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem), derfor en kø.
|
||||
|
||||
-- (eksplicit hale: # er udefineret på en tabel med huller, og udførte opgaver nulstilles)
|
||||
local function push(scan, task)
|
||||
scan.tail = (scan.tail or 0) + 1
|
||||
scan.queue[scan.tail] = task
|
||||
end
|
||||
|
||||
local function vote(votes, key)
|
||||
votes[key] = (votes[key] or 0) + 1
|
||||
end
|
||||
|
||||
local function winner(votes)
|
||||
local best, bestVotes = nil, 0
|
||||
for key, count in pairs(votes) do
|
||||
if count > bestVotes then best, bestVotes = key, count end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function newScan(adEnv, entry)
|
||||
local field, fieldId = entry.field, entry.id
|
||||
local scan = {id = fieldId, queue = {}, head = 1, tail = 0, points = {}, fruitVotes = {},
|
||||
anySum = 0, readySum = 0, immatureSum = 0, totalSum = 0, soil = {fert = {0, 0}, lime = {0, 0}, plowLevel = {0, 0}, weed = {0, 0}, prepared = {0, 0}, plowed = {0, 0}}}
|
||||
local info = {id = fieldId, field = field, points = scan.points, verts = {}, mowable = false, readyRatio = 0}
|
||||
scan.info = info
|
||||
-- 1) polygon og kandidatpunkter (centroid + halvvejs mod op til 8 hjørner)
|
||||
push(scan, function()
|
||||
local verts = ADFields.readPolygon(field)
|
||||
info.verts = verts
|
||||
local cx, cz = ADFields.polygonCenter(verts)
|
||||
if cx == nil then cx, cz = field.posX, field.posZ end
|
||||
info.x, info.z = cx, cz
|
||||
if cx == nil then return end
|
||||
-- midten, markskiltet og halvvejs mod op til SAMPLE_DIRECTIONS hjørner: store marker (Dakota
|
||||
-- 16x: 1,7 km) har striber uden afgrøde, så ét punkt kan ramme forkert
|
||||
local candidates = {{x = cx, z = cz}}
|
||||
if field.posX ~= nil and field.posZ ~= nil and (field.posX - cx) ^ 2 + (field.posZ - cz) ^ 2 > 1 then
|
||||
table.insert(candidates, {x = field.posX, z = field.posZ})
|
||||
end
|
||||
local step = math.max(1, math.floor(#verts / ADFields.SAMPLE_DIRECTIONS))
|
||||
for index = 1, #verts, step do
|
||||
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
|
||||
end
|
||||
-- 2) én opgave pr. kandidat: er punktet stadig mark? (bygninger fjerner areal)
|
||||
for _, candidate in ipairs(candidates) do
|
||||
push(scan, function()
|
||||
local ok, onField = pcall(isOnField, candidate.x, candidate.z)
|
||||
if ok and onField then table.insert(scan.points, candidate) end
|
||||
end)
|
||||
end
|
||||
-- 3) afgrøde pr. punkt (efter alle kandidater)
|
||||
push(scan, function()
|
||||
for _, point in ipairs(scan.points) do
|
||||
push(scan, function()
|
||||
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, point.x, point.z)
|
||||
if ok and type(index) == "number" and index > 0 then vote(scan.fruitVotes, index) end
|
||||
end)
|
||||
end
|
||||
push(scan, function() ADFields.scanPerFruit(scan) end)
|
||||
end)
|
||||
end)
|
||||
return scan
|
||||
end
|
||||
|
||||
-- Når afgrøden kendes: klar-andel, vækststadie og jord pr. punkt (én opgave pr. punkt).
|
||||
function ADFields.scanPerFruit(scan)
|
||||
local info = scan.info
|
||||
-- spillets egen marktilstand (som fields.xml) er sandheden; punkt-afstemningen er reserve.
|
||||
-- (getFieldDataAtWorldPosition giver isOnField, densityBits, groundType — IKKE vækststadie)
|
||||
local state = ADFields.readFieldState(info.field)
|
||||
info.growthState = state ~= nil and state.growthState or nil
|
||||
info.cultivable = ADFields.isCultivable(state, #scan.points)
|
||||
if state ~= nil then
|
||||
info.fruitIndex = state.fruitIndex
|
||||
else
|
||||
info.fruitIndex = winner(scan.fruitVotes)
|
||||
end
|
||||
local fruitIndex = info.fruitIndex
|
||||
local settings = missionSettings()
|
||||
scan.settings = settings
|
||||
-- slåafgrøder: genvækst i mellemhøjde tæller ikke som klar (in-game 27/9: halvt slået F90 blev
|
||||
-- klippet igen i mellemhøjde)
|
||||
local prime = nil
|
||||
local fruitDesc = fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
|
||||
if fruitIndex ~= nil and ADFields.getMowable()[fruitIndex] ~= nil then
|
||||
prime = ADFields.primeState(fruitDesc)
|
||||
end
|
||||
for _, point in ipairs(scan.points) do
|
||||
push(scan, function()
|
||||
local x, z = point.x, point.z
|
||||
if fruitIndex ~= nil then
|
||||
-- returnerer fruitValue, numPixels (afgrøde i høsthøjde), totalNumPixels (hele arealet), growthState
|
||||
local okA, _, anyPixels, totalPixels, pointState = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, true, true)
|
||||
if okA and type(totalPixels) == "number" then scan.totalSum = scan.totalSum + totalPixels end
|
||||
if okA and anyPixels then
|
||||
scan.anySum = scan.anySum + anyPixels
|
||||
if not ADFields.isPrime(pointState, prime) then scan.immatureSum = scan.immatureSum + anyPixels end
|
||||
end
|
||||
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, false, false)
|
||||
-- utoppede kartofler tæller som klar (in-game 27/9 F16: klar-andel 0 -> sprang høsten over)
|
||||
if okA and anyPixels and ADFields.isPreparable(pointState, fruitDesc) then
|
||||
okR, readyPixels = true, math.max(readyPixels or 0, anyPixels)
|
||||
end
|
||||
if okR and readyPixels and ADFields.isPrime(okA and pointState or nil, prime) then
|
||||
scan.readySum = scan.readySum + readyPixels
|
||||
end
|
||||
end
|
||||
end)
|
||||
push(scan, function()
|
||||
if type(FieldDensityMap) ~= "table" then return end
|
||||
local x, z = point.x, point.z
|
||||
local readings = {
|
||||
fert = readDensity(FieldDensityMap.SPRAY_LEVEL, x, z),
|
||||
lime = readDensity(FieldDensityMap.LIME_LEVEL, x, z),
|
||||
plowLevel = readDensity(FieldDensityMap.PLOW_LEVEL, x, z),
|
||||
weed = settings.weedsEnabled and readWeed(x, z) or nil,
|
||||
}
|
||||
-- klargjort/pløjet jord i punktet (kun på markjord): klargøringstrin der allerede er gjort går videre
|
||||
local prepared, plowed = ADFields.classifyGround(readDensity(FieldDensityMap.GROUND_TYPE, x, z), ADFields.groundValues())
|
||||
if prepared ~= nil then
|
||||
readings.prepared = prepared and 1 or 0
|
||||
readings.plowed = plowed and 1 or 0
|
||||
end
|
||||
for key, value in pairs(readings) do
|
||||
if value ~= nil then
|
||||
scan.soil[key][1] = scan.soil[key][1] + value
|
||||
scan.soil[key][2] = scan.soil[key][2] + 1
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Samler resultatet når køen er tom.
|
||||
local function finishScan(adEnv, scan)
|
||||
local info = scan.info
|
||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
||||
local fruitDesc = info.fruitIndex ~= nil and g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
|
||||
if mowable ~= nil and not ADFields.isMowableFruit(fruitDesc ~= nil and fruitDesc.name or nil, mowable) then mowable = nil end
|
||||
if mowable ~= nil then
|
||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||
end
|
||||
local fruit = nil
|
||||
if info.fruitIndex ~= nil then
|
||||
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
|
||||
info.readyRatio = scan.anySum > 0 and scan.readySum / scan.anySum or 0
|
||||
info.immatureRatio = scan.anySum > 0 and scan.immatureSum / scan.anySum or 0
|
||||
info.anyPixels = scan.anySum
|
||||
-- andel af hele arealet der stadig står i høsthøjde (fremdrift på fanen Måned)
|
||||
info.standingRatio = scan.totalSum > 0 and scan.readySum / scan.totalSum or nil
|
||||
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||
info.fruitName = fruit ~= nil and fruit.name or nil
|
||||
info.harvestFillType = fruit ~= nil and fruit.fillType ~= nil and fruit.fillType.index or nil
|
||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
||||
end
|
||||
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
||||
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), fruit) or nil
|
||||
info.phase = fruit ~= nil and ADFields.growthPhase(info.growthState, fruit) or nil
|
||||
info.growing = info.fruitIndex ~= nil and ADFields.isGrowing(info.phase, info.anyPixels, info.readyRatio)
|
||||
-- høstet stub eller visnet afgrøde er i praksis en bar mark (klargøring/såning)
|
||||
if info.kind == "harvest" and (info.phase == "cut" or info.phase == "withered" or info.phase == "none") then
|
||||
info.kind = "bare"
|
||||
info.stubbleOf = info.phase == "cut" and info.cropTitle or nil
|
||||
end
|
||||
readSoil(info)
|
||||
if info.x ~= nil then
|
||||
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
|
||||
local settings = scan.settings or missionSettings()
|
||||
for key, sum in pairs(scan.soil) do
|
||||
info[key] = sum[2] > 0 and sum[1] / sum[2] or nil
|
||||
end
|
||||
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
|
||||
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
|
||||
info.needs = ADFields.soilNeeds(info, settings)
|
||||
if not settings.limeRequired then info.lime = nil end
|
||||
local previous = ADFields.cache[info.id]
|
||||
if previous ~= nil and previous.markerCheckedAt ~= nil and ADFields.now - previous.markerCheckedAt < ADFields.MARKER_REFRESH_MS then
|
||||
info.markerId, info.markerName, info.markerDistance, info.markerCheckedAt = previous.markerId, previous.markerName, previous.markerDistance, previous.markerCheckedAt
|
||||
elseif info.x ~= nil then
|
||||
local marker, distance = findMarker(adEnv, info.id, info.x, info.z, info.verts)
|
||||
info.markerId = marker ~= nil and marker.id or nil
|
||||
info.markerName = marker ~= nil and marker.name or nil
|
||||
info.markerDistance = distance
|
||||
info.markerCheckedAt = ADFields.now
|
||||
end
|
||||
return info
|
||||
-- markens form: ændret mark (anden signatur end kursus-registret) -> gemte kurser til marken glemmes
|
||||
info.shapeSig = ADFields.shapeSignature(info.verts)
|
||||
if ADCourseCache ~= nil and ADCourseCache.checkField ~= nil then pcall(ADCourseCache.checkField, info.id, info.shapeSig) end
|
||||
ADFields.cache[info.id] = info
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
local function ownedFields(farmId)
|
||||
local result = {}
|
||||
local all = g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}
|
||||
for _, field in pairs(all) do
|
||||
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||
if id ~= nil and fieldOwner(field) == farmId then
|
||||
table.insert(result, {id = id, field = field})
|
||||
end
|
||||
-- Marklisten (ejede marker) i portioner af 25 marker pr. frame.
|
||||
local function newOwnedScan(farmId)
|
||||
local all = {}
|
||||
for _, field in pairs(g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}) do
|
||||
table.insert(all, field)
|
||||
end
|
||||
table.sort(result, function(a, b) return a.id < b.id end)
|
||||
return result
|
||||
local scan = {owned = true, queue = {}, head = 1, tail = 0, result = {}}
|
||||
for start = 1, #all, 25 do
|
||||
push(scan, function()
|
||||
for index = start, math.min(start + 24, #all) do
|
||||
local field = all[index]
|
||||
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||
if id ~= nil and fieldOwner(field) == farmId then
|
||||
table.insert(scan.result, {id = id, field = field})
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
return scan
|
||||
end
|
||||
|
||||
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
|
||||
function ADFields.scanNext(adEnv, farmId)
|
||||
local owned = ownedFields(farmId)
|
||||
local function finishOwnedScan(scan)
|
||||
table.sort(scan.result, function(a, b) return a.id < b.id end)
|
||||
local ids = {}
|
||||
for _, entry in ipairs(owned) do ids[entry.id] = true end
|
||||
for _, entry in ipairs(scan.result) do ids[entry.id] = true end
|
||||
for id in pairs(ADFields.cache) do
|
||||
if not ids[id] then ADFields.cache[id] = nil end
|
||||
end
|
||||
ADFields.order = owned
|
||||
if #owned == 0 then return end
|
||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
||||
local entry = owned[ADFields.scanIndex]
|
||||
local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
|
||||
if ok then
|
||||
ADFields.cache[entry.id] = info
|
||||
else
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
|
||||
end
|
||||
ADFields.order = scan.result
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
|
||||
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
|
||||
ADFields.SCAN_INTERVAL_MS = 2000
|
||||
ADFields.MAX_TASKS_PER_FRAME = 8
|
||||
ADFields.FRAME_BUDGET_MS = 0.5
|
||||
|
||||
function ADFields.clock()
|
||||
if netGetTime == nil then return nil end
|
||||
local ok, ms = pcall(netGetTime)
|
||||
return ok and type(ms) == "number" and ms or nil
|
||||
end
|
||||
ADFields.lastScanStart = nil
|
||||
|
||||
function ADFields.scanStep(adEnv, farmId, now)
|
||||
ADFields.now = now or ADFields.now
|
||||
local scan = ADFields.activeScan
|
||||
if scan == nil then
|
||||
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
|
||||
ADFields.ownedAt = ADFields.now
|
||||
scan = newOwnedScan(farmId)
|
||||
else
|
||||
if ADFields.lastScanStart ~= nil and ADFields.now - ADFields.lastScanStart < ADFields.SCAN_INTERVAL_MS then return end
|
||||
local owned = ADFields.order
|
||||
if #owned == 0 then return end
|
||||
ADFields.lastScanStart = ADFields.now
|
||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
||||
scan = newScan(adEnv, owned[ADFields.scanIndex])
|
||||
end
|
||||
ADFields.activeScan = scan
|
||||
end
|
||||
local task = scan.queue[scan.head]
|
||||
if task ~= nil then
|
||||
-- flere små opgaver pr. frame inden for et tidsbudget (efter genstart skal alle marker scannes;
|
||||
-- én pr. frame gjorde listen fuld først efter flere minutter). Uden ur (tests): én pr. frame.
|
||||
local started = ADFields.clock()
|
||||
for _ = 1, ADFields.MAX_TASKS_PER_FRAME do
|
||||
task = scan.queue[scan.head]
|
||||
if task == nil then return end
|
||||
scan.queue[scan.head] = nil
|
||||
scan.head = scan.head + 1
|
||||
local ok, err = pcall(task)
|
||||
if not ok then
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
|
||||
ADFields.activeScan = nil
|
||||
return
|
||||
end
|
||||
local current = started ~= nil and ADFields.clock() or nil
|
||||
if current == nil or current - started >= ADFields.FRAME_BUDGET_MS then return end
|
||||
end
|
||||
return
|
||||
end
|
||||
-- køen er tom: saml resultatet
|
||||
ADFields.activeScan = nil
|
||||
local ok, err
|
||||
if scan.owned then
|
||||
ok, err = pcall(finishOwnedScan, scan)
|
||||
else
|
||||
ok, err = pcall(finishScan, adEnv, scan)
|
||||
end
|
||||
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
|
||||
end
|
||||
|
||||
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||
function ADFields.list()
|
||||
if ADFields.listCache ~= nil then return ADFields.listCache end
|
||||
local infos = {}
|
||||
for _, entry in ipairs(ADFields.order) do
|
||||
local info = ADFields.cache[entry.id]
|
||||
if info ~= nil then table.insert(infos, info) end
|
||||
-- bygget-over marker (stald, biogas, gårdsplads) kan ikke dyrkes: ikke med i oversigt/arbejde
|
||||
if info ~= nil and info.cultivable ~= false then table.insert(infos, info) end
|
||||
end
|
||||
ADFields.listCache = infos
|
||||
return infos
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
-- AD Fleet Names
|
||||
-- Konsol "spuNameFleet": traktorer/lastbiler uden for flows får spillets butiksnavn (nummereret når der er flere
|
||||
-- af samme model), og deres egen AD-parkering hedder "P <navn>". Flow-traktorer beholder flowets navn (mod'en
|
||||
-- sætter det selv); selvkørende maskiner (mejetærsker, roeoptager …) beholder deres navn.
|
||||
|
||||
ADFleetNames = {}
|
||||
ADFleetNames.LOG_PREFIX = "ADSmartPickup: navne: "
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- vehicles = {{id, model, name, inRun, selfPropelled}} -> {{id, name, parkName, rename}}
|
||||
-- parkUse = {[parkIndex] = antal køretøjer (også redskaber) med den parkering}: delte parkeringer røres ikke.
|
||||
function ADFleetNames.plan(vehicles, parkUse)
|
||||
local byModel = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not vehicle.inRun and not vehicle.selfPropelled then
|
||||
byModel[vehicle.model] = byModel[vehicle.model] or {}
|
||||
table.insert(byModel[vehicle.model], vehicle)
|
||||
end
|
||||
end
|
||||
local newNames = {}
|
||||
for model, list in pairs(byModel) do
|
||||
table.sort(list, function(a, b) return tostring(a.id) < tostring(b.id) end)
|
||||
for index, vehicle in ipairs(list) do
|
||||
newNames[vehicle.id] = #list > 1 and string.format("%s %d", model, index) or model
|
||||
end
|
||||
end
|
||||
local result = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
local name = newNames[vehicle.id] or vehicle.name or vehicle.model
|
||||
local ownPark = vehicle.park == nil or ((parkUse or {})[vehicle.park] or 1) <= 1
|
||||
table.insert(result, {id = vehicle.id, name = name, parkName = "P " .. name,
|
||||
rename = newNames[vehicle.id] ~= nil and name ~= vehicle.name,
|
||||
renamePark = newNames[vehicle.id] ~= nil and ownPark})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
function ADFleetNames.apply(adEnv)
|
||||
local vehicles, byId, parkUse = {}, {}, {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
if type(index) == "number" and index >= 1 and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then
|
||||
parkUse[index] = (parkUse[index] or 0) + 1
|
||||
end
|
||||
end
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if id ~= nil and vehicle.getName ~= nil then
|
||||
local ownIndex = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
table.insert(vehicles, {id = id, model = tostring(vehicle:getName()), name = ADRuns.vehicleLabel(vehicle),
|
||||
park = type(ownIndex) == "number" and ownIndex >= 1 and ownIndex or nil,
|
||||
inRun = ADRunsController.getRunForVehicle(vehicle) ~= nil,
|
||||
selfPropelled = not ADPool.canPull(ADPool.ownRole(vehicle))})
|
||||
byId[id] = vehicle
|
||||
end
|
||||
end
|
||||
local renamed, parks = 0, 0
|
||||
for _, entry in ipairs(ADFleetNames.plan(vehicles, parkUse)) do
|
||||
local vehicle = byId[entry.id]
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if entry.rename and module ~= nil and module.setName ~= nil then
|
||||
module:setName(entry.name)
|
||||
if module.raiseDirtyFlag ~= nil then module:raiseDirtyFlag() end
|
||||
renamed = renamed + 1
|
||||
end
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
if entry.renamePark and marker ~= nil and marker.name ~= entry.parkName then
|
||||
adEnv.ADGraphManager:renameMapMarker(entry.parkName, index)
|
||||
parks = parks + 1
|
||||
end
|
||||
end
|
||||
Logging.info(ADFleetNames.LOG_PREFIX .. "%d maskiner omdøbt, %d parkeringer omdøbt", renamed, parks)
|
||||
return renamed, parks
|
||||
end
|
||||
|
||||
function ADFleetNames:consoleNameFleet()
|
||||
local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil
|
||||
if adEnv == nil then return "spuNameFleet: AutoDrive er ikke klar endnu" end
|
||||
local ok, renamed, parks = pcall(ADFleetNames.apply, adEnv)
|
||||
if not ok then return "spuNameFleet: fejl: " .. tostring(renamed) end
|
||||
return string.format("spuNameFleet: %d maskiner og %d parkeringer omdøbt — gem spillet", renamed, parks)
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuNameFleet", "Giver traktorer uden for flows butiksnavne og deres parkeringer 'P <navn>'", "consoleNameFleet", ADFleetNames)
|
||||
end
|
||||
@@ -0,0 +1,567 @@
|
||||
-- AD Flow Guide
|
||||
-- "Opsæt flow": flowguidens trin oven på ADGuide (ren Lua, ingen spil-API) — samme trin-skærm og knapper
|
||||
-- som markguiden. Kladden er en kopi af flowets felter, så intet ændres før [Gem]/[Start flow]. Det
|
||||
-- spil-afhængige kommer i `context` (GUI'et henter det ud fra kladden, se previewRun):
|
||||
-- {buildings = {{id, name, label, hasMarker, text}} -- kandidater til Fra (udkørsel) / Til (forsyning)
|
||||
-- fillTypes = {{name, title}} -- varer målet tager (forsyning, Vælg selv)
|
||||
-- products = {{name, title}} -- varer kilderne har (udkørsel)
|
||||
-- storeBuildings = {{id, name, text}} -- lagre der kan tage flowets lagervarer
|
||||
-- waitPools = {{group, count}}
|
||||
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked, solo}} -- label = kort navn (resumé)
|
||||
-- wagons = {{id, label}}} -- ledige vogne til solo-traktorer (bedste først)
|
||||
-- Solo-traktor (ingen vogn bagpå) får en ledig vogn fra context.wagons (draft.wagons = {[traktorId] = vognId}),
|
||||
-- som den henter ved "Connect <vognens parkering>" — uden at vognens parkering skal omdøbes.
|
||||
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
|
||||
|
||||
ADFlowGuide = {}
|
||||
|
||||
ADFlowGuide.ORDER = {"name", "direction", "buildings", "goods", "store", "rules", "wait", "tractors", "summary"}
|
||||
ADFlowGuide.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||
ADFlowGuide.MAX_NAMED_TRACTORS = 3
|
||||
ADFlowGuide.MAX_NAMED_GOODS = 3
|
||||
|
||||
local function L(key, args) return {key = key, args = args} end
|
||||
local function T(text) return {text = text} end
|
||||
|
||||
local function isOut(guide) return guide.draft.kind == "outbound" end
|
||||
|
||||
local function hasStorePolicy(draft)
|
||||
for _, product in ipairs(draft.products or {}) do
|
||||
if product.policy == "store" or product.policy == "sellElseStore" then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local VISIBLE = {
|
||||
name = function(guide) return guide.draft.isNew == true end,
|
||||
store = function(guide) return isOut(guide) and hasStorePolicy(guide.draft) end,
|
||||
}
|
||||
|
||||
function ADFlowGuide.isVisible(step, guide)
|
||||
local rule = VISIBLE[step]
|
||||
if rule ~= nil then return rule(guide) == true end
|
||||
for _, id in ipairs(ADFlowGuide.ORDER) do
|
||||
if id == step then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
ADFlowGuide.DEF = {order = ADFlowGuide.ORDER, isVisible = ADFlowGuide.isVisible}
|
||||
|
||||
-- Overskrift pr. trin; "Fra"/"Til" følger retningen.
|
||||
function ADFlowGuide.stepKey(step, guide)
|
||||
if step == "buildings" then
|
||||
return (guide ~= nil and isOut(guide)) and "spu_guide_flow_step_from" or "spu_guide_flow_step_to"
|
||||
end
|
||||
return "spu_guide_flow_step_" .. tostring(step)
|
||||
end
|
||||
|
||||
local KIND_HINTS = {buildings = true, goods = true, rules = true}
|
||||
function ADFlowGuide.hintKey(step, guide)
|
||||
if KIND_HINTS[step] then
|
||||
return "spu_guide_flow_hint_" .. step .. ((guide ~= nil and isOut(guide)) and "_out" or "_in")
|
||||
end
|
||||
return "spu_guide_flow_hint_" .. tostring(step)
|
||||
end
|
||||
|
||||
-- ---- kladde ----
|
||||
|
||||
local RUN_FIELDS = {
|
||||
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
||||
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
||||
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
|
||||
}
|
||||
|
||||
local function draftFrom(run, isNew)
|
||||
local draft = {}
|
||||
for _, key in ipairs(RUN_FIELDS) do draft[key] = ADGuide.deepCopy(run[key]) end
|
||||
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products", "ingredients", "vehicleIds"}) do
|
||||
draft[key] = draft[key] or {}
|
||||
end
|
||||
draft.kind = draft.kind == "outbound" and "outbound" or "supply"
|
||||
draft.ingredientMode = draft.ingredientMode == "manual" and "manual" or "auto"
|
||||
draft.sellAtPercent = draft.sellAtPercent or ADRuns.DEFAULT_SELL_AT_PERCENT
|
||||
draft.storeAbovePercent = draft.storeAbovePercent or ADRuns.DEFAULT_STORE_ABOVE_PERCENT
|
||||
draft.sellAbovePercent = draft.sellAbovePercent or ADRuns.DEFAULT_SELL_ABOVE_PERCENT
|
||||
draft.returnBelowPercent = draft.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT
|
||||
draft.loops = draft.loops or 0
|
||||
draft.waitPoolGroup = draft.waitPoolGroup or ""
|
||||
draft.wagons = draft.wagons or {}
|
||||
draft.collectRound = draft.collectRound == true
|
||||
draft.isNew = isNew == true
|
||||
draft.showAllBuildings = false
|
||||
draft.storeAuto = #draft.storeBuildingIds == 0
|
||||
return draft
|
||||
end
|
||||
|
||||
function ADFlowGuide.open(run, context, options)
|
||||
return ADGuide.new(ADFlowGuide.DEF, draftFrom(run, false), context, options)
|
||||
end
|
||||
|
||||
-- Nyt flow: intet oprettes før [Gem]; kladden har standardværdierne fra ADRuns.createRun.
|
||||
function ADFlowGuide.openNew(name, context)
|
||||
local _, run = ADRuns.createRun(ADRuns.newStore(), name, "supply")
|
||||
local draft = draftFrom(run, true)
|
||||
draft.id = nil
|
||||
return ADGuide.new(ADFlowGuide.DEF, draft, context)
|
||||
end
|
||||
|
||||
-- Run-formet tabel til controllerens opslag (varer, vogne) før noget er gemt: markører fra de valgte
|
||||
-- bygninger. markerIds(buildingId, role) -> {wayPointId, ...}.
|
||||
function ADFlowGuide.previewRun(draft, markerIds)
|
||||
local run = ADGuide.deepCopy(draft)
|
||||
local function resolve(ids, role)
|
||||
local result, seen = {}, {}
|
||||
for _, buildingId in ipairs(ids or {}) do
|
||||
for _, wayPointId in ipairs(markerIds(buildingId, role) or {}) do
|
||||
if not seen[wayPointId] then
|
||||
seen[wayPointId] = true
|
||||
table.insert(result, wayPointId)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
run.sourceWayPointIds = resolve(draft.sourceBuildingIds, "load")
|
||||
run.targetWayPointIds = resolve(draft.targetBuildingIds, "unload")
|
||||
run.targetWayPointId = run.targetWayPointIds[1]
|
||||
return run
|
||||
end
|
||||
|
||||
-- ---- vogne til solo-traktorer ----
|
||||
|
||||
local function soloIds(context)
|
||||
local set = {}
|
||||
for _, vehicle in ipairs((context or {}).vehicles or {}) do
|
||||
if vehicle.solo then set[vehicle.id] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
-- Ny vogn-tabel: fravalgte traktorer slipper deres vogn; valgte solo-traktorer uden (gyldig) vogn får den første
|
||||
-- ledige i context.wagons, som ingen anden traktor i flowet har. Vogne til ikke-solo-traktorer bevares (husket).
|
||||
function ADFlowGuide.assignWagons(draft, context)
|
||||
local chosen, solo, offered = {}, soloIds(context), {}
|
||||
for _, id in ipairs(draft.vehicleIds or {}) do chosen[id] = true end
|
||||
for _, wagon in ipairs((context or {}).wagons or {}) do offered[wagon.id] = true end
|
||||
local wagons, used = {}, {}
|
||||
for tractorId, wagonId in pairs(draft.wagons or {}) do
|
||||
local valid = chosen[tractorId] and (not solo[tractorId] or offered[wagonId])
|
||||
if valid and not used[wagonId] then
|
||||
wagons[tractorId] = wagonId
|
||||
used[wagonId] = true
|
||||
end
|
||||
end
|
||||
for _, tractorId in ipairs(draft.vehicleIds or {}) do
|
||||
if solo[tractorId] and wagons[tractorId] == nil then
|
||||
for _, wagon in ipairs((context or {}).wagons or {}) do
|
||||
if not used[wagon.id] then
|
||||
wagons[tractorId] = wagon.id
|
||||
used[wagon.id] = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return wagons
|
||||
end
|
||||
|
||||
-- ---- valg ----
|
||||
|
||||
local SET_VALUES = {
|
||||
kind = true, ingredientMode = true, sellAtPercent = true, storeAbovePercent = true, sellAbovePercent = true, loops = true,
|
||||
returnBelowPercent = true, waitPoolGroup = true,
|
||||
}
|
||||
local BOOL_VALUES = {showAllBuildings = true, storeAuto = true, collectRound = true}
|
||||
local LIST_VALUES = {ingredients = true, storeBuildingIds = true, vehicleIds = true}
|
||||
|
||||
local function setPolicy(products, name, policy)
|
||||
local result = {}
|
||||
for _, product in ipairs(products or {}) do
|
||||
if product.fillType ~= name then table.insert(result, {fillType = product.fillType, policy = product.policy}) end
|
||||
end
|
||||
if policy ~= "off" then table.insert(result, {fillType = name, policy = policy}) end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADFlowGuide.choose(guide, id, value)
|
||||
if id == "showBlocked" then
|
||||
-- visning, ikke en del af flowet: fold listen over traktorer der ikke kan bruges ud/ind
|
||||
local copy = {}
|
||||
for key, field in pairs(guide) do copy[key] = field end
|
||||
copy.showBlocked = not guide.showBlocked
|
||||
return copy
|
||||
end
|
||||
local draft = ADGuide.deepCopy(guide.draft)
|
||||
if SET_VALUES[id] then
|
||||
draft[id] = value
|
||||
elseif BOOL_VALUES[id] then
|
||||
draft[id] = value == true
|
||||
elseif LIST_VALUES[id] then
|
||||
draft[id] = ADGuide.toggleItem(draft[id], value.item, value.on)
|
||||
if id == "vehicleIds" then draft.wagons = ADFlowGuide.assignWagons(draft, guide.context) end
|
||||
elseif id == "wagon" then
|
||||
draft.wagons = ADGuide.deepCopy(draft.wagons or {})
|
||||
draft.wagons[value.item] = value.value
|
||||
elseif id == "buildings" then
|
||||
local key = draft.kind == "outbound" and "sourceBuildingIds" or "targetBuildingIds"
|
||||
draft[key] = ADGuide.toggleItem(draft[key], value.item, value.on)
|
||||
elseif id == "policy" then
|
||||
draft.products = setPolicy(draft.products, value.item, value.value)
|
||||
elseif id == "name" then
|
||||
if type(value) ~= "string" or not value:match("%S") then return guide end
|
||||
draft.name = value
|
||||
else
|
||||
return guide
|
||||
end
|
||||
return ADGuide.withDraft(guide, draft)
|
||||
end
|
||||
|
||||
-- ---- rækker ----
|
||||
|
||||
local function choice(id, title, options, value, item)
|
||||
return {type = "choice", id = id, title = title, options = options, value = value, item = item}
|
||||
end
|
||||
|
||||
local function toggle(id, title, value)
|
||||
return {type = "toggle", id = id, title = title, off = L("ui_no"), on = L("ui_yes"), value = value == true}
|
||||
end
|
||||
|
||||
local function check(id, item, text, value)
|
||||
return {type = "check", id = id, item = item, title = T(text), value = value == true}
|
||||
end
|
||||
|
||||
local function text(key, args) return {type = "text", label = L(key, args)} end
|
||||
|
||||
local function percentOptions(from, to, step)
|
||||
local options = {}
|
||||
for percent = from, to, step do table.insert(options, {label = T(string.format("%d %%", percent)), value = percent}) end
|
||||
return options
|
||||
end
|
||||
|
||||
-- "Sælg altid over": Fra, 50-100 %
|
||||
local function sellAboveOptions()
|
||||
local options = {{label = L("spu_sellAbove_off"), value = 0}}
|
||||
for _, option in ipairs(percentOptions(50, ADRuns.MAX_SELL_ABOVE_PERCENT, 5)) do table.insert(options, option) end
|
||||
return options
|
||||
end
|
||||
|
||||
local function chosenSet(list)
|
||||
local set = {}
|
||||
for _, value in ipairs(list or {}) do set[value] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
local function buildingIds(draft)
|
||||
return draft.kind == "outbound" and draft.sourceBuildingIds or draft.targetBuildingIds
|
||||
end
|
||||
|
||||
local function rowsName(guide)
|
||||
return {
|
||||
{type = "text", label = T(guide.draft.name or "")},
|
||||
{type = "link", id = "rename", value = true, label = L("spu_guide_flow_rename")},
|
||||
}
|
||||
end
|
||||
|
||||
local function rowsDirection(guide)
|
||||
return {choice("kind", L("spu_direction"), {
|
||||
{label = L("spu_direction_in"), value = "supply"}, {label = L("spu_direction_out"), value = "outbound"}}, guide.draft.kind)}
|
||||
end
|
||||
|
||||
-- Bygninger i fast rækkefølge (navn, markør); uden markør kun med "Vis alle" (eller når valgt).
|
||||
local function rowsBuildings(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local chosen = chosenSet(buildingIds(draft))
|
||||
local candidates = guide.context.buildings or {}
|
||||
local shown = ADFlowView.buildingRows(candidates, chosen, draft.showAllBuildings)
|
||||
local hidden = ADFlowView.hiddenCount(candidates, shown)
|
||||
if hidden > 0 or draft.showAllBuildings then
|
||||
table.insert(rows, toggle("showAllBuildings", L("spu_showAllBuildings", {hidden}), draft.showAllBuildings))
|
||||
end
|
||||
for _, building in ipairs(shown) do
|
||||
table.insert(rows, check("buildings", building.id, building.text or building.name, chosen[building.id]))
|
||||
end
|
||||
if #shown == 0 then table.insert(rows, text("spu_noBuildings")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsGoods(guide)
|
||||
local draft, context, rows = guide.draft, guide.context, {}
|
||||
if isOut(guide) then
|
||||
local products = context.products or {}
|
||||
if #products == 0 then return {text("spu_guide_flow_noProducts")} end
|
||||
local options = {}
|
||||
for _, policy in ipairs(ADFlowGuide.POLICIES) do table.insert(options, {label = L("spu_policy_" .. policy), value = policy}) end
|
||||
for _, product in ipairs(products) do
|
||||
table.insert(rows, choice("policy", T(product.text or product.title), options, ADRuns.getProductPolicy(draft, product.name), product.name))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
table.insert(rows, choice("ingredientMode", L("spu_guide_flow_goods"), {
|
||||
{label = L("spu_guide_flow_goodsAutoChoice"), value = "auto"}, {label = L("spu_guide_manual"), value = "manual"}}, draft.ingredientMode))
|
||||
if draft.ingredientMode ~= "manual" then
|
||||
table.insert(rows, text("spu_guide_flow_goodsAutoText"))
|
||||
return rows
|
||||
end
|
||||
local chosen = chosenSet(draft.ingredients)
|
||||
for _, fillType in ipairs(context.fillTypes or {}) do
|
||||
table.insert(rows, check("ingredients", fillType.name, " " .. tostring(fillType.title), chosen[fillType.name]))
|
||||
end
|
||||
if #(context.fillTypes or {}) == 0 then table.insert(rows, text("spu_guide_flow_noGoodsTo")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsStore(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
|
||||
{label = L("spu_guide_flow_storeAutoChoice"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeAuto))
|
||||
if draft.storeAuto then
|
||||
table.insert(rows, text("spu_guide_flow_storeAutoText"))
|
||||
return rows
|
||||
end
|
||||
local chosen = chosenSet(draft.storeBuildingIds)
|
||||
for _, building in ipairs(guide.context.storeBuildings or {}) do
|
||||
table.insert(rows, check("storeBuildingIds", building.id, building.text, chosen[building.id]))
|
||||
end
|
||||
if #(guide.context.storeBuildings or {}) == 0 then table.insert(rows, text("spu_noBuildings")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function productPolicies(draft)
|
||||
local policies = {}
|
||||
for _, product in ipairs(draft.products or {}) do table.insert(policies, product.policy) end
|
||||
return policies
|
||||
end
|
||||
|
||||
-- Kun regler varernes politik bruger: rent lager-flow viser ikke salgsgrænser og omvendt (in-game 29/9).
|
||||
local function rowsRules(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local rows = {}
|
||||
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
||||
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||
return rows
|
||||
end
|
||||
local loops = {{label = L("spu_loops_infinite"), value = 0}}
|
||||
for count = 1, ADRuns.MAX_LOOPS do table.insert(loops, {label = T(tostring(count)), value = count}) end
|
||||
return {
|
||||
choice("loops", L("spu_loops"), loops, draft.loops),
|
||||
choice("returnBelowPercent", L("spu_returnBelow"), percentOptions(ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10), draft.returnBelowPercent),
|
||||
}
|
||||
end
|
||||
|
||||
local function rowsWait(guide)
|
||||
local options = {{label = L("spu_waitPool_all"), value = ""}}
|
||||
for _, pool in ipairs(guide.context.waitPools or {}) do
|
||||
table.insert(options, {label = T(string.format("%s (%d)", tostring(pool.group), pool.count or 0)), value = pool.group})
|
||||
end
|
||||
return {choice("waitPoolGroup", L("spu_waitPool"), options, guide.draft.waitPoolGroup or "")}
|
||||
end
|
||||
|
||||
-- Vognvalg under en valgt solo-traktor: dens egen vogn + ledige vogne ingen anden traktor i flowet har.
|
||||
local function rowWagon(guide, tractorId)
|
||||
local wagons = guide.draft.wagons or {}
|
||||
local usedByOthers = {}
|
||||
for otherId, wagonId in pairs(wagons) do
|
||||
if otherId ~= tractorId then usedByOthers[wagonId] = true end
|
||||
end
|
||||
local options = {}
|
||||
for _, wagon in ipairs(guide.context.wagons or {}) do
|
||||
if not usedByOthers[wagon.id] then table.insert(options, {label = T(tostring(wagon.label)), value = wagon.id}) end
|
||||
end
|
||||
if #options == 0 or wagons[tractorId] == nil then return text("spu_guide_flow_noFreeWagon") end
|
||||
return choice("wagon", L("spu_guide_flow_wagon"), options, wagons[tractorId], tractorId)
|
||||
end
|
||||
|
||||
-- Traktorer: flowets egne først, så de ledige. Traktorer i andre flows og blokerede (vognen passer ikke)
|
||||
-- skjules med en linje hver.
|
||||
local function rowsTractors(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local inRun = chosenSet(draft.vehicleIds)
|
||||
local entries = {}
|
||||
local other, blocked = 0, 0
|
||||
local blockedTexts = {}
|
||||
for _, vehicle in ipairs(guide.context.vehicles or {}) do
|
||||
local state = "free"
|
||||
if inRun[vehicle.id] then
|
||||
state = "in"
|
||||
elseif vehicle.otherRunId ~= nil and vehicle.otherRunId ~= draft.id then
|
||||
state = "other"
|
||||
other = other + 1
|
||||
elseif vehicle.blocked then
|
||||
state = "blocked"
|
||||
blocked = blocked + 1
|
||||
table.insert(blockedTexts, tostring(vehicle.text))
|
||||
end
|
||||
table.insert(entries, {id = vehicle.id, label = tostring(vehicle.text), state = state})
|
||||
end
|
||||
local solo = soloIds(guide.context)
|
||||
for _, entry in ipairs(ADFlowView.vehicleRows(entries, false)) do
|
||||
table.insert(rows, check("vehicleIds", entry.id, entry.label, entry.state == "in"))
|
||||
if entry.state == "in" and solo[entry.id] then
|
||||
local wagonRow = rowWagon(guide, entry.id)
|
||||
if wagonRow ~= nil then table.insert(rows, wagonRow) end
|
||||
end
|
||||
end
|
||||
if #rows == 0 then table.insert(rows, text("spu_guide_flow_noTractors")) end
|
||||
if other > 0 then table.insert(rows, text("spu_guide_flow_otherFlows", {other})) end
|
||||
if blocked > 0 then
|
||||
-- foldet sammen som standard; linjen med antallet folder navn + grund ud/ind
|
||||
local key = guide.showBlocked and "spu_guide_flow_blockedHide" or "spu_guide_flow_blockedShow"
|
||||
table.insert(rows, {type = "link", id = "showBlocked", value = true, label = L(key, {blocked})})
|
||||
if guide.showBlocked then
|
||||
table.sort(blockedTexts)
|
||||
for _, line in ipairs(blockedTexts) do table.insert(rows, {type = "text", label = T(" " .. line)}) end
|
||||
end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsSummary(guide)
|
||||
local rows = {}
|
||||
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local ROWS = {
|
||||
name = rowsName, direction = rowsDirection, buildings = rowsBuildings, goods = rowsGoods, store = rowsStore,
|
||||
rules = rowsRules, wait = rowsWait, tractors = rowsTractors, summary = rowsSummary,
|
||||
}
|
||||
|
||||
function ADFlowGuide.rows(guide)
|
||||
local builder = ROWS[guide.step]
|
||||
return builder ~= nil and builder(guide) or {}
|
||||
end
|
||||
|
||||
-- ---- resumé ----
|
||||
|
||||
-- Korte navne til resuméet (field, ellers navn, ellers rækkens tekst).
|
||||
local function namesFor(ids, entries, field)
|
||||
local byId = {}
|
||||
for _, entry in ipairs(entries or {}) do byId[entry.id or entry.name] = entry[field] or entry.name or entry.text end
|
||||
local names = {}
|
||||
for _, id in ipairs(ids or {}) do table.insert(names, tostring(byId[id] or id)) end
|
||||
return names
|
||||
end
|
||||
|
||||
local SUMMARY = {
|
||||
name = function(guide) return {T(guide.draft.name or "")} end,
|
||||
direction = function(guide) return {L(isOut(guide) and "spu_direction_out" or "spu_direction_in")} end,
|
||||
buildings = function(guide)
|
||||
local names = namesFor(buildingIds(guide.draft), guide.context.buildings, "name")
|
||||
if #names == 0 then return {L("spu_guide_flow_none")} end
|
||||
return {T(ADFlowView.joinCounted(names, " + ", ""))}
|
||||
end,
|
||||
goods = function(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local parts = {}
|
||||
local titles = {}
|
||||
for _, product in ipairs(guide.context.products or {}) do titles[product.name] = product.title end
|
||||
for _, product in ipairs(draft.products or {}) do
|
||||
table.insert(parts, {key = "spu_policy_" .. product.policy, prefix = tostring(titles[product.fillType] or product.fillType) .. ": "})
|
||||
end
|
||||
if #parts == 0 then return {L("spu_guide_flow_none")} end
|
||||
return parts
|
||||
end
|
||||
if draft.ingredientMode ~= "manual" then return {L("spu_guide_flow_goodsAuto")} end
|
||||
if #draft.ingredients == 0 then return {L("spu_guide_flow_none")} end
|
||||
if #draft.ingredients > ADFlowGuide.MAX_NAMED_GOODS then return {L("spu_guide_flow_sumGoods", {#draft.ingredients})} end
|
||||
local byName = {}
|
||||
for _, fillType in ipairs(guide.context.fillTypes or {}) do byName[fillType.name] = fillType.title end
|
||||
local names = {}
|
||||
for _, name in ipairs(draft.ingredients) do table.insert(names, tostring(byName[name] or name)) end
|
||||
return {T(table.concat(names, " + "))}
|
||||
end,
|
||||
store = function(guide)
|
||||
local draft = guide.draft
|
||||
if draft.storeAuto or #draft.storeBuildingIds == 0 then return {L("spu_guide_sum_auto")} end
|
||||
if #draft.storeBuildingIds > 2 then return {L("spu_guide_sum_stores", {#draft.storeBuildingIds})} end
|
||||
return {T(table.concat(namesFor(draft.storeBuildingIds, guide.context.storeBuildings, "name"), " + "))}
|
||||
end,
|
||||
rules = function(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||
return parts
|
||||
end
|
||||
local loops = (draft.loops or 0) > 0 and L("spu_guide_flow_sumLoops", {draft.loops}) or L("spu_guide_flow_sumLoopsInf")
|
||||
return {loops, L("spu_guide_flow_sumReturn", {draft.returnBelowPercent})}
|
||||
end,
|
||||
wait = function(guide)
|
||||
local group = guide.draft.waitPoolGroup or ""
|
||||
return {group == "" and L("spu_waitPool_all") or T(group)}
|
||||
end,
|
||||
tractors = function(guide)
|
||||
local ids = guide.draft.vehicleIds
|
||||
if #ids == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||
if #ids > ADFlowGuide.MAX_NAMED_TRACTORS then return {L("spu_guide_flow_sumTractors", {#ids})} end
|
||||
-- traktorer der ikke findes (solgt, eller ikke indlæst endnu) vises ikke som rå id
|
||||
local known = {}
|
||||
for _, vehicle in ipairs(guide.context.vehicles or {}) do known[vehicle.id] = true end
|
||||
local present = {}
|
||||
for _, id in ipairs(ids) do if known[id] then table.insert(present, id) end end
|
||||
if #present == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||
return {T(table.concat(namesFor(present, guide.context.vehicles, "label"), ", "))}
|
||||
end,
|
||||
}
|
||||
|
||||
-- Resumé-linjer {step, title, parts}: Navn (altid) + de synlige trin.
|
||||
function ADFlowGuide.summary(guide)
|
||||
local lines = {}
|
||||
for _, step in ipairs(ADFlowGuide.ORDER) do
|
||||
local build = SUMMARY[step]
|
||||
if build ~= nil and (step == "name" or ADFlowGuide.isVisible(step, guide)) then
|
||||
table.insert(lines, {step = step, title = L(ADFlowGuide.stepKey(step, guide)), parts = build(guide)})
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- gem ----
|
||||
|
||||
function ADFlowGuide.result(guide)
|
||||
local draft = guide.draft
|
||||
local storeIds = ADGuide.deepCopy(draft.storeBuildingIds)
|
||||
if ADFlowGuide.isVisible("store", guide) and draft.storeAuto then storeIds = {} end
|
||||
local changes = {
|
||||
kind = draft.kind, sourceBuildingIds = ADGuide.deepCopy(draft.sourceBuildingIds),
|
||||
targetBuildingIds = ADGuide.deepCopy(draft.targetBuildingIds), storeBuildingIds = storeIds,
|
||||
products = ADGuide.deepCopy(draft.products), ingredientMode = draft.ingredientMode,
|
||||
ingredients = ADGuide.deepCopy(draft.ingredients), sellAtPercent = draft.sellAtPercent,
|
||||
storeAbovePercent = draft.storeAbovePercent, sellAbovePercent = draft.sellAbovePercent or 0,
|
||||
collectRound = draft.collectRound, loops = draft.loops,
|
||||
returnBelowPercent = draft.returnBelowPercent, waitPoolGroup = draft.waitPoolGroup or "",
|
||||
}
|
||||
if draft.name ~= nil and draft.name:match("%S") then changes.name = draft.name end
|
||||
return {isNew = draft.isNew == true, runId = draft.id, name = draft.name, kind = draft.kind, changes = changes,
|
||||
vehicleIds = ADGuide.deepCopy(draft.vehicleIds), wagons = ADFlowGuide.resultWagons(draft)}
|
||||
end
|
||||
|
||||
-- Vogne for de valgte traktorer (til run.wagons ved gem).
|
||||
function ADFlowGuide.resultWagons(draft)
|
||||
local chosen, wagons = chosenSet(draft.vehicleIds), {}
|
||||
for tractorId, wagonId in pairs(draft.wagons or {}) do
|
||||
if chosen[tractorId] then wagons[tractorId] = wagonId end
|
||||
end
|
||||
return wagons
|
||||
end
|
||||
|
||||
-- Traktorer der skal tildeles/fjernes efter gem.
|
||||
function ADFlowGuide.vehicleChanges(oldIds, newIds)
|
||||
local old, new = chosenSet(oldIds), chosenSet(newIds)
|
||||
local added, removed = {}, {}
|
||||
for _, id in ipairs(newIds or {}) do if not old[id] then table.insert(added, id) end end
|
||||
for _, id in ipairs(oldIds or {}) do if not new[id] then table.insert(removed, id) end end
|
||||
return added, removed
|
||||
end
|
||||
@@ -101,18 +101,27 @@ end
|
||||
-- Bygninger: buildings = {{id, name, hasMarker}}, chosen = {[id] = true}. Valgte først; bygninger uden
|
||||
-- markør kun hvis valgte eller showAll (de kan alligevel ikke bruges før en markør er sat).
|
||||
function ADFlowView.buildingRows(buildings, chosen, showAll)
|
||||
local picked, withMarker, withoutMarker = {}, {}, {}
|
||||
local shown, withoutMarker = {}, {}
|
||||
for _, building in ipairs(buildings or {}) do
|
||||
if chosen ~= nil and chosen[building.id] then
|
||||
table.insert(picked, building)
|
||||
elseif building.hasMarker then
|
||||
table.insert(withMarker, building)
|
||||
if (chosen ~= nil and chosen[building.id]) or building.hasMarker then
|
||||
table.insert(shown, building)
|
||||
elseif showAll then
|
||||
table.insert(withoutMarker, building)
|
||||
end
|
||||
end
|
||||
-- fast rækkefølge: bygningsnavn, så markørnavn (label) — et tryk må ikke flytte rækken under markøren
|
||||
local function byNameThenLabel(list)
|
||||
local copy = {}
|
||||
for _, item in ipairs(list) do table.insert(copy, item) end
|
||||
table.sort(copy, function(a, b)
|
||||
if a.name ~= b.name then return ADFlowView.naturalLess(a.name, b.name) end
|
||||
if (a.label or "") ~= (b.label or "") then return ADFlowView.naturalLess(a.label or "", b.label or "") end
|
||||
return tostring(a.id) < tostring(b.id)
|
||||
end)
|
||||
return copy
|
||||
end
|
||||
local rows = {}
|
||||
for _, list in ipairs({sortedBy(picked, "name"), sortedBy(withMarker, "name"), sortedBy(withoutMarker, "name")}) do
|
||||
for _, list in ipairs({byNameThenLabel(shown), byNameThenLabel(withoutMarker)}) do
|
||||
for _, building in ipairs(list) do table.insert(rows, building) end
|
||||
end
|
||||
return rows
|
||||
@@ -138,3 +147,53 @@ function ADFlowView.joinCounted(names, separator, empty)
|
||||
end
|
||||
return table.concat(parts, separator)
|
||||
end
|
||||
|
||||
-- Lang tekst -> linjer på højst maxChars (ombrydes ved mellemrum; indrykning gentages på hver linje,
|
||||
-- så en indrykket forklaring forbliver indrykket). Ét ord længere end linjen deles ikke.
|
||||
function ADFlowView.wrapText(text, maxChars)
|
||||
text = tostring(text or "")
|
||||
if #text <= maxChars then return {text} end
|
||||
local indent = string.match(text, "^%s*") or ""
|
||||
local lines, line = {}, indent
|
||||
for word in string.gmatch(string.sub(text, #indent + 1), "%S+") do
|
||||
if line ~= indent and #line + 1 + #word > maxChars then
|
||||
table.insert(lines, line)
|
||||
line = indent .. word
|
||||
else
|
||||
line = line == indent and (indent .. word) or (line .. " " .. word)
|
||||
end
|
||||
end
|
||||
if line ~= indent then table.insert(lines, line) end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- status pr. flow (Flows-listen) ----
|
||||
|
||||
ADFlowView.LEVEL_COLORS = {
|
||||
running = {0.30, 0.75, 0.35, 1},
|
||||
waiting = {0.95, 0.80, 0.25, 1},
|
||||
error = {0.90, 0.35, 0.30, 1},
|
||||
stopped = {0.55, 0.55, 0.55, 1},
|
||||
}
|
||||
|
||||
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
|
||||
-- Hvilke udkørselsregler betyder noget for varernes politik? policies = {"store"|"sell"|"sellElseStore"|"off"}
|
||||
-- -> sells, stores. Ingen varer med salg/lager endnu = begge (reglerne kan sættes før varerne vælges).
|
||||
function ADFlowView.policyUse(policies)
|
||||
local sells, stores = false, false
|
||||
for _, policy in ipairs(policies or {}) do
|
||||
if policy == "sell" or policy == "sellElseStore" then sells = true end
|
||||
if policy == "store" or policy == "sellElseStore" then stores = true end
|
||||
end
|
||||
if not sells and not stores then return true, true end
|
||||
return sells, stores
|
||||
end
|
||||
|
||||
function ADFlowView.flowStatus(s)
|
||||
if s.stopped then return "stopped" end
|
||||
if s.warning ~= nil then return "error" end
|
||||
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
|
||||
if (s.running or 0) > 0 and s.reason == nil then return "running" end
|
||||
if (s.running or 0) > 0 or s.canStop then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
-- AD Flow Wagons
|
||||
-- Flows ejer deres vogne, men traktor og vogn er ikke låst sammen: den vogn der hænger på en flow-traktor huskes
|
||||
-- (run.wagons = {[traktorId] = vognId}). Starter flowet med en solo-traktor, henter den sin vogn ved "Connect <vognens
|
||||
-- parkering>". Stoppes/slettes flowet, eller er dets bygning væk, sættes vognen på plads og traktoren kører hjem —
|
||||
-- så er begge ledige i maskinpuljen (en stoppet flows vogn må lånes; en kørende flows aldrig).
|
||||
|
||||
ADFlowWagons = {}
|
||||
ADFlowWagons.LOG_PREFIX = "ADSmartPickup: flow-vogne: "
|
||||
ADFlowWagons.REMEMBER_MS = 10000 -- husk vogne / prøv ventende starter igen
|
||||
ADFlowWagons.BUILDING_CHECK_MS = 60000
|
||||
ADFlowWagons.MISSING_CHECKS = 2 -- så mange tjek i træk før en bygning regnes for væk
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
ADFlowWagons.serialize = function(wagons) return ADRuns.serializeWagons(wagons) end
|
||||
ADFlowWagons.parse = function(text) return ADRuns.parseWagons(text) end
|
||||
|
||||
-- Husk traktorens vogn: ny tabel ved ændring, samme tabel når intet ændres.
|
||||
function ADFlowWagons.remember(wagons, tractorId, wagonId)
|
||||
if (wagons or {})[tractorId] == wagonId then return wagons end
|
||||
local copy = ADRuns.copyWagons(wagons)
|
||||
copy[tractorId] = wagonId
|
||||
return copy
|
||||
end
|
||||
|
||||
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
|
||||
function ADFlowWagons.startAction(s)
|
||||
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
|
||||
if s.tractorBusy then return "wait" end
|
||||
if s.hasRear or s.wagonId == nil or not s.wagonExists or not s.hasConnect then return "start" end
|
||||
if s.wagonAttached or s.wagonBusy then return "wait" end
|
||||
return "fetch"
|
||||
end
|
||||
|
||||
-- Hent vogn fejlede: ét nyt forsøg når AD standsede undervejs (forbigående, fx spærret af en anden traktor);
|
||||
-- ikke ved kobling der fejler ved markøren (den skal rettes).
|
||||
ADFlowWagons.FETCH_RETRIES = 1
|
||||
function ADFlowWagons.retryFetch(reason, retries)
|
||||
return tostring(reason):find("^stoppedAway") ~= nil and (retries or 0) < ADFlowWagons.FETCH_RETRIES
|
||||
end
|
||||
|
||||
-- s = {flowRunning, ownWagon, adActive} -> start traktoren efter spillerens egen påkobling?
|
||||
function ADFlowWagons.startAfterManualAttach(s)
|
||||
return s.flowRunning == true and s.ownWagon == true and not s.adActive
|
||||
end
|
||||
|
||||
-- Husk vognen bag traktoren ved hjemtur? Kun når et KØRENDE flow stoppes (reason "stop") — ved oprydning efter
|
||||
-- indlæsning ("afterLoad") kan traktoren have et puljeredskab på fra et lån (in-game 29/9).
|
||||
function ADFlowWagons.shouldRememberOnHome(reason)
|
||||
return reason == "stop"
|
||||
end
|
||||
|
||||
-- Påkobling uden ejer (hentet før genindlæsning, markopgaven tabt): s = {owned, idleMs} -> "home"|"wait"|"none"
|
||||
ADFlowWagons.ORPHAN_MS = 120000
|
||||
-- s.atOwnPark: traktoren holder på sin egen parkering = parkeret sæt, ikke forældreløs (vinmarks-REX, 29/9)
|
||||
ADFlowWagons.OWN_PARK_M = 30
|
||||
function ADFlowWagons.orphanAction(s)
|
||||
if s.owned or s.atOwnPark then return "none" end
|
||||
return (s.idleMs or 0) >= ADFlowWagons.ORPHAN_MS and "home" or "wait"
|
||||
end
|
||||
|
||||
-- Hvorfor "wait" (til loggen). s som startAction.
|
||||
function ADFlowWagons.waitReason(s)
|
||||
if s.tractorBusy then return "tractorBusy" end
|
||||
if s.wagonAttached then return "wagonAttached" end
|
||||
return "wagonReserved"
|
||||
end
|
||||
|
||||
-- Ingen husket vogn: gæt ud fra parkeringsnavnet — en ledig vogn hvis parkering indeholder flowets navn, helst med
|
||||
-- traktorens nummer til sidst ("Cow Food 3" -> "P Cow Food vogn 3"). candidates = {{id, parkName}}, taken = {[id]=true}
|
||||
function ADFlowWagons.guessWagon(runName, tractorNumber, candidates, taken)
|
||||
local wanted = tostring(runName or ""):lower()
|
||||
if wanted == "" then return nil end
|
||||
local matching = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local park = tostring(candidate.parkName or ""):lower()
|
||||
if not (taken or {})[candidate.id] and park:find(wanted, 1, true) ~= nil then table.insert(matching, candidate) end
|
||||
end
|
||||
table.sort(matching, function(a, b) return tostring(a.parkName) < tostring(b.parkName) end)
|
||||
for _, candidate in ipairs(matching) do
|
||||
if tractorNumber ~= nil and tostring(candidate.parkName):match("%s(%d+)$") == tostring(tractorNumber) then return candidate.id end
|
||||
end
|
||||
return matching[1] ~= nil and matching[1].id or nil
|
||||
end
|
||||
|
||||
-- Ledige vogne til flow-guiden, bedste først: parkering med flowets navn, så "Disponibel", så resten (alfabetisk).
|
||||
-- candidates = {{id, label}} -> ny sorteret liste.
|
||||
function ADFlowWagons.rankWagons(runName, candidates)
|
||||
local wanted = tostring(runName or ""):lower()
|
||||
local function rank(wagon)
|
||||
local label = tostring(wagon.label or ""):lower()
|
||||
if wanted ~= "" and label:find(wanted, 1, true) ~= nil then return 1 end
|
||||
if label:find("disponibel", 1, true) ~= nil then return 2 end
|
||||
return 3
|
||||
end
|
||||
local ranked = {}
|
||||
for _, wagon in ipairs(candidates or {}) do table.insert(ranked, wagon) end
|
||||
table.sort(ranked, function(a, b)
|
||||
local ra, rb = rank(a), rank(b)
|
||||
if ra ~= rb then return ra < rb end
|
||||
return tostring(a.label) < tostring(b.label)
|
||||
end)
|
||||
return ranked
|
||||
end
|
||||
|
||||
function ADFlowWagons.ownerOf(runs, wagonId)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
for _, id in pairs(run.wagons or {}) do
|
||||
if id == wagonId then return run end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Holdes vognen af et flow? Flowets vogn er flowets, også når flowet er stoppet — puljen, markarbejde og
|
||||
-- marksilo må ikke låne den (in-game 29/9: marksilo lånte Cow Food-traktoren med vognen på efter stop).
|
||||
function ADFlowWagons.isHeld(runs, wagonId)
|
||||
return ADFlowWagons.ownerOf(runs, wagonId) ~= nil
|
||||
end
|
||||
|
||||
-- Et tjek af om bygningen mangler: -> ny tæller, væk?
|
||||
function ADFlowWagons.missingStep(count, missingNow)
|
||||
if not missingNow then return 0, false end
|
||||
local next = (count or 0) + 1
|
||||
return next, next >= ADFlowWagons.MISSING_CHECKS
|
||||
end
|
||||
|
||||
-- Flowets bygnings-id'er der ikke findes i exists = {[id] = true}. Kun bygninger der er set i denne session
|
||||
-- (seen) tæller: et forældet id i et gammelt flow må ikke stoppe et flow der virker.
|
||||
function ADFlowWagons.missingBuildings(run, exists, seen)
|
||||
local missing = {}
|
||||
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds"}) do
|
||||
for _, id in ipairs(run[key] or {}) do
|
||||
if not exists[id] and (seen or {})[id] then table.insert(missing, id) end
|
||||
end
|
||||
end
|
||||
return missing
|
||||
end
|
||||
|
||||
-- Hvor læsset afleveres ved stop: AD's 2. markør kun hvis den er et af flowets mål — den kan være forældet
|
||||
-- (gammel AD-tilstand) eller et Wait-punkt, Wait-logikken har skiftet til. Ellers flowets første mål; nil = silo.
|
||||
function ADFlowWagons.emptyTarget(secondMarkerId, runTargetIds)
|
||||
local targets = runTargetIds or {}
|
||||
for _, id in ipairs(targets) do
|
||||
if id == secondMarkerId then return id end
|
||||
end
|
||||
return targets[1]
|
||||
end
|
||||
|
||||
-- Tøm vognen før den parkeres (flowet stoppet): flowets mål -> silo/egen fabrik -> hjem.
|
||||
-- stage: "start" | "target" | "dump"; s = {cargo, hasTarget, adActive, elapsedMs}
|
||||
-- -> "home" | "target" | "dump" | "wait" | "giveUp"
|
||||
ADFlowWagons.EMPTY_MIN_LITERS = 100
|
||||
ADFlowWagons.EMPTY_TIMEOUT_MS = 600000
|
||||
ADFlowWagons.EMPTY_STALL_MS = 45000 -- ved målet uden at læsset falder så længe = målet er fuldt
|
||||
ADFlowWagons.EMPTY_NEAR_M = 40 -- m fra aflæsningsmarkøren tæller som "ved målet"
|
||||
ADFlowWagons.MAX_DUMP_ATTEMPTS = 3 -- siloer der prøves før vognen parkeres med last
|
||||
|
||||
-- s = {nearTarget, sinceChangeMs}
|
||||
function ADFlowWagons.isStalled(s)
|
||||
return s.nearTarget == true and (s.sinceChangeMs or 0) >= ADFlowWagons.EMPTY_STALL_MS
|
||||
end
|
||||
|
||||
function ADFlowWagons.emptyStep(stage, s)
|
||||
local cargo = s.cargo or 0
|
||||
if stage == "start" then
|
||||
if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end
|
||||
return s.hasTarget and "target" or "dump"
|
||||
end
|
||||
if (s.elapsedMs or 0) >= ADFlowWagons.EMPTY_TIMEOUT_MS then return "giveUp" end
|
||||
if s.stalled and cargo >= ADFlowWagons.EMPTY_MIN_LITERS then
|
||||
if stage == "target" then return "dump" end
|
||||
-- silo der ikke kunne tage imod: prøv den næste (op til MAX_DUMP_ATTEMPTS)
|
||||
return (s.dumpAttempts or 0) < ADFlowWagons.MAX_DUMP_ATTEMPTS and "dump" or "giveUp"
|
||||
end
|
||||
if s.adActive then return "wait" end
|
||||
if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end
|
||||
if stage == "target" then return "dump" end
|
||||
return "giveUp"
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local waiting = setmetatable({}, {__mode = "k"}) -- traktor -> runId (venter på sin vogn)
|
||||
local fetching = setmetatable({}, {__mode = "k"}) -- traktor -> runId (henter sin vogn)
|
||||
local emptying = setmetatable({}, {__mode = "k"}) -- traktor -> {stage, since, fillType, runName}
|
||||
local homing = setmetatable({}, {__mode = "k"}) -- traktor -> true (flowets hjemtur: vogn af, traktor hjem)
|
||||
local fetchRetries = setmetatable({}, {__mode = "k"}) -- traktor -> antal nye hente-forsøg
|
||||
local lastWaitText = setmetatable({}, {__mode = "k"}) -- traktor -> sidst loggede venteårsag (kun ændringer logges)
|
||||
local orphans = setmetatable({}, {__mode = "k"}) -- traktor -> ms siden påkobling uden ejer
|
||||
local missingCount = {} -- runId -> tjek i træk
|
||||
local seenBuildings = {} -- bygnings-id'er set i denne session
|
||||
local rememberTimer, buildingTimer = 0, 0
|
||||
|
||||
local function runs()
|
||||
return ADRunsController.store ~= nil and ADRunsController.store.runs or {}
|
||||
end
|
||||
|
||||
local function findAnyVehicle(id)
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
if ADRunsController.getVehicleId(vehicle) == id then return vehicle end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function connectOf(adEnv, wagon)
|
||||
local index = wagon.advd ~= nil and wagon.advd.parkDestination or nil
|
||||
local park = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
if park == nil then return nil end
|
||||
local marker = ADPool.connectFor(park.name, adEnv.ADGraphManager:getMapMarkers())
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
-- Traktorer i stoppede flows er ledige i puljen; traktorer i kørende flows aldrig.
|
||||
-- s = {hasRun, stopped, carriesFlowWagon, homeTrip} -> må puljen/markarbejde/marksilo låne traktoren?
|
||||
function ADFlowWagons.tractorFree(s)
|
||||
if not s.hasRun then return true end
|
||||
return s.stopped == true and not s.carriesFlowWagon and not s.homeTrip
|
||||
end
|
||||
|
||||
function ADFlowWagons.isFreeTractor(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local carries = false
|
||||
if run ~= nil then
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(vehicle)) do
|
||||
local wagonId = ADRunsController.getVehicleId(implement.object)
|
||||
if wagonId ~= nil and ADFlowWagons.ownerOf(runs(), wagonId) ~= nil then carries = true end
|
||||
end
|
||||
end
|
||||
return ADFlowWagons.tractorFree({hasRun = run ~= nil, stopped = run ~= nil and run.stopped == true,
|
||||
carriesFlowWagon = carries, homeTrip = emptying[vehicle] ~= nil or homing[vehicle] ~= nil})
|
||||
end
|
||||
|
||||
-- Holder flowet noget: en traktor henter/venter på sin vogn eller har en vogn bagpå (til Stop-knappen).
|
||||
function ADFlowWagons.hasActivity(run, vehiclesById)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = vehiclesById ~= nil and vehiclesById[vehicleId] or nil
|
||||
-- vogn bagpå tæller kun i et flow der er startet: et flow der aldrig kom i gang (fx 'ingen vogn', og spilleren
|
||||
-- har selv koblet på) skal vise Start, ikke Stop (in-game 29/9: Milk Store)
|
||||
local holdsWagon = vehicle ~= nil and ADRunsController.runningRuns[run.id] == true and #ADHitch.rearImplementsOf(vehicle) > 0
|
||||
if vehicle ~= nil and (waiting[vehicle] ~= nil or fetching[vehicle] ~= nil or holdsWagon) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADFlowWagons.hasWagonFor(run, tractorId)
|
||||
return (run.wagons or {})[tractorId] ~= nil
|
||||
end
|
||||
|
||||
function ADFlowWagons.isHeldWagon(wagonId)
|
||||
return ADFlowWagons.isHeld(runs(), wagonId)
|
||||
end
|
||||
|
||||
-- Frakoblede vogne med parkering, som intet kørende flow holder, og som flowet ikke allerede har givet en traktor.
|
||||
local function guessFor(adEnv, run, vehicle, tractorId)
|
||||
local taken = {}
|
||||
for _, id in pairs(run.wagons or {}) do taken[id] = true end
|
||||
local candidates = {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
local attached = any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil
|
||||
if any.spec_motorized == nil and any.spec_attachable ~= nil and not attached and type(index) == "number" and index >= 1
|
||||
and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then
|
||||
local id = ADRunsController.getVehicleId(any)
|
||||
local park = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
if id ~= nil and park ~= nil and not ADFlowWagons.isHeld(runs(), id) then
|
||||
table.insert(candidates, {id = id, parkName = park.name})
|
||||
end
|
||||
end
|
||||
end
|
||||
local number = nil
|
||||
for index, id in ipairs(run.vehicleIds or {}) do
|
||||
if id == tractorId then number = index end
|
||||
end
|
||||
local label = tostring(ADRuns.vehicleLabel(vehicle))
|
||||
number = tonumber(label:match("%s(%d+)$")) or number
|
||||
local wagonId = ADFlowWagons.guessWagon(run.name, number, candidates, taken)
|
||||
if wagonId ~= nil then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' får vognen ved parkeringen der matcher flowets navn", tostring(run.name), label)
|
||||
end
|
||||
return wagonId
|
||||
end
|
||||
|
||||
-- Flowet startes igen mens traktoren tømmer/kører hjem efter et stop: afbryd, så den kører videre med sin vogn.
|
||||
-- Puljens egen hjemtur (fra markarbejde) røres ikke — der sidder et andet redskab bagpå.
|
||||
-- s = {emptying, flowHoming, poolReturning} -> true = afbryd
|
||||
function ADFlowWagons.abortHomeTripOnStart(s)
|
||||
return s.emptying == true or s.flowHoming == true
|
||||
end
|
||||
|
||||
-- Hvilken af isPoolBusy's grunde (til loggen).
|
||||
function ADFlowWagons.busyWhy(vehicle, tractorId)
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil then return "markopgave" end
|
||||
if ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle) then return "lånt af marksilo" end
|
||||
if ADPool ~= nil and ADPool.isReturning(vehicle) then return "sætter redskab af" end
|
||||
if ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle) then return "Courseplay kører" end
|
||||
if emptying[vehicle] ~= nil then return "tømmer vognen" end
|
||||
return "?"
|
||||
end
|
||||
|
||||
-- Lånt ud af puljen: markarbejde, marksilo-lån, på hjemtur eller Courseplay kører den.
|
||||
function ADFlowWagons.isPoolBusy(vehicle, tractorId)
|
||||
return (ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil)
|
||||
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle))
|
||||
or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||
or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle))
|
||||
or (ADFlowWagons.isEmptying ~= nil and ADFlowWagons.isEmptying(vehicle))
|
||||
end
|
||||
|
||||
-- Før en flow-traktor startes: hent sin vogn / vent på den. -> håndteret, resultat (som startVehicle)
|
||||
function ADFlowWagons.beforeStart(adEnv, run, vehicle)
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
if fetching[vehicle] ~= nil then return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"} end
|
||||
if ADFlowWagons.abortHomeTripOnStart({emptying = emptying[vehicle] ~= nil, flowHoming = homing[vehicle] ~= nil}) then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' startet igen — tømning/hjemtur afbrudt, kører videre med vognen",
|
||||
tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
emptying[vehicle] = nil
|
||||
if homing[vehicle] ~= nil then
|
||||
homing[vehicle] = nil
|
||||
ADHitch.cancel(vehicle)
|
||||
end
|
||||
end
|
||||
local wagonId = (run.wagons or {})[tractorId]
|
||||
if wagonId == nil and #ADHitch.rearImplementsOf(vehicle) == 0 then
|
||||
wagonId = guessFor(adEnv, run, vehicle, tractorId)
|
||||
if wagonId ~= nil then run.wagons = ADFlowWagons.remember(run.wagons, tractorId, wagonId) end
|
||||
end
|
||||
local wagon = wagonId ~= nil and findAnyVehicle(wagonId) or nil
|
||||
local connectId = wagon ~= nil and connectOf(adEnv, wagon) or nil
|
||||
local startState = {
|
||||
tractorBusy = ADFlowWagons.isPoolBusy(vehicle, tractorId),
|
||||
hasRear = #ADHitch.rearImplementsOf(vehicle) > 0, wagonId = wagonId, wagonExists = wagon ~= nil,
|
||||
hasConnect = connectId ~= nil,
|
||||
wagonAttached = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() ~= nil,
|
||||
wagonBusy = wagonId ~= nil and ADPool.isReserved(wagonId),
|
||||
}
|
||||
local action = ADFlowWagons.startAction(startState)
|
||||
if action == "start" then lastWaitText[vehicle] = nil return false end
|
||||
ADRunsController.runningRuns[run.id] = true
|
||||
if action == "wait" then
|
||||
local holder = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() or nil
|
||||
local waitText = ADFlowWagons.waitReason(startState) .. ": " .. (startState.tractorBusy and ADFlowWagons.busyWhy(vehicle, tractorId)
|
||||
or (holder ~= nil and ("sidder på " .. tostring(ADRuns.vehicleLabel(holder)))) or "reserveret af puljen")
|
||||
if lastWaitText[vehicle] ~= waitText then
|
||||
lastWaitText[vehicle] = waitText
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' venter på sin vogn (%s)", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), waitText)
|
||||
end
|
||||
waiting[vehicle] = run.id
|
||||
return true, {vehicle = vehicle, ok = true, reason = "waitingWagon"}
|
||||
end
|
||||
waiting[vehicle] = nil
|
||||
lastWaitText[vehicle] = nil
|
||||
fetching[vehicle] = run.id
|
||||
local runId = run.id
|
||||
local okFetch, errFetch = pcall(ADHitch.fetch, adEnv, vehicle, connectId, function(tractor, ok, detail)
|
||||
fetching[tractor] = nil
|
||||
local current = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if current == nil or current.stopped then
|
||||
ADHitch.returnHome(adEnv, tractor)
|
||||
return
|
||||
end
|
||||
if ok then
|
||||
fetchRetries[tractor] = nil
|
||||
local result = ADRunsController.startVehicle(adEnv, current, tractor)
|
||||
if result ~= nil and not result.ok then
|
||||
-- flowet kan ikke startes (markør, bygning …): ikke efterladt ved Connect med vognen
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke starte (%s) — vognen sættes tilbage",
|
||||
tostring(current.name), tostring(ADRuns.vehicleLabel(tractor)), tostring(result.reason))
|
||||
ADHitch.returnHome(adEnv, tractor)
|
||||
end
|
||||
elseif ADFlowWagons.retryFetch(detail, fetchRetries[tractor]) then
|
||||
fetchRetries[tractor] = (fetchRetries[tractor] or 0) + 1
|
||||
waiting[tractor] = runId -- retryWaiting starter den igen om lidt
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s) — prøver igen", tostring(current.name),
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(detail))
|
||||
else
|
||||
fetchRetries[tractor] = nil
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s)", tostring(current.name),
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(detail))
|
||||
end
|
||||
end)
|
||||
if not okFetch then
|
||||
fetching[vehicle] = nil
|
||||
return true, {vehicle = vehicle, ok = false, reason = "error"}
|
||||
end
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' henter sin vogn", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"}
|
||||
end
|
||||
|
||||
-- Stop/slet/bygning væk: vognen tømmes (flowets mål, ellers silo/egen fabrik), sættes på plads, traktoren hjem.
|
||||
local nowMs = 0
|
||||
|
||||
local function cargoOf(adEnv, vehicle)
|
||||
local level, fillType = 0, nil
|
||||
local ok, units = pcall(adEnv.AutoDrive.getAllDischargeableUnits, vehicle)
|
||||
for _, unit in ipairs(ok and units or {}) do
|
||||
local object, index = unit.object, unit.fillUnitIndex
|
||||
if object ~= nil and object.getFillUnitFillLevel ~= nil then
|
||||
local unitLevel = object:getFillUnitFillLevel(index) or 0
|
||||
if unitLevel > 0 then
|
||||
level = level + unitLevel
|
||||
fillType = fillType or object:getFillUnitFillType(index)
|
||||
end
|
||||
end
|
||||
end
|
||||
return level, fillType
|
||||
end
|
||||
|
||||
local function goHome(adEnv, vehicle)
|
||||
emptying[vehicle] = nil
|
||||
homing[vehicle] = true
|
||||
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle, function(tractor) homing[tractor] = nil end)
|
||||
if not (okHome and pooled) then
|
||||
homing[vehicle] = nil
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
end
|
||||
|
||||
local function fillName(fillType)
|
||||
local desc = fillType ~= nil and g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||||
return desc ~= nil and (desc.title or desc.name) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function startDump(adEnv, vehicle, state)
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local isReachable = function(markerId)
|
||||
return ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil or ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId)
|
||||
end
|
||||
-- marksiloer er ikke tømmesteder (de er til markens høst); siloer der allerede fejlede springes over
|
||||
local okSet, fieldSilos = pcall(function() return ADFieldSilos ~= nil and ADFieldSilos.fieldSiloPlaceableSet(adEnv) or nil end)
|
||||
state.triedMarkers = state.triedMarkers or {}
|
||||
local ok, marker, why = pcall(ADLoadSwap.findDumpMarker, adEnv, vehicle, fillType, cargo, nil, isReachable,
|
||||
okSet and fieldSilos or nil, state.triedMarkers, true)
|
||||
if not ok or marker == nil then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' har stadig %d l %s — intet sted at aflæsse (%s); parkeres med last",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(ok and why or marker))
|
||||
goHome(adEnv, vehicle)
|
||||
return
|
||||
end
|
||||
state.stage, state.since = "dump", nowMs
|
||||
state.markerId, state.markerName, state.lastCargo = marker.id, marker.name, nil
|
||||
state.dumpAttempts = (state.dumpAttempts or 0) + 1
|
||||
state.triedMarkers[marker.id] = true
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, marker.id, nil, fillType)
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' aflæsser resten (%d l %s) ved %s før parkering",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(marker.name))
|
||||
end
|
||||
|
||||
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows).
|
||||
-- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
|
||||
local function rememberWagonOf(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
if wagonId == nil or tractorId == nil then return end
|
||||
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
run.wagons = updated
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object)))
|
||||
end
|
||||
end
|
||||
|
||||
-- Én traktor hjem (fra et stoppet/slettet flow): tøm først, så parkering.
|
||||
-- reason: "stop" (et kørende flow stoppes: vognen huskes) | "afterLoad" (oprydning: intet huskes)
|
||||
function ADFlowWagons.homeVehicle(adEnv, vehicle, runName, reason)
|
||||
waiting[vehicle] = nil
|
||||
if ADFlowWagons.shouldRememberOnHome(reason or "stop") then
|
||||
rememberWagonOf(vehicle) -- vognen sættes af nu: flowet skal kunne finde den igen ved næste start
|
||||
end
|
||||
ADHitch.stow(vehicle, "flowet stoppet")
|
||||
if fetching[vehicle] ~= nil then
|
||||
fetching[vehicle] = nil
|
||||
ADHitch.cancel(vehicle)
|
||||
end
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local second = module ~= nil and module.getSecondMarker ~= nil and module:getSecondMarker() or nil
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local targetId = ADFlowWagons.emptyTarget(second ~= nil and second.id or nil, run ~= nil and run.targetWayPointIds or nil)
|
||||
local target = targetId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local action = ADFlowWagons.emptyStep("start", {cargo = cargo, hasTarget = target ~= nil})
|
||||
if action == "home" then return goHome(adEnv, vehicle) end
|
||||
local state = {stage = action, since = nowMs, runName = runName,
|
||||
markerId = target ~= nil and target.id or nil, markerName = target ~= nil and target.name or nil}
|
||||
emptying[vehicle] = state
|
||||
if action == "target" then
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, target.id, nil, fillType)
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' afleverer sit læs (%d l %s) ved %s før parkering",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(target.name))
|
||||
else
|
||||
startDump(adEnv, vehicle, state)
|
||||
end
|
||||
end
|
||||
|
||||
-- ADHitch: spilleren har selv kørt traktoren til Connect og koblet på.
|
||||
function ADFlowWagons.onManualAttach(adEnv, vehicle, wagon)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
orphans[vehicle] = 0 -- ryddes af tickOrphans når nogen ejer opgaven (flow, markopgave, AD, spilleren)
|
||||
if run == nil then return end
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
local wagonId = wagon ~= nil and ADRunsController.getVehicleId(wagon) or nil
|
||||
local remembered = (run.wagons or {})[tractorId]
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
-- ikke stoppet af spilleren = gælder også et flow hvis Start blev afvist ('ingen vogn') — det er derfor han henter vognen
|
||||
if not ADFlowWagons.startAfterManualAttach({flowRunning = not run.stopped,
|
||||
ownWagon = wagonId ~= nil and (remembered == nil or remembered == wagonId),
|
||||
adActive = module ~= nil and module:isActive()}) then return end
|
||||
fetchRetries[vehicle], waiting[vehicle], orphans[vehicle] = nil, nil, nil
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har selv hentet sin vogn — startes", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
end
|
||||
|
||||
-- Traktorer der har koblet et redskab på uden ejer: efter ORPHAN_MS sættes redskabet tilbage og traktoren kører hjem.
|
||||
local function tickOrphans(adEnv, elapsed)
|
||||
for vehicle, idle in pairs(orphans) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local owned = (module ~= nil and module:isActive()) or (vehicle.getIsControlled ~= nil and vehicle:getIsControlled())
|
||||
or (run ~= nil and not run.stopped) or (ADFieldJobs ~= nil and id ~= nil and ADFieldJobs.state.assignments[id] ~= nil)
|
||||
or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle)) or #ADHitch.rearImplementsOf(vehicle) == 0
|
||||
local atOwnPark = false
|
||||
local parkId = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
|
||||
local parkWp = parkId ~= nil and adEnv.ADGraphManager:getWayPointById(parkId) or nil
|
||||
if parkWp ~= nil and vehicle.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
atOwnPark = (x - parkWp.x) ^ 2 + (z - parkWp.z) ^ 2 <= ADFlowWagons.OWN_PARK_M ^ 2
|
||||
end
|
||||
local action = ADFlowWagons.orphanAction({owned = owned, atOwnPark = atOwnPark, idleMs = idle + elapsed})
|
||||
if action == "none" then
|
||||
orphans[vehicle] = nil
|
||||
elseif action == "home" then
|
||||
orphans[vehicle] = nil
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' har et redskab på uden opgave (hentet før genindlæsning?) — sættes tilbage og kører hjem",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
goHome(adEnv, vehicle)
|
||||
else
|
||||
orphans[vehicle] = idle + elapsed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFlowWagons.isEmptying(vehicle)
|
||||
return emptying[vehicle] ~= nil
|
||||
end
|
||||
|
||||
function ADFlowWagons.sendHome(adEnv, run)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
if vehicle ~= nil then
|
||||
local ok, err = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name)
|
||||
if not ok then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "hjem fejlede: %s", tostring(err))
|
||||
goHome(adEnv, vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Står traktoren ved tømningens markør, uden at læsset er faldet i EMPTY_STALL_MS?
|
||||
local function stalledAt(adEnv, vehicle, state, cargo)
|
||||
if state.lastCargo == nil or cargo < state.lastCargo - 1 then
|
||||
state.lastCargo, state.changedAt = cargo, nowMs
|
||||
end
|
||||
local wayPoint = state.markerId ~= nil and adEnv.ADGraphManager:getWayPointById(state.markerId) or nil
|
||||
local near = false
|
||||
if wayPoint ~= nil and vehicle.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
near = math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2) <= ADFlowWagons.EMPTY_NEAR_M
|
||||
end
|
||||
return ADFlowWagons.isStalled({nearTarget = near, sinceChangeMs = nowMs - (state.changedAt or nowMs)})
|
||||
end
|
||||
|
||||
local function tickEmptying(adEnv)
|
||||
local list = {}
|
||||
for vehicle, state in pairs(emptying) do table.insert(list, {vehicle = vehicle, state = state}) end
|
||||
for _, item in ipairs(list) do
|
||||
local vehicle, state = item.vehicle, item.state
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if controlled then
|
||||
emptying[vehicle] = nil -- spilleren har overtaget
|
||||
else
|
||||
local stalled = stalledAt(adEnv, vehicle, state, cargo)
|
||||
local action = ADFlowWagons.emptyStep(state.stage, {cargo = cargo, adActive = module ~= nil and module:isActive(),
|
||||
elapsedMs = nowMs - state.since, stalled = stalled, dumpAttempts = state.dumpAttempts})
|
||||
if stalled and action ~= "wait" then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' kan ikke aflæsse ved %s (fuldt?) — videre", tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
tostring(state.markerName))
|
||||
end
|
||||
if action == "home" then
|
||||
goHome(adEnv, vehicle)
|
||||
elseif action == "dump" then
|
||||
startDump(adEnv, vehicle, state)
|
||||
elseif action == "giveUp" then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' kunne ikke tømme vognen (%d l %s tilbage) — parkeres med last",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType))
|
||||
goHome(adEnv, vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function rememberAll(adEnv)
|
||||
for _, run in ipairs(runs()) do
|
||||
if not run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
if wagonId ~= nil then
|
||||
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
run.wagons = updated
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function retryWaiting(adEnv)
|
||||
local list = {}
|
||||
for vehicle, runId in pairs(waiting) do table.insert(list, {vehicle = vehicle, runId = runId}) end
|
||||
for _, item in ipairs(list) do
|
||||
waiting[item.vehicle] = nil
|
||||
local run = ADRuns.getRun(ADRunsController.store, item.runId)
|
||||
if run ~= nil and not run.stopped then pcall(ADRunsController.startVehicle, adEnv, run, item.vehicle) end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkBuildings(adEnv)
|
||||
local exists = {}
|
||||
local placeables = g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
local id = ADBuildings.getId(placeable)
|
||||
if id ~= nil then exists[id] = true end
|
||||
end
|
||||
if next(exists) == nil then return end -- listen er ikke klar: sig intet
|
||||
for id in pairs(exists) do seenBuildings[id] = true end
|
||||
for _, run in ipairs(runs()) do
|
||||
if not run.stopped then
|
||||
local missing = ADFlowWagons.missingBuildings(run, exists, seenBuildings)
|
||||
local count, gone = ADFlowWagons.missingStep(missingCount[run.id], #missing > 0)
|
||||
missingCount[run.id] = count
|
||||
if gone then
|
||||
missingCount[run.id] = 0
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': bygning findes ikke længere (%s) — stoppet, traktorer kørt hjem",
|
||||
tostring(run.name), table.concat(missing, ", "))
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||
pcall(ADFieldJobs.notify, string.format("Flow '%s': bygning findes ikke længere — stoppet, traktorer kørt hjem", tostring(run.name)), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Én gang efter indlæsning: et stoppet flows traktor der holder stille med sin vogn (gemt midt i hjemturen — AD
|
||||
-- kørte den færdig, men afkoblingen er glemt) køres hjem. Ikke løbende: spilleren kan selv have koblet på.
|
||||
ADFlowWagons.AFTER_LOAD_MS = 120000
|
||||
local sinceLoad, cleanedUp = 0, false
|
||||
|
||||
-- Efter indlæsning: traktorer uden for flows med et puljeredskab på (hentet før gemning, opgaven tabt) følges som
|
||||
-- forældreløse — får de ingen opgave inden ORPHAN_MS, sættes redskabet tilbage (in-game 29/9: 3 læssevogne).
|
||||
local function adoptOrphansAfterLoad(adEnv)
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
if (run == nil or run.stopped) and vehicle.spec_attacherJoints ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
and not ADFlowWagons.isEmptying(vehicle) and not homing[vehicle] then
|
||||
orphans[vehicle] = orphans[vehicle] or 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanupAfterLoad(adEnv)
|
||||
for _, run in ipairs(runs()) do
|
||||
if run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
local module = vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local controlled = vehicle ~= nil and vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if module ~= nil and not module:isActive() and not controlled and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
and not ADFlowWagons.isPoolBusy(vehicle, vehicleId) then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s' er stoppet: '%s' tømmer og sætter sin vogn på plads", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name, "afterLoad")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local emptyTimer = 0
|
||||
|
||||
function ADFlowWagons.tick(adEnv, dt)
|
||||
if adEnv == nil or ADRunsController.store == nil then return end
|
||||
if ADHitch ~= nil and ADHitch.onManualAttach == nil then ADHitch.onManualAttach = ADFlowWagons.onManualAttach end
|
||||
nowMs = nowMs + dt
|
||||
emptyTimer = emptyTimer + dt
|
||||
if emptyTimer >= 2000 then
|
||||
emptyTimer = 0
|
||||
local okOrphan, errOrphan = pcall(tickOrphans, adEnv, 2000)
|
||||
if not okOrphan then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave: %s", tostring(errOrphan)) end
|
||||
local okEmpty, errEmpty = pcall(tickEmptying, adEnv)
|
||||
if not okEmpty then Logging.warning(ADFlowWagons.LOG_PREFIX .. "tømning: %s", tostring(errEmpty)) end
|
||||
end
|
||||
if not cleanedUp then
|
||||
sinceLoad = sinceLoad + dt
|
||||
if sinceLoad >= ADFlowWagons.AFTER_LOAD_MS then
|
||||
cleanedUp = true
|
||||
local ok, err = pcall(cleanupAfterLoad, adEnv)
|
||||
if not ok then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning efter indlæsning: %s", tostring(err)) end
|
||||
local okAdopt, errAdopt = pcall(adoptOrphansAfterLoad, adEnv)
|
||||
if not okAdopt then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave efter indlæsning: %s", tostring(errAdopt)) end
|
||||
local okPrune, errPrune = pcall(ADRunsController.pruneMissingVehicles, adEnv)
|
||||
if not okPrune then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning af solgte traktorer: %s", tostring(errPrune)) end
|
||||
end
|
||||
end
|
||||
rememberTimer = rememberTimer + dt
|
||||
buildingTimer = buildingTimer + dt
|
||||
if rememberTimer >= ADFlowWagons.REMEMBER_MS then
|
||||
rememberTimer = 0
|
||||
rememberAll(adEnv)
|
||||
retryWaiting(adEnv)
|
||||
end
|
||||
if buildingTimer >= ADFlowWagons.BUILDING_CHECK_MS then
|
||||
buildingTimer = 0
|
||||
checkBuildings(adEnv)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
-- AD Guide
|
||||
-- Fælles guide-motor (ren Lua, ingen spil-API) for "Opsæt mark" og "Opsæt flow": ét trin ad gangen,
|
||||
-- trin der ikke er relevante springes over, tælling "Trin 3 af 7" over de synlige trin, og [Ret] der
|
||||
-- åbner ét enkelt trin. Motoren kender ikke marker eller flows; en trin-definition giver rækkefølgen og
|
||||
-- hvornår et trin vises:
|
||||
-- def = {order = {stepId, ...}, isVisible = function(stepId, guide) -> bool}
|
||||
-- Guiden er en tabel {def, draft, context, step, single, returnTo, snapshot}; alle funktioner returnerer
|
||||
-- en ny guide (kladden kopieres), så [Annullér] aldrig har ændret noget.
|
||||
|
||||
ADGuide = {}
|
||||
|
||||
-- Kopi af rene data (tabeller uden cykler); funktioner og tal/tekster deles.
|
||||
function ADGuide.deepCopy(value)
|
||||
if type(value) ~= "table" then return value end
|
||||
local copy = {}
|
||||
for key, item in pairs(value) do copy[key] = ADGuide.deepCopy(item) end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Ny liste med item tilføjet (on) eller fjernet; aldrig dubletter.
|
||||
function ADGuide.toggleItem(list, item, on)
|
||||
local result, found = {}, false
|
||||
for _, value in ipairs(list or {}) do
|
||||
if value == item then
|
||||
found = true
|
||||
if on then table.insert(result, value) end
|
||||
else
|
||||
table.insert(result, value)
|
||||
end
|
||||
end
|
||||
if on and not found then table.insert(result, item) end
|
||||
return result
|
||||
end
|
||||
|
||||
local function clone(guide, changes)
|
||||
local copy = {}
|
||||
for key, value in pairs(guide) do copy[key] = value end
|
||||
for key, value in pairs(changes or {}) do
|
||||
if value == false and (key == "snapshot") then copy[key] = nil else copy[key] = value end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function isVisible(guide, step)
|
||||
return guide.def.isVisible == nil or guide.def.isVisible(step, guide) == true
|
||||
end
|
||||
|
||||
function ADGuide.visibleSteps(guide)
|
||||
local steps = {}
|
||||
for _, step in ipairs(guide.def.order) do
|
||||
if isVisible(guide, step) then table.insert(steps, step) end
|
||||
end
|
||||
return steps
|
||||
end
|
||||
|
||||
local function orderIndex(guide, step)
|
||||
for index, id in ipairs(guide.def.order) do
|
||||
if id == step then return index end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Første synlige trin efter (dir = 1) eller før (dir = -1) det aktuelle i rækkefølgen.
|
||||
local function neighbour(guide, dir)
|
||||
local order = guide.def.order
|
||||
local index = orderIndex(guide, guide.step) + dir
|
||||
while index >= 1 and index <= #order do
|
||||
if isVisible(guide, order[index]) then return order[index] end
|
||||
index = index + dir
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- options = {step = id, single = bool, returnTo = "close"|"summary"}
|
||||
function ADGuide.new(def, draft, context, options)
|
||||
options = options or {}
|
||||
local guide = {def = def, draft = draft, context = context, step = options.step,
|
||||
single = options.single == true, returnTo = options.returnTo or "close"}
|
||||
if guide.step == nil then
|
||||
guide.step = ADGuide.visibleSteps(guide)[1] or def.order[1]
|
||||
end
|
||||
if guide.single then guide.snapshot = ADGuide.deepCopy(draft) end
|
||||
return guide
|
||||
end
|
||||
|
||||
-- index (nil hvis det aktuelle trin ikke er synligt), total synlige trin.
|
||||
function ADGuide.position(guide)
|
||||
local steps = ADGuide.visibleSteps(guide)
|
||||
local index = nil
|
||||
for position, step in ipairs(steps) do
|
||||
if step == guide.step then index = position end
|
||||
end
|
||||
return index, #steps
|
||||
end
|
||||
|
||||
-- Sidste trin: ingen synlige efter, eller et [Ret]-trin der gemmer direkte.
|
||||
function ADGuide.isLast(guide)
|
||||
if guide.single and guide.returnTo == "close" then return true end
|
||||
if guide.single then return false end
|
||||
return neighbour(guide, 1) == nil
|
||||
end
|
||||
|
||||
-- Næste: event "done" når der gemmes (sidste trin, eller [Ret] fra fanens oversigt).
|
||||
function ADGuide.next(guide)
|
||||
if guide.single then
|
||||
if guide.returnTo == "summary" then
|
||||
return clone(guide, {step = "summary", single = false, snapshot = false}), nil
|
||||
end
|
||||
return guide, "done"
|
||||
end
|
||||
local step = neighbour(guide, 1)
|
||||
if step == nil then return guide, "done" end
|
||||
return clone(guide, {step = step}), nil
|
||||
end
|
||||
|
||||
-- Tilbage: event "cancel" når guiden lukkes (første trin, eller [Ret] fra fanens oversigt).
|
||||
-- I et [Ret]-trin fra guidens eget oversigts-trin fortrydes trinnets ændringer.
|
||||
function ADGuide.back(guide)
|
||||
if guide.single then
|
||||
if guide.returnTo == "summary" then
|
||||
return clone(guide, {step = "summary", single = false, draft = guide.snapshot or guide.draft, snapshot = false}), nil
|
||||
end
|
||||
return guide, "cancel"
|
||||
end
|
||||
local step = neighbour(guide, -1)
|
||||
if step == nil then return guide, "cancel" end
|
||||
return clone(guide, {step = step}), nil
|
||||
end
|
||||
|
||||
-- [Ret]: åbn kun ét trin. returnTo "summary" (guidens oversigt) eller "close" (gem og luk).
|
||||
function ADGuide.jump(guide, step, returnTo)
|
||||
return clone(guide, {step = step, single = true, returnTo = returnTo or "close", snapshot = ADGuide.deepCopy(guide.draft)})
|
||||
end
|
||||
|
||||
function ADGuide.withDraft(guide, draft)
|
||||
return clone(guide, {draft = draft})
|
||||
end
|
||||
|
||||
function ADGuide.withContext(guide, context)
|
||||
return clone(guide, {context = context})
|
||||
end
|
||||
|
||||
-- label = {key = "...", args = {...}} (oversættes) eller {text = "..."} (færdig tekst); valgfri prefix
|
||||
-- (færdig tekst foran, fx "Mælk: " foran den oversatte politik).
|
||||
function ADGuide.labelText(label, getText)
|
||||
if type(label) ~= "table" then return "" end
|
||||
if label.prefix ~= nil then
|
||||
local rest = {}
|
||||
for key, value in pairs(label) do if key ~= "prefix" then rest[key] = value end end
|
||||
return tostring(label.prefix) .. ADGuide.labelText(rest, getText)
|
||||
end
|
||||
if label.text ~= nil then return tostring(label.text) end
|
||||
local template = getText(label.key)
|
||||
if label.args == nil or #label.args == 0 then return tostring(template) end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local ok, text = pcall(string.format, template, unpackFn(label.args))
|
||||
return ok and text or tostring(template)
|
||||
end
|
||||
|
||||
-- Resumé-linje {step, title, parts} -> "Afgrøde: Behold det der gror, så igen".
|
||||
function ADGuide.lineText(line, getText)
|
||||
local parts = {}
|
||||
for _, part in ipairs(line.parts or {}) do
|
||||
local text = ADGuide.labelText(part, getText)
|
||||
if text ~= "" then table.insert(parts, text) end
|
||||
end
|
||||
return ADGuide.labelText(line.title, getText) .. ": " .. (#parts > 0 and table.concat(parts, ", ") or "-")
|
||||
end
|
||||
@@ -0,0 +1,582 @@
|
||||
-- AD Hitch
|
||||
-- Kobler redskaber på og af med AutoDrive.
|
||||
-- "Connect <navn>" (eller "Kobl <navn>"): markøren ligger lige under vognens trækøje med et bakkespor ned til
|
||||
-- den. Når en traktor har kørt AD "Kør til" dertil og står stille, kobles det redskab på, som spillet selv finder
|
||||
-- inden for koblingsafstand (samme kald som Courseplay bruger til skærebord: updateVehiclesInAttachRange +
|
||||
-- attachImplementFromInfo).
|
||||
-- "Afkobl <navn>" (eller "Kobl af", "Detach", "Disconnect"): ved ankomst kobles redskaberne BAG traktoren af;
|
||||
-- frontvægt og frontlæsser bliver siddende.
|
||||
-- ADHitch.fetch()/ADHitch.drop() er indgangen for flows/markarbejde: kør til markøren, kobl på/af, meld tilbage.
|
||||
|
||||
ADHitch = {}
|
||||
ADHitch.LOG_PREFIX = "ADSmartPickup: kobling: "
|
||||
-- afkobling tjekkes først, så "Kobl af X" ikke læses som "Kobl <af X>"
|
||||
ADHitch.MARKER_PREFIXES = {
|
||||
{prefix = "afkobl", action = "detach"},
|
||||
{prefix = "kobl af", action = "detach"},
|
||||
{prefix = "detach", action = "detach"},
|
||||
{prefix = "disconnect", action = "detach"},
|
||||
{prefix = "connect", action = "attach"},
|
||||
{prefix = "kobl", action = "attach"},
|
||||
}
|
||||
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
|
||||
ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet
|
||||
ADHitch.STILL_SPEED = 1 -- km/t
|
||||
ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop
|
||||
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
|
||||
ADHitch.DETACH_TIMEOUT_MS = 20000 -- afkobling: redskabet skal måske foldes ud først (plov: "unfold the tool")
|
||||
ADHitch.TICK_MS = 250
|
||||
ADHitch.ROUTE_WAIT_MS = 10000 -- pause ved almindelige markører midt i en tur
|
||||
ADHitch.ROOMY_ATTACH_DISTANCE = 3 -- m: koblingsrækkevidde når spillets egen ikke rækker
|
||||
ADHitch.CREPT_EXTRA_DISTANCE = 8 -- m: ekstra ankomstafstand når traktoren har bakket hen til vognen
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- "Connect Bredal" -> "attach", "Bredal"; "Afkobl" -> "detach", ""; andre navne -> nil
|
||||
function ADHitch.parseMarker(name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
local trimmed = name:match("^%s*(.-)%s*$")
|
||||
local lower = trimmed:lower()
|
||||
for _, entry in ipairs(ADHitch.MARKER_PREFIXES) do
|
||||
local prefix = entry.prefix
|
||||
if lower == prefix then return entry.action, "" end
|
||||
if lower:sub(1, #prefix + 1) == prefix .. " " then
|
||||
return entry.action, (trimmed:sub(#prefix + 2):match("^%s*(.-)$"))
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- implements = {{z = position i traktorens rum, ...}} -> kun dem bag traktoren
|
||||
function ADHitch.rearImplements(implements)
|
||||
local rear = {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if implement.z ~= nil and implement.z < 0 then table.insert(rear, implement) end
|
||||
end
|
||||
return rear
|
||||
end
|
||||
|
||||
-- konsol-argumenter "Connect Bredal > Start > Afkobl Bredal > Start" -> {"Connect Bredal", "Start", ...}
|
||||
function ADHitch.parseTrip(...)
|
||||
local steps = {}
|
||||
for part in (table.concat({...}, " ") .. ">"):gmatch("([^>]*)>") do
|
||||
local name = part:match("^%s*(.-)%s*$")
|
||||
if name ~= "" then table.insert(steps, name) end
|
||||
end
|
||||
return steps
|
||||
end
|
||||
|
||||
-- markørnavn -> attach / detach / arrive (almindelig markør: bare køre dertil)
|
||||
function ADHitch.stepAction(name)
|
||||
return ADHitch.parseMarker(name) or "arrive"
|
||||
end
|
||||
|
||||
-- Spillerens egen AD "Kør til": markørens navn (Connect/Afkobl) først, ellers er puljeredskabets egen parkering
|
||||
-- stedet vognen sættes af. -> "attach" | "detach" | nil
|
||||
function ADHitch.manualAction(parsedAction, isPoolImplementPark)
|
||||
if parsedAction ~= nil then return parsedAction end
|
||||
return isPoolImplementPark and "detach" or nil
|
||||
end
|
||||
|
||||
-- I en tur: en almindelig markør, der er AD-parkeringen for redskabet bag traktoren, er stedet vognen sættes af.
|
||||
function ADHitch.resolveAction(action, isImplementPark)
|
||||
if action == "arrive" and isImplementPark then return "detach" end
|
||||
return action
|
||||
end
|
||||
|
||||
-- pause efter et trin: ved almindelige markører undervejs holdes der, ikke efter sidste trin
|
||||
function ADHitch.waitAfter(action, index, count)
|
||||
if action == "arrive" and index < count then return ADHitch.ROUTE_WAIT_MS end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- markers = {{name, id}} -> markøren med navnet (store/små bogstaver ligegyldigt)
|
||||
function ADHitch.findMarker(markers, name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
local wanted = name:lower()
|
||||
for _, marker in pairs(markers or {}) do
|
||||
if type(marker.name) == "string" and marker.name:lower() == wanted then return marker end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
|
||||
local function arriveDistanceOf(state)
|
||||
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
||||
end
|
||||
|
||||
function ADHitch.hasArrived(state)
|
||||
return not state.adActive and state.distance <= arriveDistanceOf(state) and state.speed <= ADHitch.STILL_SPEED
|
||||
end
|
||||
|
||||
function ADHitch.newJob(markerId, markerName, action)
|
||||
return {markerId = markerId, markerName = markerName, action = action, state = "driving", elapsed = 0,
|
||||
actElapsed = 0, seenActive = false}
|
||||
end
|
||||
|
||||
-- Afkobling bekræftes: "done" først når intet sidder bag traktoren efter et forsøg. -> "attempt" | "done" | "nothing"
|
||||
function ADHitch.detachStep(rearCount, attempted)
|
||||
if rearCount > 0 then return "attempt" end
|
||||
return attempted and "done" or "nothing"
|
||||
end
|
||||
|
||||
-- Et skridt i jobbet. succeeded = lykkedes kobling/afkobling i denne tick (kun relevant i "acting").
|
||||
-- Tilstande: driving -> acting -> done | failed (reason: stoppedAway / noAttachable / nothingToDetach)
|
||||
function ADHitch.step(job, state, dt, succeeded)
|
||||
if job.state == "driving" then
|
||||
job.elapsed = job.elapsed + dt
|
||||
if state.adActive then
|
||||
job.seenActive = true
|
||||
elseif ADHitch.hasArrived(state) then
|
||||
job.state = "acting"
|
||||
elseif state.distance > arriveDistanceOf(state) and (job.seenActive or job.elapsed >= ADHitch.START_GRACE_MS) then
|
||||
job.state, job.reason = "failed", string.format("stoppedAway (%.0f m fra markøren)", state.distance)
|
||||
end
|
||||
elseif job.state == "acting" then
|
||||
if succeeded then
|
||||
job.state = "done"
|
||||
else
|
||||
job.actElapsed = job.actElapsed + dt
|
||||
local limit = job.action == "detach" and ADHitch.DETACH_TIMEOUT_MS or ADHitch.ACT_TIMEOUT_MS
|
||||
if job.actElapsed >= limit then
|
||||
job.state, job.reason = "failed", ADHitch.TIMEOUT_REASON[job.action]
|
||||
end
|
||||
end
|
||||
end
|
||||
return job
|
||||
end
|
||||
|
||||
-- Hjemtur: hvert puljeredskabs parkering (der sættes det af), så traktorens egen parkering.
|
||||
function ADHitch.homeRoute(implementParkIds, tractorParkId)
|
||||
local route, seen = {}, {}
|
||||
for _, id in ipairs(implementParkIds or {}) do
|
||||
if not seen[id] then seen[id] = true table.insert(route, id) end
|
||||
end
|
||||
if tractorParkId ~= nil then table.insert(route, tractorParkId) end
|
||||
return route
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local jobs = setmetatable({}, {__mode = "k"}) -- [vehicle] = job (+ onDone)
|
||||
local tickTimer = 0
|
||||
|
||||
local function vehicleName(vehicle)
|
||||
if type(vehicle) ~= "table" then return tostring(vehicle) end
|
||||
return vehicle.getFullName ~= nil and vehicle:getFullName() or tostring(vehicle.configFileName)
|
||||
end
|
||||
|
||||
local function stateModule(vehicle)
|
||||
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
end
|
||||
|
||||
local function vehicleState(adEnv, vehicle, markerId)
|
||||
local module = stateModule(vehicle)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(markerId)
|
||||
-- nærmeste led i sættet: bakker AD med vogn, er det vognen der ender på markøren, ikke traktoren
|
||||
-- (in-game 28/9: vognen stod på Bredal, traktoren et vognslæb væk -> "stoppedAway")
|
||||
local distance = math.huge
|
||||
local parts = {vehicle}
|
||||
if vehicle.getChildVehicles ~= nil then
|
||||
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
||||
if ok and children ~= nil then parts = children end
|
||||
end
|
||||
for _, part in pairs(parts) do
|
||||
if wayPoint ~= nil and part.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(part.rootNode)
|
||||
distance = math.min(distance, math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2))
|
||||
end
|
||||
end
|
||||
return {
|
||||
adActive = module ~= nil and module:isActive(),
|
||||
distance = distance,
|
||||
speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- Kobler det redskab på, som spillet finder inden for koblingsafstand. -> ok, redskab eller årsag
|
||||
function ADHitch.tryAttach(vehicle)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then
|
||||
return false, "noJoints"
|
||||
end
|
||||
-- spillets egen rækkevidde først, så en rummeligere (spillet retter selv vognen ind ved koblingen)
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ,
|
||||
AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
local info = spec.attachableInfo
|
||||
if (info == nil or info.attachable == nil) and ADHitch.ROOMY_ATTACH_DISTANCE ^ 2 > AttacherJoints.MAX_ATTACH_DISTANCE_SQ then
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitch.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
info = spec.attachableInfo
|
||||
end
|
||||
if info == nil or info.attachable == nil then return false, "noAttachable" end
|
||||
local attacher = info.attacherVehicle or vehicle
|
||||
local farmId = vehicle.getActiveFarm ~= nil and vehicle:getActiveFarm() or vehicle:getOwnerFarmId()
|
||||
local allowed, warning = info.attachable:isAttachAllowed(farmId, attacher)
|
||||
if not allowed then return false, tostring(warning or "notAllowed") end
|
||||
attacher:attachImplementFromInfo(info)
|
||||
return true, info.attachable
|
||||
end
|
||||
|
||||
-- Redskaber koblet direkte på traktoren, med deres placering langs traktoren (z > 0 foran, z < 0 bagved).
|
||||
local function attachedImplements(vehicle)
|
||||
local list = {}
|
||||
if vehicle.getAttachedImplements == nil then return list end
|
||||
for _, implement in ipairs(vehicle:getAttachedImplements()) do
|
||||
local jointDesc = vehicle.spec_attacherJoints.attacherJoints[implement.jointDescIndex]
|
||||
local node = jointDesc ~= nil and (jointDesc.jointTransform or jointDesc.rootNode) or nil
|
||||
if implement.object ~= nil and node ~= nil then
|
||||
local _, _, z = localToLocal(node, vehicle.rootNode, 0, 0, 0)
|
||||
table.insert(list, {object = implement.object, z = z, name = vehicleName(implement.object)})
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Er markøren (waypoint-id) AD-parkeringen for et af redskaberne bag traktoren?
|
||||
local function isImplementPark(adEnv, vehicle, markerId)
|
||||
for _, implement in ipairs(ADHitch.rearImplements(attachedImplements(vehicle))) do
|
||||
local advd = implement.object.advd
|
||||
local index = advd ~= nil and advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
if marker ~= nil and marker.id == markerId then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Efter påkobling: fold foldbare redskaber sammen til vejen (CP folder selv ud på marken).
|
||||
-- Klar til vej: s = {turnedOn, lowered, unfolded, canFold} -> liste af "turnOff" | "raise" | "fold"
|
||||
function ADHitch.stowActions(s)
|
||||
local actions = {}
|
||||
if s.turnedOn then table.insert(actions, "turnOff") end
|
||||
if s.lowered then table.insert(actions, "raise") end
|
||||
if s.unfolded and s.canFold then table.insert(actions, "fold") end
|
||||
return actions
|
||||
end
|
||||
|
||||
-- Alle redskaber på maskinen (også forrest og bag et andet redskab): {object, attacher, jointDescIndex}
|
||||
local function allImplements(vehicle, list)
|
||||
list = list or {}
|
||||
if vehicle == nil or vehicle.getAttachedImplements == nil then return list end
|
||||
for _, implement in ipairs(vehicle:getAttachedImplements()) do
|
||||
if implement.object ~= nil then
|
||||
table.insert(list, {object = implement.object, attacher = vehicle, jointDescIndex = implement.jointDescIndex})
|
||||
allImplements(implement.object, list)
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Stop af mark/flow: sluk, løft og fold alle redskaber til transport. -> antal redskaber der fik noget gjort
|
||||
function ADHitch.stowForTransport(vehicle)
|
||||
local count = 0
|
||||
for _, item in ipairs(allImplements(vehicle)) do
|
||||
local object = item.object
|
||||
local foldDirection = object.spec_foldable ~= nil and -(object.spec_foldable.turnOnFoldDirection or 1) or nil
|
||||
local actions = ADHitch.stowActions({
|
||||
turnedOn = object.getIsTurnedOn ~= nil and object:getIsTurnedOn(),
|
||||
lowered = object.getIsLowered ~= nil and object:getIsLowered(true),
|
||||
unfolded = object.getIsUnfolded ~= nil and object:getIsUnfolded() and foldDirection ~= nil,
|
||||
canFold = foldDirection ~= nil and object.setFoldDirection ~= nil
|
||||
and (object.getIsFoldAllowed == nil or object:getIsFoldAllowed(foldDirection, false)),
|
||||
})
|
||||
for _, action in ipairs(actions) do
|
||||
if action == "turnOff" and object.setIsTurnedOn ~= nil then
|
||||
pcall(object.setIsTurnedOn, object, false)
|
||||
elseif action == "raise" and item.attacher.setJointMoveDown ~= nil and item.jointDescIndex ~= nil then
|
||||
pcall(item.attacher.setJointMoveDown, item.attacher, item.jointDescIndex, false)
|
||||
elseif action == "fold" then
|
||||
pcall(object.setFoldDirection, object, foldDirection)
|
||||
end
|
||||
end
|
||||
if #actions > 0 then count = count + 1 end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Som stowForTransport, men logget og pcall'et (til stop-vejene).
|
||||
function ADHitch.stow(vehicle, why)
|
||||
local ok, count = pcall(ADHitch.stowForTransport, vehicle)
|
||||
if not ok then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s': kunne ikke gøre redskaber klar til vej: %s", vehicleName(vehicle), tostring(count))
|
||||
elseif count > 0 then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s': %d redskab(er) slukket, løftet og foldet (%s)", vehicleName(vehicle), count, tostring(why))
|
||||
end
|
||||
end
|
||||
|
||||
function ADHitch.foldForTransport(vehicle)
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(vehicle)) do
|
||||
local object = implement.object
|
||||
local unfolded = object.getIsUnfolded ~= nil and object:getIsUnfolded()
|
||||
if unfolded and object.spec_foldable ~= nil and object.setFoldDirection ~= nil then
|
||||
object:setFoldDirection(-(object.spec_foldable.turnOnFoldDirection or 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADHitch.rearImplementsOf(vehicle)
|
||||
return ADHitch.rearImplements(attachedImplements(vehicle))
|
||||
end
|
||||
|
||||
local function parkIdOf(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
-- Kobler redskaberne bag traktoren af. -> ok, navne eller årsag. Med job bekræftes afkoblingen ved næste tick
|
||||
-- (ok først når intet sidder bagpå); uden job (konsol) meldes ok straks efter kaldet.
|
||||
function ADHitch.tryDetach(vehicle, job)
|
||||
local rear = ADHitch.rearImplements(attachedImplements(vehicle))
|
||||
local step = ADHitch.detachStep(#rear, job ~= nil and job.detachNames ~= nil)
|
||||
if step == "done" then return true, job.detachNames end
|
||||
if step == "nothing" then return false, "nothingToDetach" end
|
||||
local names = {}
|
||||
for _, implement in ipairs(rear) do
|
||||
local object = implement.object
|
||||
local allowed, warning = true, nil
|
||||
if object.isDetachAllowed ~= nil then allowed, warning = object:isDetachAllowed() end
|
||||
if not allowed and object.spec_foldable ~= nil and object.setFoldDirection ~= nil then
|
||||
-- plove o.l. må kun kobles af udfoldet (in-game 28/9: "You first need to unfold the tool") — fold ud
|
||||
-- og prøv igen ved næste tick, mens foldningen kører
|
||||
object:setFoldDirection(object.spec_foldable.turnOnFoldDirection or 1)
|
||||
return false, "unfolding: " .. tostring(warning)
|
||||
end
|
||||
if not allowed then return false, tostring(warning or "detachNotAllowed") end
|
||||
vehicle:detachImplementByObject(object)
|
||||
table.insert(names, implement.name)
|
||||
end
|
||||
if job == nil then return true, table.concat(names, ", ") end
|
||||
if job.detachNames ~= nil then
|
||||
job.detachRetries = (job.detachRetries or 0) + 1
|
||||
if job.detachRetries == 1 then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s': '%s' sidder der stadig efter afkobling — prøver igen", vehicleName(vehicle), table.concat(names, ", "))
|
||||
end
|
||||
end
|
||||
job.detachNames = job.detachNames or table.concat(names, ", ")
|
||||
return false, "stillAttached: " .. table.concat(names, ", ")
|
||||
end
|
||||
|
||||
local function start(adEnv, vehicle, markerId, action, onDone)
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerId)
|
||||
local job = ADHitch.newJob(markerId, marker ~= nil and marker.name or tostring(markerId), action)
|
||||
job.onDone = onDone
|
||||
jobs[vehicle] = job
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, markerId)
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' kører til '%s'", vehicleName(vehicle), job.markerName)
|
||||
return job
|
||||
end
|
||||
|
||||
-- Kørende kobl-på-job for traktoren (til bakningen de sidste meter), ellers nil.
|
||||
function ADHitch.pendingAttach(vehicle)
|
||||
local job = jobs[vehicle]
|
||||
if job ~= nil and job.action == "attach" and job.state == "driving" then return job end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Afbryd et hente-/koble-job (Stop alt, høsten slut, spilleren overtager): intet kobles på bagefter, og
|
||||
-- onDone kaldes ikke. -> true hvis der var et job.
|
||||
function ADHitch.cancel(vehicle)
|
||||
local had = jobs[vehicle] ~= nil
|
||||
jobs[vehicle] = nil
|
||||
if ADPool ~= nil and ADPool.setReturning ~= nil then ADPool.setReturning(vehicle, false) end
|
||||
if had and vehicle.stopAutoDrive ~= nil then pcall(vehicle.stopAutoDrive, vehicle) end
|
||||
return had
|
||||
end
|
||||
|
||||
-- Kør til en Connect-markør og kobl på. onDone(vehicle, ok, redskab eller årsag) kaldes én gang.
|
||||
function ADHitch.fetch(adEnv, vehicle, markerId, onDone)
|
||||
return start(adEnv, vehicle, markerId, "attach", onDone)
|
||||
end
|
||||
|
||||
-- Kør til en Afkobl-markør og kobl af. onDone(vehicle, ok, navne eller årsag) kaldes én gang.
|
||||
function ADHitch.drop(adEnv, vehicle, markerId, onDone)
|
||||
return start(adEnv, vehicle, markerId, "detach", onDone)
|
||||
end
|
||||
|
||||
-- Tur: markørerne køres i rækkefølge. Connect = kobl på, Afkobl eller vognens egen AD-parkering = kobl af,
|
||||
-- andre = kør dertil (og hold
|
||||
-- ROUTE_WAIT_MS, hvis det ikke er sidste trin). Stopper ved første fejl. onDone(vehicle, ok, årsag) til sidst.
|
||||
local waits = {} -- {vehicle, leftMs, continue}
|
||||
|
||||
function ADHitch.route(adEnv, vehicle, markerIds, onDone)
|
||||
local function runStep(index)
|
||||
if index > #markerIds then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' er færdig med turen (%d trin)", vehicleName(vehicle), #markerIds)
|
||||
if onDone ~= nil then pcall(onDone, vehicle, true) end
|
||||
return
|
||||
end
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerIds[index])
|
||||
local action = ADHitch.resolveAction(ADHitch.stepAction(marker ~= nil and marker.name or nil),
|
||||
isImplementPark(adEnv, vehicle, markerIds[index]))
|
||||
start(adEnv, vehicle, markerIds[index], action, function(_, ok, detail)
|
||||
if not ok then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s' stopper turen ved trin %d", vehicleName(vehicle), index)
|
||||
if onDone ~= nil then pcall(onDone, vehicle, false, detail) end
|
||||
return
|
||||
end
|
||||
local waitMs = ADHitch.waitAfter(action, index, #markerIds)
|
||||
if waitMs > 0 then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' holder %d s ved '%s'", vehicleName(vehicle), waitMs / 1000,
|
||||
marker ~= nil and marker.name or "?")
|
||||
table.insert(waits, {vehicle = vehicle, leftMs = waitMs, continue = function() runStep(index + 1) end})
|
||||
else
|
||||
runStep(index + 1)
|
||||
end
|
||||
end)
|
||||
end
|
||||
runStep(1)
|
||||
end
|
||||
|
||||
-- Puljeredskaber bag traktoren sættes af på deres parkering, og traktoren kører hjem til sin egen.
|
||||
-- Kun redskaber med en Connect-markør til deres parkering er puljeredskaber; andre bliver siddende.
|
||||
-- Parkeringer (waypoint-id) for puljeredskaberne bag traktoren (tom = ingen puljeredskaber bag).
|
||||
function ADHitch.poolParkIds(adEnv, tractor)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local parkIds = {}
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do
|
||||
local parkId = parkIdOf(adEnv, implement.object)
|
||||
local park = parkId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(parkId) or nil
|
||||
if park ~= nil and ADPool.connectFor(park.name, markers) ~= nil then table.insert(parkIds, parkId) end
|
||||
end
|
||||
return parkIds
|
||||
end
|
||||
|
||||
function ADHitch.returnHome(adEnv, tractor, onDone)
|
||||
local parkIds = ADHitch.poolParkIds(adEnv, tractor)
|
||||
if #parkIds == 0 then return false end
|
||||
local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor))
|
||||
ADPool.setReturning(tractor, true)
|
||||
ADHitch.route(adEnv, tractor, route, function(vehicle, ok, detail)
|
||||
ADPool.setReturning(vehicle, false)
|
||||
if onDone ~= nil then onDone(vehicle, ok, detail) end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
local function tickWaits(elapsed)
|
||||
for i = #waits, 1, -1 do
|
||||
local wait = waits[i]
|
||||
wait.leftMs = wait.leftMs - elapsed
|
||||
if wait.leftMs <= 0 then
|
||||
table.remove(waits, i)
|
||||
wait.continue()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Spilleren sender selv en traktor til en Connect-/Afkobl-markør med AD "Kør til": den kobles også.
|
||||
local function watchManualTrips(adEnv)
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local module = stateModule(vehicle)
|
||||
if jobs[vehicle] == nil and vehicle.spec_attacherJoints ~= nil and module ~= nil and module:isActive()
|
||||
and module:getMode() == adEnv.AutoDrive.MODE_DRIVETO then
|
||||
local marker = module:getFirstMarker()
|
||||
local action = nil
|
||||
if marker ~= nil then
|
||||
local isPoolPark = false
|
||||
for _, parkId in ipairs(ADHitch.poolParkIds(adEnv, vehicle)) do
|
||||
if parkId == marker.id then isPoolPark = true end
|
||||
end
|
||||
action = ADHitch.manualAction(ADHitch.parseMarker(marker.name), isPoolPark)
|
||||
end
|
||||
if action ~= nil then jobs[vehicle] = ADHitch.newJob(marker.id, marker.name, action) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local DONE_TEXT = {attach = "koblede '%s' på", detach = "koblede '%s' af", arrive = "er fremme%s"}
|
||||
local FAIL_TEXT = {attach = "kunne ikke koble på", detach = "kunne ikke koble af", arrive = "nåede ikke frem"}
|
||||
|
||||
local function finish(vehicle, job, ok, detail, adEnv)
|
||||
jobs[vehicle] = nil
|
||||
if ok then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' " .. DONE_TEXT[job.action] .. " ved '%s'", vehicleName(vehicle),
|
||||
vehicleName(detail), job.markerName)
|
||||
else
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s' %s ved '%s': %s", vehicleName(vehicle), FAIL_TEXT[job.action],
|
||||
job.markerName, tostring(detail))
|
||||
end
|
||||
if job.onDone ~= nil then
|
||||
pcall(job.onDone, vehicle, ok, detail)
|
||||
elseif ok and job.action == "attach" and ADHitch.onManualAttach ~= nil then
|
||||
-- spillerens egen tur til Connect: lad fx flowet starte traktoren bagefter
|
||||
local okHook, err = pcall(ADHitch.onManualAttach, adEnv, vehicle, detail)
|
||||
if not okHook then Logging.warning(ADHitch.LOG_PREFIX .. "efter manuel kobling: %s", tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
local ACTIONS = {attach = ADHitch.tryAttach, detach = ADHitch.tryDetach, arrive = function() return true, "" end}
|
||||
|
||||
function ADHitch.tick(adEnv, dt)
|
||||
if adEnv == nil or adEnv.ADGraphManager == nil then return end
|
||||
tickTimer = tickTimer + dt
|
||||
if tickTimer < ADHitch.TICK_MS then return end
|
||||
local elapsed = tickTimer
|
||||
tickTimer = 0
|
||||
tickWaits(elapsed)
|
||||
watchManualTrips(adEnv)
|
||||
-- kopi: et færdigt trin starter næste trin (nyt job på samme maskine) midt i løkken
|
||||
local current = {}
|
||||
for vehicle, job in pairs(jobs) do current[vehicle] = job end
|
||||
for vehicle, job in pairs(current) do
|
||||
local succeeded, detail = false, nil
|
||||
if job.state == "acting" then
|
||||
succeeded, detail = ACTIONS[job.action](vehicle, job)
|
||||
if not succeeded then job.lastReason = detail end
|
||||
end
|
||||
local state = vehicleState(adEnv, vehicle, job.markerId)
|
||||
if job.crept then state.arriveDistance = ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
|
||||
ADHitch.step(job, state, elapsed, succeeded)
|
||||
if job.state == "done" then
|
||||
finish(vehicle, job, true, detail, adEnv)
|
||||
elseif job.state == "failed" then
|
||||
finish(vehicle, job, false, job.lastReason or job.reason, adEnv)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Konsol: "spuConnect" / "spuDetach" på den maskine spilleren sidder i.
|
||||
local function currentVehicle()
|
||||
return g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil and g_localPlayer:getCurrentVehicle() or nil
|
||||
end
|
||||
|
||||
function ADHitch:consoleConnect()
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuConnect: sæt dig i en traktor først" end
|
||||
local ok, detail = ADHitch.tryAttach(vehicle)
|
||||
if ok then return "spuConnect: koblede " .. vehicleName(detail) .. " på" end
|
||||
return "spuConnect: intet koblet på (" .. tostring(detail) .. ")"
|
||||
end
|
||||
|
||||
function ADHitch:consoleDetach()
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuDetach: sæt dig i en traktor først" end
|
||||
local ok, detail = ADHitch.tryDetach(vehicle)
|
||||
if ok then return "spuDetach: koblede " .. tostring(detail) .. " af" end
|
||||
return "spuDetach: intet koblet af (" .. tostring(detail) .. ")"
|
||||
end
|
||||
|
||||
-- "spuTrip Connect Bredal > Start > Bredal > Start": maskinen spilleren sidder i kører turen
|
||||
-- (spilleren kan blive siddende; AD kører også med spilleren i førerhuset).
|
||||
function ADHitch:consoleTrip(...)
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuTrip: sæt dig i en traktor først" end
|
||||
local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil
|
||||
if adEnv == nil then return "spuTrip: AutoDrive er ikke klar endnu" end
|
||||
local names = ADHitch.parseTrip(...)
|
||||
if #names == 0 then return "spuTrip: brug fx spuTrip Connect Bredal > Start > Bredal > Start" end
|
||||
local markers, ids = adEnv.ADGraphManager:getMapMarkers(), {}
|
||||
for _, name in ipairs(names) do
|
||||
local marker = ADHitch.findMarker(markers, name)
|
||||
if marker == nil then return "spuTrip: ukendt markør '" .. name .. "'" end
|
||||
table.insert(ids, marker.id)
|
||||
end
|
||||
ADHitch.route(adEnv, vehicle, ids)
|
||||
return "spuTrip: " .. vehicleName(vehicle) .. " kører " .. table.concat(names, " > ")
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuConnect", "Kobler nærmeste redskab på den maskine du sidder i", "consoleConnect", ADHitch)
|
||||
addConsoleCommand("spuTrip", "Kør en tur: spuTrip Connect Bredal > Start > Bredal > Start", "consoleTrip", ADHitch)
|
||||
addConsoleCommand("spuDetach", "Kobler redskaberne bag den maskine du sidder i af", "consoleDetach", ADHitch)
|
||||
end
|
||||
@@ -0,0 +1,193 @@
|
||||
-- AD Hitch Creep
|
||||
-- Bak de sidste meter hen til vognen: når AD "Kør til" når en Connect-markør, og vognens trækøje står 3-8 m
|
||||
-- lige bag traktorens krog, lægges en lille bakke-opgave i AD's kø før stop. Traktoren bakker i krybefart og
|
||||
-- standser, så snart spillet ser vognen inden for koblingsrækkevidde; ADHitch kobler derefter på som før.
|
||||
-- AD's egen bak-kollisionssensor bruges ikke: den ser vognen selv som forhindring.
|
||||
|
||||
ADHitchCreep = {}
|
||||
ADHitchCreep.LOG_PREFIX = "ADSmartPickup: kobling: "
|
||||
ADHitchCreep.ROOMY_ATTACH_DISTANCE = 3 -- m: herinden for kobler ADHitch direkte (samme som ADHitch.ROOMY_ATTACH_DISTANCE)
|
||||
ADHitchCreep.CREEP_MAX_DISTANCE = 8 -- m: længere væk end det bakkes ikke — markøren ligger forkert
|
||||
ADHitchCreep.CREEP_MAX_LATERAL = 1.5 -- m til siden: mere end det rammer en lige bakning ikke trækøjet
|
||||
ADHitchCreep.CREEP_TIMEOUT_MS = 25000
|
||||
ADHitchCreep.CREEP_SPEED = 2 -- km/t
|
||||
ADHitchCreep.AWAY_TOLERANCE = 1 -- m: længere væk end ved start = den bakker forkert
|
||||
ADHitchCreep.RANGE_CHECK_MS = 250
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- offset = {x, z}: vognens trækøje i traktorens rum set fra krogen (z < 0 = bagved) -> "attach" | "creep" | "none"
|
||||
function ADHitchCreep.plan(offset)
|
||||
if offset == nil then return "none" end
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if distance <= ADHitchCreep.ROOMY_ATTACH_DISTANCE then return "attach" end
|
||||
if offset.z >= 0 or distance > ADHitchCreep.CREEP_MAX_DISTANCE or math.abs(offset.x) > ADHitchCreep.CREEP_MAX_LATERAL then
|
||||
return "none"
|
||||
end
|
||||
return "creep"
|
||||
end
|
||||
|
||||
-- s = {inRange, distance, startDistance, elapsedMs, passed} -> "done" | "drive" | "giveUp"
|
||||
function ADHitchCreep.step(s)
|
||||
if s.inRange then return "done" end
|
||||
if (s.elapsedMs or 0) >= ADHitchCreep.CREEP_TIMEOUT_MS then return "giveUp" end
|
||||
if s.passed then return "giveUp" end
|
||||
if s.distance > s.startDistance + ADHitchCreep.AWAY_TOLERANCE then return "giveUp" end
|
||||
return "drive"
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function worldPos(node)
|
||||
local x, y, z = getWorldTranslation(node)
|
||||
return {x = x, y = y, z = z}
|
||||
end
|
||||
|
||||
local function vehicleName(vehicle)
|
||||
return vehicle ~= nil and vehicle.getName ~= nil and vehicle:getName() or "?"
|
||||
end
|
||||
|
||||
-- Traktorens ledige bagerste krog-noder.
|
||||
local function freeRearJoints(vehicle)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or spec.attacherJoints == nil or vehicle.rootNode == nil then return {} end
|
||||
local used = {}
|
||||
for _, implement in ipairs(spec.attachedImplements or {}) do
|
||||
if implement.jointDescIndex ~= nil then used[implement.jointDescIndex] = true end
|
||||
end
|
||||
local joints = {}
|
||||
for index, joint in ipairs(spec.attacherJoints) do
|
||||
if not used[index] and joint.jointTransform ~= nil then
|
||||
local p = worldPos(joint.jointTransform)
|
||||
local _, _, lz = worldToLocal(vehicle.rootNode, p.x, p.y, p.z)
|
||||
if lz < 0 then table.insert(joints, joint.jointTransform) end
|
||||
end
|
||||
end
|
||||
return joints
|
||||
end
|
||||
|
||||
-- Vognens trækøje i traktorens rum set fra krogen.
|
||||
local function offsetOf(vehicle, jointNode, targetNode)
|
||||
local j, t = worldPos(jointNode), worldPos(targetNode)
|
||||
local x, _, z = worldDirectionToLocal(vehicle.rootNode, t.x - j.x, 0, t.z - j.z)
|
||||
return {x = x, z = z}
|
||||
end
|
||||
|
||||
-- Nærmeste frakoblede redskab (samme gård) med et trækøje bag en ledig krog. -> {wagon, joint, target, offset, plan} | nil
|
||||
function ADHitchCreep.findTarget(vehicle)
|
||||
local joints = freeRearJoints(vehicle)
|
||||
if #joints == 0 or g_currentMission == nil or g_currentMission.vehicleSystem == nil then return nil end
|
||||
local farmId = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() or nil
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, object in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
|
||||
local attachable = object.spec_attachable
|
||||
if object ~= vehicle and attachable ~= nil and object.spec_motorized == nil and attachable.inputAttacherJoints ~= nil
|
||||
and (object.getAttacherVehicle == nil or object:getAttacherVehicle() == nil)
|
||||
and (farmId == nil or object.getOwnerFarmId == nil or object:getOwnerFarmId() == farmId) then
|
||||
for _, input in ipairs(attachable.inputAttacherJoints) do
|
||||
if input.node ~= nil then
|
||||
for _, joint in ipairs(joints) do
|
||||
local offset = offsetOf(vehicle, joint, input.node)
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if distance < bestDistance and distance <= ADHitchCreep.CREEP_MAX_DISTANCE + 2 then
|
||||
bestDistance = distance
|
||||
best = {wagon = object, joint = joint, target = input.node, offset = offset}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if best ~= nil then best.plan = ADHitchCreep.plan(best.offset) end
|
||||
return best
|
||||
end
|
||||
|
||||
local function isInRange(vehicle, wagon)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then return false end
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitchCreep.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
local info = spec.attachableInfo
|
||||
return info ~= nil and info.attachable == wagon
|
||||
end
|
||||
|
||||
-- AD-opgave (samme metoder som AD's AbstractTask).
|
||||
local CreepTask = {}
|
||||
CreepTask.__index = CreepTask
|
||||
|
||||
function CreepTask.new(adEnv, vehicle, target)
|
||||
return setmetatable({adEnv = adEnv, vehicle = vehicle, target = target, elapsedMs = 0, checkMs = 0, inRange = false}, CreepTask)
|
||||
end
|
||||
|
||||
function CreepTask:setUp()
|
||||
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
|
||||
self.startDistance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' bakker %.1f m hen til '%s' (%.1f m til siden)", vehicleName(self.vehicle),
|
||||
self.startDistance, vehicleName(self.target.wagon), offset.x)
|
||||
end
|
||||
|
||||
function CreepTask:finish(action, distance)
|
||||
local sdm = self.vehicle.ad.specialDrivingModule
|
||||
if sdm ~= nil and sdm.stopVehicle ~= nil then sdm:stopVehicle() end
|
||||
if action == "done" then
|
||||
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' står ved '%s' — kobler på", vehicleName(self.vehicle), vehicleName(self.target.wagon))
|
||||
else
|
||||
Logging.warning(ADHitchCreep.LOG_PREFIX .. "'%s' opgav at bakke hen til '%s' (%.1f m tilbage efter %d s)", vehicleName(self.vehicle),
|
||||
vehicleName(self.target.wagon), distance, math.floor(self.elapsedMs / 1000))
|
||||
end
|
||||
local taskModule = self.adEnv.ADTaskModule
|
||||
self.vehicle.ad.taskModule:setCurrentTaskFinished(taskModule ~= nil and taskModule.DONT_PROPAGATE or 1)
|
||||
end
|
||||
|
||||
function CreepTask:update(dt)
|
||||
self.elapsedMs = self.elapsedMs + dt
|
||||
self.checkMs = self.checkMs + dt
|
||||
if self.target.wagon.getAttacherVehicle ~= nil and self.target.wagon:getAttacherVehicle() ~= nil then
|
||||
return self:finish("giveUp", 0) -- en anden har taget vognen
|
||||
end
|
||||
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if self.checkMs >= ADHitchCreep.RANGE_CHECK_MS then
|
||||
self.checkMs = 0
|
||||
self.inRange = isInRange(self.vehicle, self.target.wagon)
|
||||
end
|
||||
local action = ADHitchCreep.step({inRange = self.inRange, distance = distance, startDistance = self.startDistance,
|
||||
elapsedMs = self.elapsedMs, passed = offset.z > 0.5})
|
||||
if action ~= "drive" then return self:finish(action, distance) end
|
||||
|
||||
local autoDrive, vehicle = self.adEnv.AutoDrive, self.vehicle
|
||||
local p = worldPos(self.target.target)
|
||||
local lx, lz = autoDrive.getDriveDirection(vehicle, p.x, p.y, p.z)
|
||||
local sdm = vehicle.ad.specialDrivingModule
|
||||
if sdm ~= nil and sdm.releaseVehicle ~= nil then sdm:releaseVehicle() end
|
||||
if vehicle.startMotor ~= nil and not vehicle:getIsMotorStarted() and vehicle:getCanMotorRun() then vehicle:startMotor() end
|
||||
-- som AD's ADSpecialDrivingModule:driveReverse (ikke-guidet): retning spejles ved bakning
|
||||
autoDrive.driveInDirection(vehicle, dt, 30, 0.3, 0.2, 20, true, false, -lx, -lz, ADHitchCreep.CREEP_SPEED, 1)
|
||||
end
|
||||
|
||||
function CreepTask:abort() end
|
||||
function CreepTask:finished() end
|
||||
function CreepTask:getInfoText() return "Bakker hen til vognen" end
|
||||
function CreepTask:getI18nInfo() return "" end
|
||||
function CreepTask:getExcludedVehiclesForCollisionCheck() return {self.target.wagon} end
|
||||
|
||||
local isInstalled = false
|
||||
|
||||
-- DriveToMode:handleFinishedTask lægger StopAndDisableADTask i køen; bakningen lægges ind lige før.
|
||||
function ADHitchCreep.install(adEnv)
|
||||
local mode = adEnv ~= nil and adEnv.DriveToMode or nil
|
||||
if isInstalled or mode == nil or mode.handleFinishedTask == nil or ADHitch == nil or ADHitch.pendingAttach == nil then return end
|
||||
isInstalled = true
|
||||
local original = mode.handleFinishedTask
|
||||
mode.handleFinishedTask = function(self)
|
||||
local ok, err = pcall(function()
|
||||
local job = self.driveToDestinationTask ~= nil and ADHitch.pendingAttach(self.vehicle) or nil
|
||||
if job == nil or job.markerId ~= self.destinationID then return end
|
||||
local target = ADHitchCreep.findTarget(self.vehicle)
|
||||
if target == nil or target.plan ~= "creep" then return end
|
||||
job.crept = true
|
||||
self.vehicle.ad.taskModule:addTask(CreepTask.new(adEnv, self.vehicle, target))
|
||||
end)
|
||||
if not ok then Logging.warning(ADHitchCreep.LOG_PREFIX .. "bakning til vognen: %s", tostring(err)) end
|
||||
return original(self)
|
||||
end
|
||||
end
|
||||
@@ -97,6 +97,19 @@ function ADLoadSwap.rankDumpCandidates(candidates, avoidMarkers)
|
||||
return ranked
|
||||
end
|
||||
|
||||
-- Flydende varer lægges kun i tanke/siloer, aldrig automatisk i en fabriks aflæsning: fabrikkerne har ofte en
|
||||
-- rist med præcis aflæsning, som en gyllevogns udløb bag på ikke rammer (in-game 26/9: 1 mio. l gylle holdt
|
||||
-- ved AIO'ens rist og spærrede vejen). Vælger brugeren selv fabrikken som lager, gælder valget.
|
||||
ADLoadSwap.LIQUIDS = {LIQUIDMANURE = true, DIGESTATE = true, MILK = true, BUFFALOMILK = true, GOATMILK = true,
|
||||
SHEEPMILK = true, WATER = true, LIQUIDFERTILIZER = true, HERBICIDE = true, METHANE = true, DIESEL = true}
|
||||
function ADLoadSwap.isLiquidName(name)
|
||||
return name ~= nil and ADLoadSwap.LIQUIDS[string.upper(tostring(name))] == true
|
||||
end
|
||||
function ADLoadSwap.isLiquid(fillType)
|
||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeNameByIndex ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or nil
|
||||
return ADLoadSwap.isLiquidName(name)
|
||||
end
|
||||
|
||||
function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables, avoidMarkers, allowFactories)
|
||||
if vehicle.rootNode == nil then
|
||||
return nil, "køretøjet har ingen position"
|
||||
@@ -166,7 +179,8 @@ end
|
||||
-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne.
|
||||
-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
|
||||
function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
|
||||
wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0)
|
||||
-- tilfældig start (0-2 s): ventende vogne tjekker ikke alle i samme frame
|
||||
wait.swapTimerMs = (wait.swapTimerMs or math.random(0, ADLoadSwap.CHECK_INTERVAL_MS)) + (dt or 0)
|
||||
if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-- AD Manual Transport
|
||||
-- Brede høstere (mejetærsker med skærebord) kan ikke køre på vejene: spilleren kører dem til marken, monterer
|
||||
-- skærebordet og stiger ud; moden overtager derfra. Rene regler (testbare uden spillet); spil-delen ligger i
|
||||
-- adFieldWork.
|
||||
|
||||
ADManualTransport = {}
|
||||
|
||||
ADManualTransport.REMIND_MS = 300000 -- påmindelse hvert 5. minut, mens moden venter på spilleren
|
||||
|
||||
-- Må høsteren vælges til marken? Det monterede skærebord/optager skal kunne tage afgrøden. Undtagelse: en manuel
|
||||
-- høster UDEN skærebord monteret (det monteres først på marken — tjekkes ved overtagelse). En roeoptager har sin
|
||||
-- optager fast på og kan aldrig tage hvede (in-game 27/9: F111 valgte roeoptageren).
|
||||
function ADManualTransport.acceptsHarvester(candidate, fruit)
|
||||
if fruit == nil then return false end
|
||||
local fruits = candidate.fruits or {}
|
||||
if candidate.manual and next(fruits) == nil then return true end
|
||||
return fruits[fruit] == true
|
||||
end
|
||||
|
||||
-- Klar til overtagelse? s = {atField, fruits = {[fruit] = true}, fruit, controlled, forced}.
|
||||
-- Returnerer true, eller false + grund: "notThere" | "noHeader" | "wrongHeader" | "playerInside".
|
||||
-- forced ("Start nu"): sted og spiller springes over, skærebordet tjekkes stadig.
|
||||
function ADManualTransport.readiness(s)
|
||||
if not s.forced and not s.atField then return false, "notThere" end
|
||||
local fruits = s.fruits or {}
|
||||
if next(fruits) == nil then return false, "noHeader" end
|
||||
if s.fruit == nil or not fruits[s.fruit] then return false, "wrongHeader" end
|
||||
if not s.forced and s.controlled then return false, "playerInside" end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADManualTransport.shouldRemind(lastAt, now)
|
||||
return lastAt == nil or now - lastAt >= ADManualTransport.REMIND_MS
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
-- AD Month View
|
||||
-- Rene regler for fanen Måned: hvilke marker har arbejde i gang, hvor langt er de, hvad kommer bagefter,
|
||||
-- og lukker vinduet (høst/såning) ved månedsskiftet? GUI'et oversætter nøglerne; spillet kaldes kun via
|
||||
-- ctx-funktionerne (testbart uden FS).
|
||||
|
||||
ADMonthView = {}
|
||||
|
||||
ADMonthView.PERIODS = 12
|
||||
-- Trin der kræver afgrøden eller såvinduet: sorteres før pleje/klargøring
|
||||
ADMonthView.MAIN_STAGES = {harvesting = true, mowing = true, sowing = true}
|
||||
-- Trin før såningen i klargøringskæden (lukker såvinduet, bliver marken ikke sået)
|
||||
ADMonthView.BEFORE_SOWING = {mowing = true, tedding = true, raking = true, collecting = true, harvesting = true,
|
||||
strawCollecting = true, baling = true, baleCollecting = true, mulching = true,
|
||||
liming = true, plowing = true, cultivating = true, sowing = true}
|
||||
|
||||
function ADMonthView.nextPeriod(period)
|
||||
if type(period) ~= "number" or period < 1 then return 1 end
|
||||
return period % ADMonthView.PERIODS + 1
|
||||
end
|
||||
|
||||
-- Visse FS25-sæsondata gemmer overgangene som lister af {startState, endState}; strukturen er ikke
|
||||
-- dokumenteret, så alle tabeller i periodens data gennemsøges. Ukendt = nej (ingen falsk alarm).
|
||||
local function findsTransition(data, fromState, toState, depth)
|
||||
if type(data) ~= "table" or depth > 3 then return false end
|
||||
if data.startState == fromState and data.endState == toState then return true end
|
||||
for _, value in pairs(data) do
|
||||
if type(value) == "table" and findsTransition(value, fromState, toState, depth + 1) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Visner afgrøden (stadie state) ved overgangen til periode nextPeriod?
|
||||
function ADMonthView.withersIn(fruit, nextPeriod, state)
|
||||
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
|
||||
local withered = fruit.witheredState
|
||||
if type(withered) ~= "number" or withered <= 0 then return false end
|
||||
local periods = fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
|
||||
local data = periods ~= nil and periods[nextPeriod] or nil
|
||||
return findsTransition(data, state, withered, 0)
|
||||
end
|
||||
|
||||
-- Såvindue for en afgrøde: "closes" (kan sås nu, ikke næste måned), "open" (også næste måned), "closed".
|
||||
function ADMonthView.sowWindow(plantableIn, fruitName, period)
|
||||
if fruitName == nil or fruitName == "" or plantableIn == nil then return "open" end
|
||||
local now = plantableIn(fruitName, period)
|
||||
if not now then return "closed" end
|
||||
return plantableIn(fruitName, ADMonthView.nextPeriod(period)) and "open" or "closes"
|
||||
end
|
||||
|
||||
-- Er marken i gang (vises i tabellen)? Venter/færdig og passiv "klar" (manuel uden Start nu) er ikke.
|
||||
function ADMonthView.isActive(field)
|
||||
if field == nil then return false end
|
||||
if field.stage == "idle" or field.stage == "done" then return false end
|
||||
if field.stage == "ready" then return ADFieldModel.shouldStart(field) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Næste trin i kæden (uden at ændre marken). nil = intet (pleje går tilbage til venter, sidste trin -> færdig).
|
||||
function ADMonthView.nextStage(field, info)
|
||||
if field.stage == "ready" then
|
||||
local stages = ADFieldModel.workStages(ADFieldModel.applyTarget(field.plan, info.kind, info.fruitName), info.kind, info.needs)
|
||||
return stages[1]
|
||||
end
|
||||
if ADFieldModel.isCare(field.stage) then return nil end
|
||||
local nextField = ADFieldModel.advance(field, info.needs)
|
||||
if nextField.stage == "done" or nextField.stage == "idle" then return nil end
|
||||
return nextField.stage
|
||||
end
|
||||
|
||||
-- Vindue for en aktiv mark: "harvestCloses" | "lastSowMonth" | "ok".
|
||||
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod)}
|
||||
function ADMonthView.window(field, info, ctx)
|
||||
local stage = field.stage
|
||||
local cropStage = stage == "harvesting" or stage == "mowing" or stage == "ready"
|
||||
if cropStage and info.kind ~= "bare" and ctx.withers ~= nil and ctx.withers(info, ADMonthView.nextPeriod(ctx.period)) then
|
||||
return "harvestCloses"
|
||||
end
|
||||
local sowsLater = field.plan ~= nil and field.plan.sow and ADMonthView.BEFORE_SOWING[stage]
|
||||
if sowsLater and ADMonthView.sowWindow(ctx.plantableIn, ADFieldModel.sowFruit(field), ctx.period) == "closes" then
|
||||
return "lastSowMonth"
|
||||
end
|
||||
return "ok"
|
||||
end
|
||||
|
||||
-- Fremdrift 0-1. Høst/slåning: andelen af HELE arealet der stadig står (standingRatio) — readyRatio duer ikke,
|
||||
-- for slået/høstet afgrøde forsvinder også fra dens nævner (in-game 27/9 F90: halvt slået = 0 %), og
|
||||
-- Courseplays tal er kun positionen i kurset (starter forfra på 0 ved genstart). Øvrige trin: Courseplay.
|
||||
function ADMonthView.progress(field, info, cpProgress)
|
||||
local cropStage = field.stage == "harvesting" or field.stage == "mowing"
|
||||
if cropStage and type(info.standingRatio) == "number" then
|
||||
return math.max(0, math.min(1, 1 - info.standingRatio))
|
||||
end
|
||||
if type(cpProgress) == "number" then return math.max(0, math.min(1, cpProgress)) end
|
||||
if cropStage and type(info.readyRatio) == "number" then
|
||||
return math.max(0, math.min(1, 1 - info.readyRatio))
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function priority(row)
|
||||
if row.window ~= "ok" then return 0 end
|
||||
if ADMonthView.MAIN_STAGES[row.stage] then return 1 end
|
||||
return 2
|
||||
end
|
||||
|
||||
-- Hele fanen: {rows = {...}, critical = n, upcoming = {...}}.
|
||||
-- infos = ADFields.list(), fieldsById = ADFieldJobs.state.fields,
|
||||
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod), cpProgress(fieldId) -> 0-1|nil}
|
||||
function ADMonthView.build(infos, fieldsById, ctx)
|
||||
local rows, upcoming, critical = {}, {}, 0
|
||||
for _, info in ipairs(infos or {}) do
|
||||
local field = fieldsById[info.id]
|
||||
if field ~= nil and ADMonthView.isActive(field) then
|
||||
local row = {id = info.id, stage = field.stage, crop = info.cropTitle, sowFruit = nil,
|
||||
progress = ADMonthView.progress(field, info, ctx.cpProgress ~= nil and ctx.cpProgress(info.id) or nil),
|
||||
nextStage = ADMonthView.nextStage(field, info), window = ADMonthView.window(field, info, ctx)}
|
||||
if field.stage == "sowing" or (info.kind == "bare" and ADMonthView.BEFORE_SOWING[field.stage]) then
|
||||
row.sowFruit = field.plan.sow and ADFieldModel.sowFruit(field) or nil
|
||||
end
|
||||
if row.window ~= "ok" then critical = critical + 1 end
|
||||
table.insert(rows, row)
|
||||
elseif field ~= nil and info.kind == "bare" and (field.stage == "idle" or field.stage == "done") then
|
||||
local target = field.plan.targetFruit
|
||||
if target ~= nil and target ~= "" and ctx.plantableIn ~= nil and ctx.plantableIn(target, ctx.period) then
|
||||
table.insert(upcoming, {id = info.id, fruit = target,
|
||||
closes = ADMonthView.sowWindow(ctx.plantableIn, target, ctx.period) == "closes"})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(rows, function(a, b)
|
||||
local pa, pb = priority(a), priority(b)
|
||||
if pa ~= pb then return pa < pb end
|
||||
return a.id < b.id
|
||||
end)
|
||||
table.sort(upcoming, function(a, b)
|
||||
if a.closes ~= b.closes then return a.closes end
|
||||
return a.id < b.id
|
||||
end)
|
||||
return {rows = rows, critical = critical, upcoming = upcoming}
|
||||
end
|
||||
|
||||
-- "50 %" / "–"
|
||||
function ADMonthView.percentText(progress)
|
||||
if progress == nil then return "–" end
|
||||
return string.format("%d %%", math.floor(progress * 100 + 0.5))
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
-- AD Names
|
||||
-- Egne bygningsnavne (FS25 kan ikke omdøbe bygninger): ADSmartPickup_names.xml i savegame-mappen,
|
||||
-- <ADSmartPickupNames><building id="placeable…" name="Gårdsilo 10"/></ADSmartPickupNames>.
|
||||
-- Moden tilføjer kun navne (nye bygninger / Omdøb-knappen) og ændrer aldrig et eksisterende navn af sig selv.
|
||||
|
||||
ADNames = {}
|
||||
ADNames.FILE_NAME = "ADSmartPickup_names.xml"
|
||||
ADNames.ROOT = "ADSmartPickupNames"
|
||||
ADNames.byId = nil
|
||||
|
||||
-- records = {{id, name}} -> {[id] = name}; tomme/ugyldige springes over
|
||||
function ADNames.fromRecords(records)
|
||||
local byId = {}
|
||||
for _, record in ipairs(records or {}) do
|
||||
local id, name = record.id, record.name
|
||||
if type(id) == "string" and id ~= "" and type(name) == "string" and name:match("%S") then byId[id] = name end
|
||||
end
|
||||
return byId
|
||||
end
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADNames.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADNames.load()
|
||||
ADNames.byId = {}
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupNames", path)
|
||||
if xmlFile == nil then return end
|
||||
local records = {}
|
||||
xmlFile:iterate(ADNames.ROOT .. ".building", function(_, key)
|
||||
table.insert(records, {id = xmlFile:getString(key .. "#id"), name = xmlFile:getString(key .. "#name")})
|
||||
end)
|
||||
xmlFile:delete()
|
||||
ADNames.byId = ADNames.fromRecords(records)
|
||||
local count = 0
|
||||
for _ in pairs(ADNames.byId) do count = count + 1 end
|
||||
Logging.info("ADSmartPickup: %d egne bygningsnavne indlæst", count)
|
||||
end
|
||||
|
||||
-- Eget navn for bygningen (placeable), ellers nil.
|
||||
function ADNames.forPlaceable(placeable)
|
||||
if placeable == nil or placeable.uniqueId == nil then return nil end
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
return (ADNames.byId or {})[tostring(placeable.uniqueId)]
|
||||
end
|
||||
|
||||
function ADNames.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupNames", path, ADNames.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local ids = {}
|
||||
for id in pairs(ADNames.byId or {}) do table.insert(ids, id) end
|
||||
table.sort(ids)
|
||||
for index, id in ipairs(ids) do
|
||||
local key = string.format("%s.building(%d)", ADNames.ROOT, index - 1)
|
||||
xmlFile:setString(key .. "#id", id)
|
||||
xmlFile:setString(key .. "#name", ADNames.byId[id])
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Omdøb (knappen i bygningsfanerne): gemmes straks; tomt navn = tilbage til butiksnavnet.
|
||||
function ADNames.set(id, name)
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
local trimmed = type(name) == "string" and name:match("^%s*(.-)%s*$") or ""
|
||||
ADNames.byId[id] = trimmed ~= "" and trimmed or nil
|
||||
pcall(ADNames.save)
|
||||
Logging.info("ADSmartPickup: bygning %s omdøbt til '%s'", tostring(id), trimmed)
|
||||
if ADBuildings ~= nil and ADBuildings.invalidate ~= nil then ADBuildings.invalidate() end
|
||||
end
|
||||
|
||||
local function position(placeable)
|
||||
local node = placeable ~= nil and placeable.rootNode or nil
|
||||
if node == nil then return nil end
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function animalTypeName(placeable)
|
||||
local ok, name = pcall(function()
|
||||
local index = placeable:getAnimalTypeIndex()
|
||||
local animalType = g_currentMission.animalSystem:getTypeByIndex(index)
|
||||
return animalType ~= nil and animalType.name or nil
|
||||
end)
|
||||
return ok and name or nil
|
||||
end
|
||||
|
||||
local function rawName(placeable)
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
return ok and name ~= nil and tostring(name) or nil
|
||||
end
|
||||
|
||||
-- Mark-markører ("F118") fra AutoDrive: {{name, x, z}}
|
||||
local function fieldMarkers(adEnv)
|
||||
local result = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if tostring(marker.name):match("^F%d+$") then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
if wayPoint ~= nil then table.insert(result, {name = marker.name, x = wayPoint.x, z = wayPoint.z}) end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Nye bygninger (uden navn) får et navn efter ADNaming; kaldes når bygningslisten bygges. Retter navnene i
|
||||
-- listen med det samme og gemmer filen, hvis der kom nye.
|
||||
function ADNames.autoName(adEnv, buildings)
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
local existing = ADNames.byId or {}
|
||||
local missing = false
|
||||
for _, building in ipairs(buildings) do if existing[building.id] == nil and building.kind ~= "sell" then missing = true break end end
|
||||
if not missing then return end
|
||||
-- gårdens klynge: valgte gårdlagre i marksilo-flowet + siloer der allerede hedder "Gårdsilo …", og naboer ≤ 60 m
|
||||
local silos, farm, queue = {}, {}, {}
|
||||
for _, building in ipairs(buildings) do
|
||||
if building.kind == "silo" then
|
||||
local x, z = position(building.placeable)
|
||||
if x ~= nil then
|
||||
local silo = {id = building.id, x = x, z = z}
|
||||
table.insert(silos, silo)
|
||||
local isStore = ADFieldSilos ~= nil and ADFieldSiloConfig ~= nil and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id)
|
||||
if isStore or tostring(existing[building.id] or ""):match("^Gårdsilo") then farm[building.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
end
|
||||
end
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue)
|
||||
for _, silo in ipairs(silos) do
|
||||
if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= 3600 then farm[silo.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
end
|
||||
local fields = fieldMarkers(adEnv)
|
||||
local inputs = {}
|
||||
for _, building in ipairs(buildings) do
|
||||
if existing[building.id] == nil then
|
||||
local x, z = position(building.placeable)
|
||||
local nearest = nil
|
||||
if x ~= nil and building.kind == "silo" then
|
||||
for _, field in ipairs(fields) do
|
||||
local distance = math.sqrt((field.x - x) ^ 2 + (field.z - z) ^ 2)
|
||||
if nearest == nil or distance < nearest.distance then nearest = {name = field.name, distance = distance} end
|
||||
end
|
||||
end
|
||||
table.insert(inputs, {id = building.id, kind = building.kind, animal = building.kind == "husbandry" and animalTypeName(building.placeable) or nil,
|
||||
storeName = rawName(building.placeable), inFarmCluster = farm[building.id] == true, nearestField = nearest})
|
||||
end
|
||||
end
|
||||
local added = ADNaming.assign(inputs, existing)
|
||||
local count = 0
|
||||
for id, name in pairs(added) do
|
||||
existing[id] = name
|
||||
count = count + 1
|
||||
Logging.info("ADSmartPickup: ny bygning navngivet '%s' (%s)", name, id)
|
||||
end
|
||||
if count == 0 then return end
|
||||
ADNames.byId = existing
|
||||
for _, building in ipairs(buildings) do
|
||||
if added[building.id] ~= nil then building.name = added[building.id] end
|
||||
end
|
||||
pcall(ADNames.save)
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
-- AD Naming
|
||||
-- Regler for automatiske navne på bygninger uden eget navn (rene funktioner; spil-delen samler input i adNames).
|
||||
-- Stalde efter dyretype, siloer efter gårdens klynge/nærmeste mark, palleskure nummereret, fabrikker efter
|
||||
-- butiksnavnet. Numrene fortsætter fra de navne, der allerede findes ("Svinestald 1-5" -> "Svinestald 6").
|
||||
|
||||
ADNaming = {}
|
||||
|
||||
ADNaming.ANIMAL_PREFIX = {COW = "Kostald", PIG = "Svinestald", SHEEP = "Fårestald", GOAT = "Gedestald",
|
||||
CHICKEN = "Hønsehus", HORSE = "Hestestald"}
|
||||
ADNaming.FIELD_SILO_DISTANCE = 200
|
||||
|
||||
local function escape(text) return (tostring(text):gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")) end
|
||||
|
||||
-- Højeste "prefix N" blandt navnene + 1.
|
||||
function ADNaming.nextNumber(names, prefix)
|
||||
local highest, pattern = 0, "^" .. escape(prefix) .. " (%d+)$"
|
||||
for _, name in ipairs(names) do
|
||||
local number = tonumber(tostring(name):match(pattern))
|
||||
if number ~= nil and number > highest then highest = number end
|
||||
end
|
||||
return highest + 1
|
||||
end
|
||||
|
||||
local function contains(names, wanted)
|
||||
for _, name in ipairs(names) do if name == wanted then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Et fast navn der kan være brugt: "X", ellers "X 2", "X 3" ...
|
||||
local function uniqueFixed(names, base)
|
||||
if not contains(names, base) then return base end
|
||||
return string.format("%s %d", base, math.max(2, ADNaming.nextNumber(names, base)))
|
||||
end
|
||||
|
||||
-- Navn for én bygning (nil = navngives ikke, fx salgssteder).
|
||||
-- b = {kind, animal, storeName, inFarmCluster, nearestField = {name, distance}}
|
||||
local function nameFor(b, names)
|
||||
if b.kind == "husbandry" then
|
||||
local prefix = ADNaming.ANIMAL_PREFIX[tostring(b.animal or ""):upper()] or "Stald"
|
||||
return string.format("%s %d", prefix, ADNaming.nextNumber(names, prefix))
|
||||
elseif b.kind == "silo" then
|
||||
if b.inFarmCluster then return string.format("Gårdsilo %d", ADNaming.nextNumber(names, "Gårdsilo")) end
|
||||
local field = b.nearestField
|
||||
if field ~= nil and field.name ~= nil and (field.distance or math.huge) <= ADNaming.FIELD_SILO_DISTANCE then
|
||||
return uniqueFixed(names, field.name .. " marksilo")
|
||||
end
|
||||
return string.format("Silo %d", ADNaming.nextNumber(names, "Silo"))
|
||||
elseif b.kind == "objectStorage" then
|
||||
return string.format("Palleskur %d", ADNaming.nextNumber(names, "Palleskur"))
|
||||
elseif b.kind == "sell" then
|
||||
return nil
|
||||
end
|
||||
if b.storeName == nil or b.storeName == "" then return nil end
|
||||
return uniqueFixed(names, b.storeName)
|
||||
end
|
||||
|
||||
-- Navne til bygninger der ikke har et endnu. existing = {[id] = navn}; returnerer kun de nye {[id] = navn}.
|
||||
function ADNaming.assign(buildings, existing)
|
||||
local names, result = {}, {}
|
||||
for _, name in pairs(existing or {}) do table.insert(names, name) end
|
||||
for _, b in ipairs(buildings or {}) do
|
||||
if b.id ~= nil and (existing or {})[b.id] == nil then
|
||||
local name = nameFor(b, names)
|
||||
if name ~= nil then
|
||||
result[b.id] = name
|
||||
table.insert(names, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Markmaskiner: navn efter rolle (høstere efter type: "beet" = roe-/kartoffeloptager, "forage" = finsnitter).
|
||||
ADNaming.ROLE_PREFIX = {collector = "Læssevogn", mower = "Skårlægger", tedder = "Vender", rake = "River",
|
||||
baler = "Ballepresser", baleCollector = "Ballesamler", seeder = "Såmaskine", roller = "Tromle",
|
||||
weeder = "Ukrudtsmaskine", fertilizer = "Gødningsspreder", limer = "Kalkspreder", herbicide = "Sprøjte",
|
||||
mulcher = "Stubhakker", cultivator = "Kultivator", plow = "Plov", unloader = "Tømmevogn"}
|
||||
ADNaming.HARVESTER_PREFIX = {beet = "Roeoptager", forage = "Finsnitter"}
|
||||
|
||||
function ADNaming.fleetName(role, subtype, names)
|
||||
local prefix
|
||||
if role == "harvester" then
|
||||
prefix = ADNaming.HARVESTER_PREFIX[subtype] or "Mejetærsker"
|
||||
else
|
||||
prefix = ADNaming.ROLE_PREFIX[role]
|
||||
end
|
||||
if prefix == nil then return nil end
|
||||
return string.format("%s %d", prefix, ADNaming.nextNumber(names or {}, prefix))
|
||||
end
|
||||
@@ -14,6 +14,8 @@ ADOutbound = {}
|
||||
ADOutbound.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
|
||||
ADOutbound.CHECK_INTERVAL_MS = 5000
|
||||
-- spred over HELE intervallet, så genplanlægningerne (og cache-genberegninger) fordeles jævnt
|
||||
ADOutbound.CHECK_JITTER_MS = 5000
|
||||
-- "Kør uanset mængde": stiger vognens fyldning ikke så længe ved kilden, køres der med det der er.
|
||||
ADOutbound.NO_PROGRESS_MS = 15000
|
||||
-- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl).
|
||||
@@ -25,6 +27,9 @@ ADOutbound.NOTIFICATION_MS = 8000
|
||||
-- spawnet, forkert farm, ...): kilden udelukkes for varen i EMPTY_SOURCE_TTL_MS og turen afsluttes.
|
||||
ADOutbound.EMPTY_SOURCE_MS = 30000
|
||||
ADOutbound.EMPTY_SOURCE_TTL_MS = 300000
|
||||
-- kilde der gav lidt og så intet mere (fx paller uden for Universal Autoloads rækkevidde): hviler så længe,
|
||||
-- så de andre kilder kommer til (in-game 26/9: æg-vognen kørte til samme hønsehus tre gange i træk)
|
||||
ADOutbound.THIN_SOURCE_REST_MS = 180000
|
||||
-- Autoload-bil på vej til aflæsning: stropperne løsnes så mange meter FØR markøren. Markøren står
|
||||
-- hvor næsen stopper, og en lastbils lad har passeret salgstriggeren dér; AutoDrive løsner selv
|
||||
-- først ved markøren ("AL unload = center"), hvilket er for sent for en lastbil.
|
||||
@@ -42,6 +47,11 @@ local warned = setmetatable({}, {__mode = "k"})
|
||||
local emptyUntil = {}
|
||||
local isInstalled = false
|
||||
|
||||
-- Glem alle "tom/hvilende kilde"-markeringer (tests; og når runs genindlæses).
|
||||
function ADOutbound.resetEmptySources()
|
||||
emptyUntil = {}
|
||||
end
|
||||
|
||||
local function now()
|
||||
if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
|
||||
return g_currentMission.time
|
||||
@@ -176,6 +186,12 @@ local function findSellQuote(adEnv, vehicle, product, sellMarkers, sellAtPercent
|
||||
if unreachableName ~= nil then
|
||||
return nil, unreachableName
|
||||
end
|
||||
-- ingen pris over grænsen: det bedste salgssted der kan nås (bruges af "sælg altid over X %")
|
||||
for _, quote in ipairs(quotes) do
|
||||
if homeId == nil or ADWaitPool.hasRouteBothWays(adEnv, vehicle, quote.wayPointId, homeId) then
|
||||
return quote, nil
|
||||
end
|
||||
end
|
||||
return quotes[1], nil
|
||||
end
|
||||
|
||||
@@ -241,7 +257,9 @@ local function findStoreMarker(adEnv, vehicle, product, capacity, sources, liter
|
||||
return {wayPointId = chosen.id, name = tostring(chosen.name), free = liters}
|
||||
end
|
||||
local avoid = ADRunsController ~= nil and ADRunsController.getPickupMarkers ~= nil and ADRunsController.getPickupMarkers() or nil
|
||||
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources), avoid, true)
|
||||
-- flydende varer (gylle, digestat, mælk ...) kun i tanke/siloer — ikke automatisk i en fabriks rist
|
||||
local allowFactories = not (ADLoadSwap.isLiquid ~= nil and ADLoadSwap.isLiquid(product.fillType))
|
||||
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources), avoid, allowFactories)
|
||||
if marker == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -346,6 +364,7 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle)
|
||||
hasAutoload = hasAutoload,
|
||||
sellAtPercent = run.sellAtPercent,
|
||||
storeAbovePercent = run.storeAbovePercent,
|
||||
sellAbovePercent = run.sellAbovePercent,
|
||||
collectRound = run.collectRound == true,
|
||||
products = products,
|
||||
}
|
||||
@@ -474,7 +493,17 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||||
local isReachable = function(candidate)
|
||||
return ADWaitPool.hasRouteTo(adEnv, vehicle, candidate.marker.id)
|
||||
end
|
||||
local candidate = ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable)
|
||||
-- "All": nærmest flowets egne kilder, ikke nærmest traktoren (in-game 29/9: Milk Store ved sukkerroeknuseren)
|
||||
local anchorX, anchorZ = nil, nil
|
||||
if run.waitPoolGroup == nil or run.waitPoolGroup == "" then
|
||||
local points = {}
|
||||
for _, id in ipairs(run.sourceWayPointIds or {}) do
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(id)
|
||||
if wayPoint ~= nil then table.insert(points, {x = wayPoint.x, z = wayPoint.z}) end
|
||||
end
|
||||
anchorX, anchorZ = ADWaitPool.anchorOf(points)
|
||||
end
|
||||
local candidate = ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable, anchorX, anchorZ)
|
||||
local marker, source = nil, nil
|
||||
if candidate ~= nil then
|
||||
ADWaitPool.claim(candidate.marker.id, vehicle)
|
||||
@@ -490,7 +519,8 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||||
warnOnce(adEnv, vehicle, "noWait", string.format("'%s' har intet ledigt Wait-punkt med vejnet (run '%s'); holder ved kilden %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(run.name), tostring(marker.name)))
|
||||
end
|
||||
local previous = holds[vehicle]
|
||||
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = 0}
|
||||
-- tilfældig start (0-2 s) så holdende vogne ikke genplanlægger i samme frame (hak hvert 5. s, 26/9)
|
||||
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = math.random(0, ADOutbound.CHECK_JITTER_MS)}
|
||||
local message = string.format("'%s' holder ved %s: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker.name), table.concat(reasons, "; "))
|
||||
if previous == nil or previous.lastLogged ~= message then
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
|
||||
@@ -753,7 +783,12 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||||
return true
|
||||
end
|
||||
if state.noProgressMs > ADOutbound.NO_PROGRESS_MS and level > 0 and not spawnPending then
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), level)
|
||||
local rests = now() + ADOutbound.THIN_SOURCE_REST_MS
|
||||
for _, fillType in ipairs(state.trip.fillTypes) do
|
||||
local key = tostring(state.current) .. ":" .. tostring(fillType)
|
||||
if emptyUntil[key] == nil or emptyUntil[key] < rests then emptyUntil[key] = rests end
|
||||
end
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l (kilden hviler %d min, så de andre kommer til)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), level, math.floor(ADOutbound.THIN_SOURCE_REST_MS / 60000))
|
||||
state.noProgressMs, state.completed = 0, true
|
||||
task:finished()
|
||||
return true
|
||||
|
||||
@@ -68,6 +68,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -123,7 +125,7 @@ local function collectCandidates(input)
|
||||
local e = ADOutboundPlanner.evaluate(product, input)
|
||||
local added = false
|
||||
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
|
||||
if wantsSell(product.policy) and e.priceOk and product.quote ~= nil and canSellWithoutReserve(e, input) then
|
||||
if wantsSell(product.policy) and (e.priceOk or e.overflow) and product.quote ~= nil and canSellWithoutReserve(e, input) then
|
||||
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
|
||||
added = true
|
||||
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh or (input.feedMode and e.available >= ADOutboundPlanner.FEED_MIN_LITERS)) and product.store ~= nil then
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
-- AD Pool
|
||||
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
||||
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
||||
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
||||
|
||||
ADPool = {}
|
||||
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
||||
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
||||
-- tip/snegl er tømmevogn.
|
||||
function ADPool.roleOf(specs)
|
||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||
if role ~= nil and role ~= "unloader" then return role end
|
||||
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
||||
return role
|
||||
end
|
||||
|
||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||
function ADPool.numberOf(value)
|
||||
if type(value) == "number" then return value end
|
||||
if type(value) ~= "table" then return nil end
|
||||
local best = nil
|
||||
for _, entry in pairs(value) do
|
||||
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
||||
function ADPool.powerOk(tractorHp, neededHp)
|
||||
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
||||
return tractorHp >= neededHp
|
||||
end
|
||||
|
||||
-- Butikskategori: markredskaber kun bag traktorer (tractorsS/M/L …) — ikke lastbil/Unimog, læssere o.l.
|
||||
-- Tømmevogne må alle trække. Ukendt kategori tillades (intet må gå i stå på manglende data).
|
||||
ADPool.ANY_PULLER_ROLES = {unloader = true}
|
||||
|
||||
function ADPool.categoryOk(category, role)
|
||||
if category == nil or ADPool.ANY_PULLER_ROLES[role] then return true end
|
||||
return tostring(category):lower():find("^tractors") ~= nil
|
||||
end
|
||||
|
||||
local function suits(tractor, implement, role)
|
||||
return ADPool.powerOk(tractor.hp, implement.neededHp) and ADPool.categoryOk(tractor.category, role)
|
||||
end
|
||||
|
||||
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
||||
function ADPool.connectFor(parkName, markers)
|
||||
if type(parkName) ~= "string" then return nil end
|
||||
local wanted = parkName:lower()
|
||||
for _, marker in pairs(markers or {}) do
|
||||
local action, target = ADHitch.parseMarker(marker.name)
|
||||
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function hasRole(item, role)
|
||||
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
||||
end
|
||||
|
||||
local function distanceSq(ax, az, bx, bz)
|
||||
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
||||
end
|
||||
|
||||
local function nearest(items, x, z, keep)
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, item in ipairs(items) do
|
||||
if keep == nil or keep(item) then
|
||||
local distance = distanceSq(item.x, item.z, x, z)
|
||||
if distance < bestDistance then best, bestDistance = item, distance end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Når opgaven slutter, sættes puljeredskabet af — også når AutoDrive ellers selv ville parkere (påfyldte
|
||||
-- sæt): AD parkerer ved redskabets plads med redskabet på. Aldrig ved manuel transport.
|
||||
-- Bedste pasform: den mindste traktor med hk nok; afstand afgør kun mellem traktorer inden for FIT_BAND af den
|
||||
-- mindste. Over OVERSIZE_FACTOR x behovet kun når intet andet passer; ukendt traktor-hk efter de kendte.
|
||||
-- Redskab uden hk-data: nærmeste (intet at matche på). tractors = allerede filtrerede kandidater.
|
||||
ADPool.FIT_BAND = 1.15
|
||||
ADPool.OVERSIZE_FACTOR = 2.5
|
||||
|
||||
function ADPool.bestFit(tractors, implement, role)
|
||||
local fitting = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if suits(tractor, implement, role) then table.insert(fitting, tractor) end
|
||||
end
|
||||
local needed = implement.neededHp
|
||||
if needed == nil or needed <= 0 then return nearest(fitting, implement.x, implement.z) end
|
||||
local sized, oversized, unknown = {}, {}, {}
|
||||
for _, tractor in ipairs(fitting) do
|
||||
if tractor.hp == nil or tractor.hp <= 0 then table.insert(unknown, tractor)
|
||||
elseif tractor.hp > needed * ADPool.OVERSIZE_FACTOR then table.insert(oversized, tractor)
|
||||
else table.insert(sized, tractor) end
|
||||
end
|
||||
local pool = #sized > 0 and sized or (#unknown > 0 and unknown or oversized)
|
||||
if pool ~= unknown then
|
||||
local smallest = math.huge
|
||||
for _, tractor in ipairs(pool) do smallest = math.min(smallest, tractor.hp) end
|
||||
local band = {}
|
||||
for _, tractor in ipairs(pool) do
|
||||
if tractor.hp <= smallest * ADPool.FIT_BAND then table.insert(band, tractor) end
|
||||
end
|
||||
pool = band
|
||||
end
|
||||
return nearest(pool, implement.x, implement.z)
|
||||
end
|
||||
|
||||
function ADPool.shouldReturnHome(assignment)
|
||||
return not assignment.manual
|
||||
end
|
||||
|
||||
-- Kun maskiner uden egen arbejdsrolle (almindelige traktorer) henter puljeredskaber — ikke mejetærskere,
|
||||
-- selvkørende sprøjter o.l., selv om de har koblingspunkter.
|
||||
function ADPool.canPull(role)
|
||||
return role == nil
|
||||
end
|
||||
|
||||
-- Hvem fik koblingen til at fejle? Kom traktoren ikke frem, er det traktoren (ingen AD-rute), ellers redskabet.
|
||||
function ADPool.failureBlames(reason)
|
||||
if tostring(reason):find("^stoppedAway") ~= nil then return "tractor" end
|
||||
return "implement"
|
||||
end
|
||||
|
||||
-- Efter koblingen: refill = markarbejdets påfyldningsplan (nil = intet at fylde).
|
||||
function ADPool.nextDrive(refill)
|
||||
if refill == nil or not refill.empty then return "drive" end
|
||||
if refill.refillId ~= nil then return "load" end
|
||||
return "empty"
|
||||
end
|
||||
|
||||
-- Puljen bruges til opgaver med ét sæt (også tømmevogne ved høst), ikke når marken har egne faste maskiner
|
||||
-- til rollen.
|
||||
function ADPool.mayUsePool(decision, hasOwnForRole)
|
||||
return (decision.count or 1) == 1 and not hasOwnForRole
|
||||
end
|
||||
|
||||
-- Kan puljevognen bære varen? (implement.carries = {[fillType] = true}; nil vare = intet krav)
|
||||
function ADPool.canCarry(implement, fillType)
|
||||
if fillType == nil then return true end
|
||||
return implement.carries ~= nil and implement.carries[fillType] == true
|
||||
end
|
||||
|
||||
-- Status til flådefanen. s = {parkName, hasConnect, attachedTo, quarantineMin, reserved} -> nøgle, argument
|
||||
function ADPool.implementStatus(s)
|
||||
if s.parkName == nil then return "noPark" end
|
||||
if not s.hasConnect then return "noConnect", s.parkName end
|
||||
if s.attachedTo ~= nil then return "attached", s.attachedTo end
|
||||
if s.flowName ~= nil then return "flow", s.flowName end
|
||||
if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end
|
||||
if s.reserved then return "reserved" end
|
||||
return "parked", s.parkName
|
||||
end
|
||||
|
||||
-- s = {hasPark, enabled, controlled, returning, fieldId, busy, offNetwork, hitched} -> nøgle, argument
|
||||
function ADPool.tractorStatus(s)
|
||||
if not s.hasPark then return "noPark" end
|
||||
if s.controlled then return "player" end
|
||||
if s.returning then return "returning" end
|
||||
if s.fieldId ~= nil then return "job", s.fieldId end
|
||||
if s.busy then return "busy" end
|
||||
if not s.enabled then return "notInFleet" end
|
||||
if s.offNetwork then return "offNetwork" end
|
||||
if s.hitched then return "hitched" end
|
||||
return "free"
|
||||
end
|
||||
|
||||
-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet.
|
||||
-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool.
|
||||
function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept)
|
||||
blocked = blocked or {}
|
||||
local free = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if not blocked[tractor.id] and not tractor.offNetwork then table.insert(free, tractor) end
|
||||
end
|
||||
local candidates = {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
||||
table.insert(candidates, implement)
|
||||
end
|
||||
end
|
||||
if #candidates == 0 then return nil, "noPoolImplement" end
|
||||
if #free == 0 then return nil, "noPoolTractor" end
|
||||
table.sort(candidates, function(a, b)
|
||||
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
||||
end)
|
||||
local weakest = nil
|
||||
for _, implement in ipairs(candidates) do
|
||||
local tractor = ADPool.bestFit(free, implement, role)
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
if weakest == nil or (implement.neededHp or 0) < (weakest.neededHp or 0) then weakest = implement end
|
||||
end
|
||||
return nil, "tooWeak", {weakest ~= nil and weakest.neededHp or 0, weakest ~= nil and weakest.name or "?"}
|
||||
end
|
||||
|
||||
-- Alle frie puljeredskaber med rollen, hver parret med den nærmeste endnu ubrugte traktor med nok hk
|
||||
-- (marksilo-flowet planlægger over mange vogne på én gang).
|
||||
function ADPool.pairAll(role, implements, tractors, blocked)
|
||||
blocked = blocked or {}
|
||||
local used, result = {}, {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if hasRole(implement, role) and not blocked[implement.id] then
|
||||
local open = {}
|
||||
for _, t in ipairs(tractors or {}) do
|
||||
if not used[t.id] and not blocked[t.id] and not t.offNetwork then table.insert(open, t) end
|
||||
end
|
||||
local tractor = ADPool.bestFit(open, implement, role)
|
||||
if tractor ~= nil then
|
||||
used[tractor.id] = true
|
||||
table.insert(result, {implement = implement, tractor = tractor})
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Ventende marker prøver igen på fælles taktslag (ikke hver sin 30 s fra eget forsøg): så udløber de i samme
|
||||
-- tick, og køens rækkefølge (længst ventende først) afgør hvem der får den næste ledige traktor.
|
||||
function ADPool.alignedRetry(nowMs, periodMs)
|
||||
return (math.floor(nowMs / periodMs) + 1) * periodMs
|
||||
end
|
||||
|
||||
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
||||
function ADPool.queueOrder(infos, waitingSince)
|
||||
local waiting, rest = {}, {}
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
||||
end
|
||||
table.sort(waiting, function(a, b)
|
||||
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
||||
if wa ~= wb then return wa < wb end
|
||||
return a.id < b.id
|
||||
end)
|
||||
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
||||
return waiting
|
||||
end
|
||||
|
||||
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
||||
local reservations, quarantined = {}, {}
|
||||
|
||||
function ADPool.reserve(owner, ids)
|
||||
reservations[owner] = ids
|
||||
end
|
||||
|
||||
function ADPool.release(owner)
|
||||
reservations[owner] = nil
|
||||
end
|
||||
|
||||
function ADPool.quarantine(id, nowMs)
|
||||
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
||||
end
|
||||
|
||||
function ADPool.quarantineMinutes(id, nowMs)
|
||||
local untilMs = quarantined[id]
|
||||
if untilMs == nil or nowMs >= untilMs then return 0 end
|
||||
return math.ceil((untilMs - nowMs) / 60000)
|
||||
end
|
||||
|
||||
function ADPool.isReserved(id)
|
||||
for _, ids in pairs(reservations) do
|
||||
for _, reserved in ipairs(ids) do if reserved == id then return true end end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADPool.blocked(nowMs)
|
||||
local result = {}
|
||||
for _, ids in pairs(reservations) do
|
||||
for _, id in ipairs(ids) do result[id] = true end
|
||||
end
|
||||
for id, untilMs in pairs(quarantined) do
|
||||
if nowMs < untilMs then result[id] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
||||
local returning = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADPool.setReturning(vehicle, isOn)
|
||||
returning[vehicle] = isOn and true or nil
|
||||
end
|
||||
|
||||
function ADPool.isReturning(vehicle)
|
||||
return returning[vehicle] == true
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function parkMarker(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
if type(index) ~= "number" or index < 1 then return nil end
|
||||
return adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
end
|
||||
|
||||
local function positionOf(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
||||
local neededHpCache = {} -- configFileName -> hk eller false (butiksdata ændrer sig ikke i et spil)
|
||||
|
||||
local function neededHpOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local cached = neededHpCache[vehicle.configFileName]
|
||||
if cached ~= nil then return cached or nil end
|
||||
local value = ADPool.readNeededHp(vehicle)
|
||||
neededHpCache[vehicle.configFileName] = value or false
|
||||
return value
|
||||
end
|
||||
|
||||
function ADPool.readNeededHp(vehicle)
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
if item == nil then return nil end
|
||||
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
||||
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
||||
end
|
||||
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
||||
end
|
||||
|
||||
local tractorHpCache = setmetatable({}, {__mode = "k"}) -- vehicle -> hk eller false
|
||||
|
||||
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. Gemmes pr. traktor.
|
||||
local function tractorHpOf(vehicle)
|
||||
local cached = tractorHpCache[vehicle]
|
||||
if cached ~= nil then return cached or nil end
|
||||
local value = ADPool.readTractorHp(vehicle)
|
||||
tractorHpCache[vehicle] = value or false
|
||||
return value
|
||||
end
|
||||
|
||||
function ADPool.readTractorHp(vehicle)
|
||||
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
||||
local kw = motor ~= nil and motor.peakMotorPower or nil
|
||||
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
||||
end
|
||||
|
||||
-- Butikskategori (fx "TRACTORSM", "TRUCKS") eller nil.
|
||||
local function categoryOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
return item ~= nil and item.categoryName or nil
|
||||
end
|
||||
|
||||
-- Varer og samlet kapacitet i redskabets fyldenheder (tømmevogne).
|
||||
function ADPool.cargoOf(vehicle)
|
||||
local carries, capacity = {}, 0
|
||||
local units = vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits or {}
|
||||
for index in ipairs(units) do
|
||||
capacity = capacity + (vehicle:getFillUnitCapacity(index) or 0)
|
||||
for fillType in pairs(vehicle:getFillUnitSupportedFillTypes(index) or {}) do carries[fillType] = true end
|
||||
end
|
||||
return carries, capacity
|
||||
end
|
||||
|
||||
-- Ikke bundet af et kørende flow (traktorer i stoppede flows er ledige).
|
||||
function ADPool.isFreeFromFlows(vehicle)
|
||||
if ADFlowWagons ~= nil then return ADFlowWagons.isFreeTractor(vehicle) end
|
||||
return ADRunsController.getRunForVehicle(vehicle) == nil
|
||||
end
|
||||
|
||||
local function isAttached(vehicle)
|
||||
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||
end
|
||||
|
||||
-- Traktorens egen rolle uden de redskaber der hænger på (mejetærsker/selvkørende har en, en traktor ikke).
|
||||
function ADPool.ownRole(vehicle)
|
||||
local specs = {}
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if vehicle[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
return ADFieldFleet.roleFromSpecs(specs)
|
||||
end
|
||||
|
||||
-- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer.
|
||||
-- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur).
|
||||
function ADPool.collect(adEnv, isBusy)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local implements, tractors = {}, {}
|
||||
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
||||
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
||||
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
||||
local held = role ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(ADRunsController.getVehicleId(vehicle))
|
||||
if role ~= nil and not held then
|
||||
local x, z = positionOf(vehicle)
|
||||
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||
table.insert(implements, {carries = carries, capacity = capacity,
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
||||
and ADPool.isFreeFromFlows(vehicle) and not controlled and not isBusy(vehicle)
|
||||
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)
|
||||
and ADPool.canPull(ADPool.ownRole(vehicle)) then
|
||||
local x, z = positionOf(vehicle)
|
||||
-- uden for AD-vejnettet: den kan ikke køre ud efter redskabet (ville sende redskaber i karantæne)
|
||||
local networkDistance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||
hp = tractorHpOf(vehicle), category = categoryOf(vehicle), parkId = park.id,
|
||||
offNetwork = networkDistance ~= nil and networkDistance > ADFieldWork.MAX_OFF_NETWORK})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
|
||||
-- Oversigt til flådefanen: alle redskaber med en markrolle (også dem der mangler parkering/Connect, så
|
||||
-- spilleren kan se hvorfor) og alle traktorer uden egen arbejdsrolle, der ikke hører til et flow.
|
||||
function ADPool.overview(adEnv)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local nowMs = ADFieldJobs.now or 0
|
||||
local implements, tractors = {}, {}
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil then
|
||||
local role = ADPool.roleOf(ADCpBridge.getSpecs(vehicle))
|
||||
if role ~= nil then
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local attacher = isAttached(vehicle) and vehicle:getAttacherVehicle() or nil
|
||||
local key, arg = ADPool.implementStatus({
|
||||
parkName = park ~= nil and park.name or nil,
|
||||
hasConnect = park ~= nil and ADPool.connectFor(park.name, markers) ~= nil,
|
||||
attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil,
|
||||
quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0,
|
||||
reserved = id ~= nil and ADPool.isReserved(id),
|
||||
flowName = id ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(id)
|
||||
and tostring(ADFlowWagons.ownerOf(ADRunsController.store.runs, id).name) or nil,
|
||||
})
|
||||
table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
-- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination
|
||||
if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADPool.isFreeFromFlows(vehicle)
|
||||
and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then
|
||||
local x, z = positionOf(vehicle)
|
||||
local distance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||
local assignment = state.assignments[id]
|
||||
local rear = ADHitch.rearImplementsOf(vehicle)
|
||||
local key, arg = ADPool.tractorStatus({
|
||||
hasPark = parkMarker(adEnv, vehicle) ~= nil,
|
||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled(),
|
||||
returning = ADPool.isReturning(vehicle),
|
||||
fieldId = assignment ~= nil and assignment.fieldId or nil,
|
||||
busy = ADFieldWork.isBusyVehicle(vehicle, id),
|
||||
offNetwork = distance ~= nil and distance > ADFieldWork.MAX_OFF_NETWORK,
|
||||
hitched = #rear > 0,
|
||||
})
|
||||
table.insert(tractors, {id = id, label = ADRuns.vehicleLabel(vehicle), hp = tractorHpOf(vehicle), status = key, arg = arg,
|
||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
-- AD Profiler
|
||||
-- Måler Smart Pickups indgange (AutoDrive-hooks, update-løkken, GUI) i spillet: kald, samlet tid og det
|
||||
-- værste enkeltkald pr. minut, plus det værste frame i alt. Tager noget over REPORT_MIN_MS, skrives top-listen
|
||||
-- i log.txt ("ADSmartPickup: profil …"), så et hak kan føres tilbage til præcis hvilken del der gjorde det.
|
||||
-- Indpakningen sker på modul-tabellernes felter; hooksene slår funktionerne op ved hvert kald, så de måles.
|
||||
|
||||
ADProfiler = {}
|
||||
|
||||
ADProfiler.WINDOW_MS = 60000
|
||||
ADProfiler.REPORT_MIN_MS = 2
|
||||
ADProfiler.TOP = 6
|
||||
ADProfiler.stats = {}
|
||||
ADProfiler.frameMs = 0
|
||||
ADProfiler.worstFrameMs = 0
|
||||
ADProfiler.windowStart = nil
|
||||
ADProfiler.depth = 0
|
||||
ADProfiler.installed = false
|
||||
|
||||
function ADProfiler.clock()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function record(name, started, ...)
|
||||
local finished = ADProfiler.clock()
|
||||
ADProfiler.depth = ADProfiler.depth - 1
|
||||
if started ~= nil and finished ~= nil then
|
||||
local spent = finished - started
|
||||
local stat = ADProfiler.stats[name]
|
||||
if stat == nil then
|
||||
stat = {calls = 0, total = 0, max = 0}
|
||||
ADProfiler.stats[name] = stat
|
||||
end
|
||||
stat.calls = stat.calls + 1
|
||||
stat.total = stat.total + spent
|
||||
if spent > stat.max then stat.max = spent end
|
||||
-- kun yderste måling tæller i frame-summen (indlejrede måles også hver for sig)
|
||||
if ADProfiler.depth == 0 then ADProfiler.frameMs = ADProfiler.frameMs + spent end
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
-- Pak tbl[key] ind under navnet name (gør intet hvis funktionen ikke findes).
|
||||
function ADProfiler.wrap(tbl, key, name)
|
||||
if type(tbl) ~= "table" or type(tbl[key]) ~= "function" then return end
|
||||
local original = tbl[key]
|
||||
tbl[key] = function(...)
|
||||
ADProfiler.depth = ADProfiler.depth + 1
|
||||
return record(name, ADProfiler.clock(), original(...))
|
||||
end
|
||||
end
|
||||
|
||||
-- Kaldes én gang pr. frame fra ADSmartPickup:update: afslutter frame-summen og rapporterer pr. minut.
|
||||
function ADProfiler.frame(now)
|
||||
if ADProfiler.frameMs > ADProfiler.worstFrameMs then ADProfiler.worstFrameMs = ADProfiler.frameMs end
|
||||
ADProfiler.frameMs = 0
|
||||
-- (en fejl i en indpakket funktion springer record over; dybden nulstilles derfor hvert frame)
|
||||
ADProfiler.depth = 0
|
||||
ADProfiler.windowStart = ADProfiler.windowStart or now
|
||||
if now - ADProfiler.windowStart < ADProfiler.WINDOW_MS then return end
|
||||
ADProfiler.report()
|
||||
ADProfiler.windowStart = now
|
||||
end
|
||||
|
||||
function ADProfiler.report()
|
||||
local rows = {}
|
||||
for name, stat in pairs(ADProfiler.stats) do
|
||||
table.insert(rows, {name = name, stat = stat})
|
||||
end
|
||||
table.sort(rows, function(a, b) return a.stat.max > b.stat.max end)
|
||||
local worst = rows[1] ~= nil and rows[1].stat.max or 0
|
||||
if worst >= ADProfiler.REPORT_MIN_MS or ADProfiler.worstFrameMs >= ADProfiler.REPORT_MIN_MS then
|
||||
local parts = {}
|
||||
for index = 1, math.min(ADProfiler.TOP, #rows) do
|
||||
local stat = rows[index].stat
|
||||
table.insert(parts, string.format("%s max %.1f ms (%d kald, i alt %.0f ms)", rows[index].name, stat.max, stat.calls, stat.total))
|
||||
end
|
||||
Logging.info("ADSmartPickup: profil sidste minut — værste frame %.1f ms: %s", ADProfiler.worstFrameMs, table.concat(parts, "; "))
|
||||
end
|
||||
ADProfiler.stats = {}
|
||||
ADProfiler.worstFrameMs = 0
|
||||
end
|
||||
|
||||
-- Indpak alle kendte indgange. Kaldes én gang efter at hooksene er installeret.
|
||||
function ADProfiler.install()
|
||||
if ADProfiler.installed then return end
|
||||
ADProfiler.installed = true
|
||||
local targets = {
|
||||
{ADUnloadWait, "beforeUpdate", "aflæsning/Wait (pr. vogn)"},
|
||||
{ADUnloadWait, "adoptOrphans", "genoptag ventende"},
|
||||
{ADUnloadWait, "releaseInactive", "slip standsede"},
|
||||
{ADOutbound, "beforeLoadUpdate", "udkørsel pålæsning (pr. vogn)"},
|
||||
{ADOutbound, "beforeUnloadUpdate", "udkørsel aflæsning (pr. vogn)"},
|
||||
{ADOutbound, "beforeLoadFinished", "udkørsel runde"},
|
||||
{ADOutbound, "choosePickup", "udkørsel turvalg"},
|
||||
{ADOutbound, "holdSupplyVehicle", "forsyning hold"},
|
||||
{ADOutbound, "releaseInactive", "udkørsel slip"},
|
||||
{ADLoadSwap, "tryFromWait", "læs-bytte fra Wait"},
|
||||
{ADSmartPickup, "shouldWaitForLoading", "vent ved pålæsning"},
|
||||
{ADSmartPickup, "choosePickup", "forsyning kildevalg"},
|
||||
{ADSmartPickup, "findSupplyPickup", "forsyning kildesøgning"},
|
||||
{ADRunsController, "applyVehicleNames", "traktornavne"},
|
||||
{ADFieldJobs, "tick", "markarbejde tick"},
|
||||
{ADFields, "scanStep", "markscanning"},
|
||||
{ADFieldWork, "follow", "markarbejde sæt"},
|
||||
{ADFieldWork, "dispatch", "markarbejde udsendelse"},
|
||||
{ADFieldWork, "getRigs", "markflåde"},
|
||||
{ADBuildings, "list", "bygningsliste"},
|
||||
{ADSources, "getFarmInventory", "gårdens lagre"},
|
||||
{ADWaitPool, "hasRouteTo", "rutetjek"},
|
||||
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
|
||||
}
|
||||
for _, target in ipairs(targets) do
|
||||
ADProfiler.wrap(target[1], target[2], target[3])
|
||||
end
|
||||
if SmartPickupFrame ~= nil then
|
||||
ADProfiler.wrap(SmartPickupFrame, "refreshLive", "menu opdatering")
|
||||
ADProfiler.wrap(SmartPickupFrame, "rebuild", "menu genopbygning")
|
||||
end
|
||||
end
|
||||
@@ -17,6 +17,9 @@ ADRuns.KINDS = {supply = true, outbound = true}
|
||||
ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true}
|
||||
ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100
|
||||
ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95
|
||||
-- "Sælg altid over X %": kildens lager over grænsen sælges uanset pris (0 = fra). Så hober f.eks. gylle
|
||||
-- ved BGA'en sig ikke op, når prisen er lav.
|
||||
ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT = 0, 0, 100
|
||||
|
||||
local function copyList(list)
|
||||
local copy = {}
|
||||
@@ -39,6 +42,28 @@ local function clampRange(value, default, min, max)
|
||||
return math.max(min, math.min(max, math.floor(number)))
|
||||
end
|
||||
|
||||
-- Flow-vogne: {[traktorId] = vognId} (maskinpulje — flowet henter sin egen vogn).
|
||||
function ADRuns.copyWagons(wagons)
|
||||
local copy = {}
|
||||
for tractorId, wagonId in pairs(wagons or {}) do copy[tractorId] = wagonId end
|
||||
return copy
|
||||
end
|
||||
|
||||
function ADRuns.serializeWagons(wagons)
|
||||
local keys = {}
|
||||
for tractorId in pairs(wagons or {}) do table.insert(keys, tractorId) end
|
||||
table.sort(keys)
|
||||
local parts = {}
|
||||
for _, tractorId in ipairs(keys) do table.insert(parts, tractorId .. "=" .. wagons[tractorId]) end
|
||||
return table.concat(parts, ";")
|
||||
end
|
||||
|
||||
function ADRuns.parseWagons(text)
|
||||
local wagons = {}
|
||||
for tractorId, wagonId in string.gmatch(tostring(text or ""), "([^=;]+)=([^=;]+)") do wagons[tractorId] = wagonId end
|
||||
return wagons
|
||||
end
|
||||
|
||||
local function copyRun(run)
|
||||
return {
|
||||
id = run.id,
|
||||
@@ -51,6 +76,7 @@ local function copyRun(run)
|
||||
products = copyProducts(run.products),
|
||||
sellAtPercent = run.sellAtPercent,
|
||||
storeAbovePercent = run.storeAbovePercent,
|
||||
sellAbovePercent = run.sellAbovePercent,
|
||||
collectRound = run.collectRound,
|
||||
targetWayPointId = run.targetWayPointId,
|
||||
targetWayPointIds = copyList(run.targetWayPointIds),
|
||||
@@ -60,6 +86,8 @@ local function copyRun(run)
|
||||
loops = run.loops,
|
||||
returnBelowPercent = run.returnBelowPercent,
|
||||
vehicleIds = copyList(run.vehicleIds),
|
||||
stopped = run.stopped == true,
|
||||
wagons = ADRuns.copyWagons(run.wagons),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -98,6 +126,7 @@ function ADRuns.createRun(store, name, kind)
|
||||
products = {},
|
||||
sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT,
|
||||
storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT,
|
||||
sellAbovePercent = ADRuns.DEFAULT_SELL_ABOVE_PERCENT,
|
||||
collectRound = false,
|
||||
targetWayPointId = nil,
|
||||
targetWayPointIds = {},
|
||||
@@ -164,6 +193,7 @@ function ADRuns.updateRun(store, runId, changes)
|
||||
if changes.products ~= nil then run.products = copyProducts(changes.products) end
|
||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
||||
if changes.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
|
||||
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
||||
return new
|
||||
end
|
||||
@@ -308,10 +338,43 @@ function ADRuns.unassignVehicle(store, vehicleId)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- traktoren forlader flowet: dens huskede vogn hører ikke længere til flowet
|
||||
if run.wagons ~= nil then run.wagons[vehicleId] = nil end
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
-- Traktorer der ikke findes længere (solgt/byttet) fjernes fra deres flow. exists(vehicleId) -> bool.
|
||||
-- Mangler ALLE flowenes traktorer, er indlæsningen nok ikke færdig/gået galt: intet fjernes.
|
||||
-- -> ny store, {{runName, vehicleId}} over de fjernede
|
||||
function ADRuns.pruneMissingVehicles(store, exists)
|
||||
local missing, total = {}, 0
|
||||
for _, run in ipairs(store.runs) do
|
||||
for _, id in ipairs(run.vehicleIds) do
|
||||
total = total + 1
|
||||
if not exists(id) then table.insert(missing, {runName = run.name, vehicleId = id}) end
|
||||
end
|
||||
end
|
||||
if #missing == 0 or #missing == total then return store, {} end
|
||||
local new = store
|
||||
for _, entry in ipairs(missing) do new = ADRuns.unassignVehicle(new, entry.vehicleId) end
|
||||
return new, missing
|
||||
end
|
||||
|
||||
-- Én besked pr. berørt flow efter pruneMissingVehicles: {runName, removed, left, critical} (critical = flowet
|
||||
-- står uden traktorer og virker ikke). I samme rækkefølge som flowene.
|
||||
function ADRuns.pruneNotices(storeAfter, removed)
|
||||
local byName = {}
|
||||
for _, entry in ipairs(removed or {}) do byName[entry.runName] = (byName[entry.runName] or 0) + 1 end
|
||||
local notices = {}
|
||||
for _, run in ipairs(storeAfter.runs) do
|
||||
if byName[run.name] ~= nil then
|
||||
table.insert(notices, {runName = run.name, removed = byName[run.name], left = #run.vehicleIds, critical = #run.vehicleIds == 0})
|
||||
end
|
||||
end
|
||||
return notices
|
||||
end
|
||||
|
||||
-- En traktor er kun i ét run. Returnerer ny store + id på det run den blev flyttet FRA (nil = ingen).
|
||||
function ADRuns.assignVehicle(store, runId, vehicleId)
|
||||
local previous = ADRuns.findRunByVehicle(store, vehicleId)
|
||||
@@ -364,7 +427,10 @@ function ADRuns.toRecords(store)
|
||||
products = ADRuns.serializeProducts(run.products),
|
||||
sellAt = run.sellAtPercent,
|
||||
storeAbove = run.storeAbovePercent,
|
||||
sellAbove = run.sellAbovePercent or 0,
|
||||
collectRound = run.collectRound and 1 or 0,
|
||||
stopped = run.stopped and 1 or 0,
|
||||
wagons = ADRuns.serializeWagons(run.wagons),
|
||||
}
|
||||
end
|
||||
return records
|
||||
@@ -407,6 +473,7 @@ function ADRuns.fromRecords(records)
|
||||
products = ADRuns.parseProducts(record.products),
|
||||
sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT),
|
||||
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT),
|
||||
collectRound = tonumber(record.collectRound) == 1,
|
||||
targetWayPointId = targets[1],
|
||||
targetWayPointIds = targets,
|
||||
@@ -416,6 +483,8 @@ function ADRuns.fromRecords(records)
|
||||
loops = clampLoops(record.loops),
|
||||
returnBelowPercent = clampPercent(record.returnBelow),
|
||||
vehicleIds = copyList(record.vehicleIds),
|
||||
stopped = tonumber(record.stopped) == 1,
|
||||
wagons = ADRuns.parseWagons(record.wagons),
|
||||
})
|
||||
store.nextId = math.max(store.nextId, id + 1)
|
||||
end
|
||||
|
||||
@@ -86,10 +86,11 @@ end
|
||||
|
||||
function ADRunsController.deleteRun(runId, adEnv)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if run ~= nil and adEnv ~= nil then
|
||||
if run ~= nil and adEnv ~= nil and not run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet")
|
||||
end
|
||||
if ADFlowWagons ~= nil then pcall(ADFlowWagons.sendHome, adEnv, run) end
|
||||
end
|
||||
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
|
||||
end
|
||||
@@ -256,6 +257,11 @@ function ADRunsController.applyVehicleNamesThrottled(adEnv, dt)
|
||||
if nameTimerMs < ADRunsController.NAME_INTERVAL_MS then return end
|
||||
nameTimerMs = 0
|
||||
ADRunsController.applyVehicleNames(adEnv)
|
||||
-- markmaskiner uden eget navn (stadig modelnavnet) får et rollenavn
|
||||
if ADFieldWork ~= nil and ADFieldWork.autoNameFleet ~= nil and ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||
local ok, err = pcall(ADFieldWork.autoNameFleet, adEnv)
|
||||
if not ok then Logging.warning(ADRunsController.LOG_PREFIX .. "navne til markflåden fejlede: %s", tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- varer ----
|
||||
@@ -372,24 +378,49 @@ function ADRunsController.chooseTarget(adEnv, run, vehicle)
|
||||
end
|
||||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local bestId, bestNeed = nil, nil
|
||||
local bestId, bestNeed, bestInbound = nil, nil, 0
|
||||
for _, entry in ipairs(getTargetStations(adEnv, run, farmId)) do
|
||||
-- læs andre traktorer er på vej med til stalden, tæller med — så fordeler de sig på staldene
|
||||
local pending = ADSmartPickup.getPending ~= nil and ADSmartPickup.getPending(entry.wayPointId, vehicle) or nil
|
||||
local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId, pending)
|
||||
local need = ranked[1]
|
||||
if need ~= nil and need.freeSpace >= (ADSupplyPlanner.MIN_FREE_LITERS or 1000) then
|
||||
-- færrest andre vogne på vej først: et drop (drive-on-bunker, trug) tager én ad gangen, så tre
|
||||
-- vogne mod det mindst fyldte mål holder i kø mens de andre mål står tomme (in-game 26/9)
|
||||
local inbound = ADSmartPickup.countEnRoute ~= nil and ADSmartPickup.countEnRoute(entry.wayPointId, vehicle) or 0
|
||||
local isBetter = bestNeed == nil
|
||||
or (need.fits == true and bestNeed.fits ~= true)
|
||||
or (need.fits == bestNeed.fits and need.score > bestNeed.score)
|
||||
or inbound < bestInbound
|
||||
or (inbound == bestInbound and need.fits == true and bestNeed.fits ~= true)
|
||||
or (inbound == bestInbound and need.fits == bestNeed.fits and need.score > bestNeed.score)
|
||||
if isBetter then
|
||||
bestId, bestNeed = entry.wayPointId, need
|
||||
bestId, bestNeed, bestInbound = entry.wayPointId, need, inbound
|
||||
end
|
||||
end
|
||||
end
|
||||
return bestId or targets[1]
|
||||
end
|
||||
|
||||
-- AutoDrive genstarter aktive traktorer efter indlæsning uden om run-starten: fyldte run-traktorer
|
||||
-- registreres "på vej" til deres nuværende mål (ellers ser mål-valget 0 vogne på vej overalt).
|
||||
ADRunsController.ADOPT_INTERVAL_MS = 5000
|
||||
local adoptTimerMs = 0
|
||||
function ADRunsController.adoptLoadedThrottled(adEnv, dt)
|
||||
adoptTimerMs = adoptTimerMs + (dt or 0)
|
||||
if adEnv == nil or adoptTimerMs < ADRunsController.ADOPT_INTERVAL_MS then return end
|
||||
adoptTimerMs = 0
|
||||
for _, vehicle in pairs(getFarmVehiclesById(adEnv)) do
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if run ~= nil and (run.kind or "supply") == "supply" and stateModule ~= nil and stateModule:isActive()
|
||||
and ADSmartPickup.getEnRoute(vehicle) == nil then
|
||||
local second = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
|
||||
if second ~= nil and ADSmartPickup.registerCargo(adEnv, vehicle, second.id, ADRunsController.getRunIngredients(adEnv, run, vehicle)) then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "'%s' har læs på vej til %s (overtaget efter start/indlæsning)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(second.name or second.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fire traktorer kan hedde det samme; løbenummeret er pladsen i farmens AD-køretøjsliste.
|
||||
function ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local label, model = tostring(ADRuns.vehicleLabel(vehicle)), tostring(vehicle:getName())
|
||||
@@ -812,7 +843,9 @@ function ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
return result
|
||||
end
|
||||
if not intersects(ADRunsController.getRunIngredients(adEnv, run, vehicle), carriable) then
|
||||
result.reason = "cannotCarry"
|
||||
-- AutoLoad slået til: AutoDrive regner vognen som ren palle-/ballevogn (in-game 27/9: tipvogn med UAL
|
||||
-- aktiv blev afvist i Goat Food som "kan ikke bære") — sig det, så spilleren kan slå det fra
|
||||
result.reason = result.isAutoload and "autoloadOn" or "cannotCarry"
|
||||
return result
|
||||
end
|
||||
if isFarFromNetwork(adEnv, vehicle) then
|
||||
@@ -886,6 +919,7 @@ local function setSetting(adEnv, vehicle, settingName, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
ADRunsController.setSetting = setSetting
|
||||
|
||||
-- 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)
|
||||
@@ -969,6 +1003,10 @@ local function applyRunToVehicle(adEnv, run, vehicle, ingredients)
|
||||
local targetId = ADRunsController.chooseTarget(adEnv, run, vehicle) or run.targetWayPointId
|
||||
local dump = ADRunsController.findWrongCargoDump(adEnv, run, vehicle, targetId)
|
||||
stateModule:setSecondMarkerByWayPointId(targetId)
|
||||
-- allerede fyldt: registrér læsset på vej, så næste traktor i samme start vælger et andet drop
|
||||
if dump == nil and ADSmartPickup.registerCargo ~= nil then
|
||||
ADSmartPickup.registerCargo(adEnv, vehicle, targetId, ingredients)
|
||||
end
|
||||
if dump ~= nil then
|
||||
if ADUnloadWait ~= nil and ADUnloadWait.setDumpTarget ~= nil then
|
||||
ADUnloadWait.setDumpTarget(vehicle, dump.marker.id)
|
||||
@@ -1035,43 +1073,174 @@ end
|
||||
|
||||
-- Pr. tildelt traktor: validér, skriv AD-tilstand, start. Afviste traktorer springes over
|
||||
-- og logges med årsag (samme nøgle som fanen viser).
|
||||
-- Start én traktor i et run: valider, anvend run'et og start AutoDrive. Returnerer resultat-posten.
|
||||
function ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
-- solo-traktor med en husket vogn: hent den først (valideringen ville afvise en traktor uden vogn)
|
||||
if ADFlowWagons ~= nil then
|
||||
local okWagon, handled, wagonResult = pcall(ADFlowWagons.beforeStart, adEnv, run, vehicle)
|
||||
if okWagon and handled then return wagonResult end
|
||||
if not okWagon then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogn: %s", tostring(handled)) end
|
||||
end
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
|
||||
if not validation.ok then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
|
||||
return result
|
||||
end
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
|
||||
if ok then
|
||||
ADRunsController.runningRuns[run.id] = true
|
||||
local second = vehicle.ad.stateModule.getSecondMarker ~= nil and vehicle.ad.stateModule:getSecondMarker() or nil
|
||||
local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1]))
|
||||
or ("markør " .. tostring(second ~= nil and second.id or run.targetWayPointId))
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), target)
|
||||
else
|
||||
result.ok, result.reason = false, "error"
|
||||
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADRunsController.startRun(adEnv, runId)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local results = {}
|
||||
if run == nil then
|
||||
return results
|
||||
end
|
||||
run.stopped = false
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
local started = 0
|
||||
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, missingBuilding = validation.missingBuilding}
|
||||
if not validation.ok then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
|
||||
end
|
||||
if validation.ok then
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
|
||||
if ok then
|
||||
started = started + 1
|
||||
local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1])) or ("markør " .. tostring(run.targetWayPointId))
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), target)
|
||||
else
|
||||
result.ok, result.reason = false, "error"
|
||||
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
end
|
||||
table.insert(results, result)
|
||||
table.insert(results, ADRunsController.startVehicle(adEnv, run, vehicle))
|
||||
end
|
||||
end
|
||||
if started > 0 then
|
||||
ADRunsController.runningRuns[runId] = true
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
-- ---- Start alle i kø ----
|
||||
-- Én traktor ad gangen med STAGGER_MS mellemrum: ellers kører de sammen fra parkeringspladsen (in-game 27/9).
|
||||
ADRunsController.STAGGER_MS = 10000
|
||||
ADRunsController.startQueue = {}
|
||||
local staggerTimerMs = 0
|
||||
|
||||
function ADRunsController.clearStartQueue()
|
||||
ADRunsController.startQueue = {}
|
||||
end
|
||||
|
||||
-- Lægger alle traktorer i flows der ikke kører i kø; ugyldige (fx uden vogn) springes over med det samme.
|
||||
-- Returnerer {queued, alreadyRunning, skipped = {resultater}}.
|
||||
function ADRunsController.queueStartAll(adEnv)
|
||||
local summary = {queued = 0, alreadyRunning = 0, skipped = {}}
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
local queue = {}
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if #run.vehicleIds > 0 then
|
||||
if ADRunsController.getRunStatus(adEnv, run).isRunning then
|
||||
summary.alreadyRunning = summary.alreadyRunning + 1
|
||||
else
|
||||
run.stopped = false
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
if vehicle ~= nil then
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
-- solo-traktor med husket vogn: henter den ved start (valideringen ser den uden vogn)
|
||||
if validation.ok or (ADFlowWagons ~= nil and (validation.reason == "noTrailer" or ADFlowWagons.hasWagonFor(run, vehicleId))) then
|
||||
table.insert(queue, {runId = run.id, vehicleId = vehicleId})
|
||||
else
|
||||
table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ADRunsController.startQueue = queue
|
||||
ADRunsController.startQueueTotal = #queue
|
||||
staggerTimerMs = ADRunsController.STAGGER_MS -- første startes ved næste tick
|
||||
summary.queued = #queue
|
||||
return summary
|
||||
end
|
||||
|
||||
-- Stop alt og parkér (Oversigt): flows, markarbejde og marksilo-lån stoppes, køretøjerne sendes til deres
|
||||
-- AD-parkeringspunkt, og moden sættes på pause (gemmes), så intet starter igen før "Start alle".
|
||||
-- Høstere med manuel transport parkeres ikke (må ikke køre på vejen med skærebord). Returnerer
|
||||
-- {stopped, parked, noPark = {navne}, manual = {navne}}.
|
||||
function ADRunsController.stopAllAndPark(adEnv)
|
||||
local summary = {stopped = 0, parked = 0, noPark = {}, manual = {}}
|
||||
ADRunsController.clearStartQueue()
|
||||
local toPark, seen = {}, {}
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
if vehicle ~= nil and not seen[vehicle] then
|
||||
seen[vehicle] = true
|
||||
if isVehicleActive(vehicle) then
|
||||
stopVehicle(vehicle)
|
||||
summary.stopped = summary.stopped + 1
|
||||
end
|
||||
table.insert(toPark, vehicle)
|
||||
end
|
||||
end
|
||||
ADRunsController.runningRuns[run.id] = nil
|
||||
run.stopped = true
|
||||
end
|
||||
if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then
|
||||
for _, stopped in ipairs(ADFieldWork.stopAllAssignments(adEnv) or {}) do
|
||||
summary.stopped = summary.stopped + 1
|
||||
if stopped.pooled then
|
||||
summary.parked = summary.parked + 1
|
||||
elseif stopped.manual then
|
||||
table.insert(summary.manual, tostring(ADRuns.vehicleLabel(stopped.vehicle)))
|
||||
elseif not seen[stopped.vehicle] then
|
||||
seen[stopped.vehicle] = true
|
||||
table.insert(toPark, stopped.vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
if ADFieldSilos ~= nil and ADFieldSilos.stopAll ~= nil then pcall(ADFieldSilos.stopAll, adEnv) end
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end
|
||||
for _, vehicle in ipairs(toPark) do
|
||||
if ADHitch ~= nil and ADHitch.stow ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil then ADHitch.stow(vehicle, "stop alt") end
|
||||
-- flow-traktor: tøm vognen, sæt den af på sin plads og kør hjem (ADFlowWagons klarer hele turen)
|
||||
local okHome, pooled = false, false
|
||||
if ADFlowWagons ~= nil and ADRunsController.getRunForVehicle(vehicle) ~= nil then
|
||||
okHome = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle)
|
||||
pooled = okHome
|
||||
elseif ADHitch ~= nil then
|
||||
okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
|
||||
end
|
||||
local park = not (okHome and pooled) and ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
|
||||
if okHome and pooled then
|
||||
summary.parked = summary.parked + 1
|
||||
elseif park ~= nil then
|
||||
local ok = pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil)
|
||||
if ok then summary.parked = summary.parked + 1 end
|
||||
else
|
||||
table.insert(summary.noPark, tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
end
|
||||
end
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "Stop alt og parkér: %d stoppet, %d til parkering, %d uden parkeringspunkt, %d høstere hentes manuelt — pause",
|
||||
summary.stopped, summary.parked, #summary.noPark, #summary.manual)
|
||||
return summary
|
||||
end
|
||||
|
||||
-- Starter næste traktor i køen, når der er gået STAGGER_MS siden forrige.
|
||||
function ADRunsController.processStartQueue(adEnv, dt)
|
||||
if #ADRunsController.startQueue == 0 then return end
|
||||
staggerTimerMs = staggerTimerMs + (dt or 0)
|
||||
if staggerTimerMs < ADRunsController.STAGGER_MS then return end
|
||||
staggerTimerMs = 0
|
||||
local item = table.remove(ADRunsController.startQueue, 1)
|
||||
local run = ADRuns.getRun(ADRunsController.store, item.runId)
|
||||
local vehicle = run ~= nil and getFarmVehiclesById(adEnv)[item.vehicleId] or nil
|
||||
if run ~= nil and vehicle ~= nil and not isVehicleActive(vehicle) then
|
||||
ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
end
|
||||
end
|
||||
|
||||
-- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job —
|
||||
-- den stoppes, så den ikke fortsætter uden run. Returnerer true hvis den blev stoppet.
|
||||
function ADRunsController.stopIfRunning(vehicle, reason)
|
||||
@@ -1089,6 +1258,11 @@ function ADRunsController.stopRun(adEnv, runId)
|
||||
if run == nil then
|
||||
return stopped
|
||||
end
|
||||
-- allerede stoppet: traktorerne kan være lånt ud af maskinpuljen — de må ikke trækkes væk midt i et job
|
||||
if run.stopped then
|
||||
ADRunsController.runningRuns[runId] = nil
|
||||
return stopped
|
||||
end
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
@@ -1099,6 +1273,12 @@ function ADRunsController.stopRun(adEnv, runId)
|
||||
end
|
||||
end
|
||||
ADRunsController.runningRuns[runId] = nil
|
||||
-- stoppet flow: vognene på plads og traktorerne hjem (ledige i maskinpuljen, vognene er stadig flowets)
|
||||
run.stopped = true
|
||||
if ADFlowWagons ~= nil then
|
||||
local okHome, err = pcall(ADFlowWagons.sendHome, adEnv, run)
|
||||
if not okHome then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogne hjem: %s", tostring(err)) end
|
||||
end
|
||||
return stopped
|
||||
end
|
||||
|
||||
@@ -1113,11 +1293,15 @@ function ADRunsController.getRunStatus(adEnv, run)
|
||||
running = running + 1
|
||||
end
|
||||
end
|
||||
local isRunning = ADRunsController.runningRuns[run.id] == true and running > 0
|
||||
-- startet fra fanen i denne session ELLER ikke stoppet af spilleren (efter genindlæsning er runningRuns tom,
|
||||
-- men traktorerne kører videre — in-game 29/9: listen viste [Start] ved kørende flows)
|
||||
local isRunning = (ADRunsController.runningRuns[run.id] == true or run.stopped == false) and running > 0
|
||||
if ADRunsController.runningRuns[run.id] and running == 0 then
|
||||
ADRunsController.runningRuns[run.id] = nil
|
||||
end
|
||||
return {running = running, total = #run.vehicleIds, isRunning = isRunning}
|
||||
-- canStop: Stop-knappen — også når et ikke-stoppet flow holder stille med vogne på / henter / venter på sin vogn
|
||||
local canStop = isRunning or (not run.stopped and ADFlowWagons ~= nil and ADFlowWagons.hasActivity(run, vehiclesById))
|
||||
return {running = running, total = #run.vehicleIds, isRunning = isRunning, canStop = canStop}
|
||||
end
|
||||
|
||||
-- Start alle (Oversigt): flows der allerede kører røres ikke, så et halvt kørende gårdbillede kan
|
||||
@@ -1287,6 +1471,12 @@ function ADRunsController.save()
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||
pcall(ADFieldJobs.save)
|
||||
end
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver
|
||||
-- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem)
|
||||
if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
|
||||
-- maskiner hvor CP kører nu, så de kan genstartes efter indlæsning (samme gem-regel som navnefilen)
|
||||
if ADCpResume ~= nil and ADFieldSilos ~= nil and ADFieldSilos.adEnv ~= nil then pcall(ADCpResume.save, ADFieldSilos.adEnv) end
|
||||
return saved
|
||||
end
|
||||
|
||||
@@ -1298,7 +1488,10 @@ function ADRunsController.load(adEnv)
|
||||
return
|
||||
end
|
||||
ADRunsController.store = store
|
||||
if ADNames ~= nil then pcall(ADNames.load) end
|
||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
||||
if ADCpResume ~= nil then pcall(ADCpResume.load) end
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, run in ipairs(store.runs) do
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
@@ -1309,6 +1502,27 @@ function ADRunsController.load(adEnv)
|
||||
end
|
||||
end
|
||||
|
||||
-- Én gang efter indlæsning (køretøjerne er klar): traktorer der ikke findes længere fjernes fra flowene.
|
||||
function ADRunsController.pruneMissingVehicles(adEnv)
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
if next(vehiclesById) == nil then return end
|
||||
local store, removed = ADRuns.pruneMissingVehicles(ADRunsController.store, function(id) return vehiclesById[id] ~= nil end)
|
||||
if #removed == 0 then return end
|
||||
ADRunsController.store = store
|
||||
for _, entry in ipairs(removed) do
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': køretøj %s findes ikke længere — fjernet fra flowet", tostring(entry.runName), tostring(entry.vehicleId))
|
||||
end
|
||||
ADRunsController.save()
|
||||
-- besked på skærmen (højre side); rød når flowet står uden traktorer
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil and g_i18n ~= nil then
|
||||
for _, notice in ipairs(ADRuns.pruneNotices(store, removed)) do
|
||||
local text = notice.critical and string.format(g_i18n:getText("spu_prune_empty"), notice.runName)
|
||||
or string.format(g_i18n:getText("spu_prune_removed"), notice.runName, notice.removed, notice.left)
|
||||
ADFieldJobs.notify(text, notice.critical)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADRunsController.installSaveHook()
|
||||
if ItemSystem == nil or ItemSystem.save == nil or Utils == nil then
|
||||
return
|
||||
|
||||
@@ -46,7 +46,11 @@ function ADRunsStorage.writeRecords(xmlFile, records)
|
||||
xmlFile:setString(key .. "#products", record.products or "")
|
||||
xmlFile:setInt(key .. "#sellAt", record.sellAt or ADRuns.DEFAULT_SELL_AT_PERCENT)
|
||||
xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT)
|
||||
xmlFile:setInt(key .. "#sellAbove", record.sellAbove or 0)
|
||||
xmlFile:setInt(key .. "#collectRound", record.collectRound or 0)
|
||||
-- maskinpulje: flowet stoppet + husket vogn pr. traktor
|
||||
xmlFile:setInt(key .. "#stopped", record.stopped or 0)
|
||||
xmlFile:setString(key .. "#wagons", record.wagons or "")
|
||||
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||
end
|
||||
@@ -74,7 +78,10 @@ function ADRunsStorage.readRecords(xmlFile)
|
||||
products = xmlFile:getString(key .. "#products"),
|
||||
sellAt = xmlFile:getInt(key .. "#sellAt"),
|
||||
storeAbove = xmlFile:getInt(key .. "#storeAbove"),
|
||||
sellAbove = xmlFile:getInt(key .. "#sellAbove"),
|
||||
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
||||
stopped = xmlFile:getInt(key .. "#stopped"),
|
||||
wagons = xmlFile:getString(key .. "#wagons"),
|
||||
vehicleIds = {},
|
||||
}
|
||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||
|
||||
@@ -10,14 +10,29 @@
|
||||
|
||||
ADSmartPickup = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADSmartPickup.dir = g_currentModDirectory
|
||||
if ADSmartPickup.dir ~= nil and source ~= nil then
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupStorageView.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupAttentionFlow.lua")
|
||||
end
|
||||
|
||||
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
|
||||
@@ -216,14 +231,21 @@ local function getUnloadStationAtMarker(adEnv, wayPoint, farmId)
|
||||
end
|
||||
|
||||
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
|
||||
-- Markør -> station er stabil (stationer og markører flytter sig sjældent): caches 10 s (ADCache).
|
||||
ADSmartPickup.STATION_CACHE_MS = 30000
|
||||
|
||||
function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
end)
|
||||
end
|
||||
|
||||
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
end)
|
||||
end
|
||||
|
||||
-- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen".
|
||||
@@ -243,7 +265,7 @@ function ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, m
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
return best, best ~= nil and bestDistance or nil
|
||||
end
|
||||
|
||||
-- Siloens EGET indhold. Udvidelses-siloer (isExtension) deler lager med naboer
|
||||
@@ -352,18 +374,26 @@ end
|
||||
-- Bedste kilde der har varen: mindst fyldte først for lagervarer, fyldeste først for
|
||||
-- varer hvis tanke løber fulde (ADSupplyPlanner). Siloer med varen i EGET lager går
|
||||
-- altid forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil.
|
||||
local function findBestSource(markerStations, fillTypes, farmId)
|
||||
local getLoadCapacity -- defineret længere nede (læs-kapacitet til kildevalget)
|
||||
|
||||
local function findBestSource(markerStations, fillTypes, farmId, loadCapacity)
|
||||
local prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes))
|
||||
local ample = ADSupplyPlanner.ampleLevel(loadCapacity)
|
||||
local best = {[true] = {}, [false] = {}}
|
||||
for _, entry in ipairs(markerStations) do
|
||||
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
|
||||
if level >= ADSmartPickup.MIN_USEFUL_LITERS and not isProtectedProductionInput(entry.station, fillTypes)
|
||||
and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest) then
|
||||
and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest, ample) then
|
||||
best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn}
|
||||
end
|
||||
end
|
||||
|
||||
local choice = best[true].marker ~= nil and best[true] or best[false]
|
||||
-- eget lager går forud — men ikke en næsten tom egen tank frem for et rigeligt nabolager
|
||||
local own, other = best[true], best[false]
|
||||
local choice = own
|
||||
if own.marker == nil or (not prefersFullest and other.marker ~= nil and own.level < ample and other.level >= ample) then
|
||||
choice = other
|
||||
end
|
||||
if choice.marker == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -392,7 +422,7 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
|
||||
return nil
|
||||
end
|
||||
local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)
|
||||
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId)
|
||||
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId, getLoadCapacity(adEnv, driver))
|
||||
end
|
||||
|
||||
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
|
||||
@@ -452,7 +482,7 @@ end
|
||||
local factoryFallbackLogged = setmetatable({}, {__mode = "k"})
|
||||
local function logFactoryFallbackOnce(driver, station)
|
||||
local placeable = station.owningPlaceable
|
||||
local name = placeable ~= nil and placeable.getName ~= nil and placeable:getName() or "fabrik"
|
||||
local name = (ADNames ~= nil and ADNames.forPlaceable(placeable)) or (placeable ~= nil and placeable.getName ~= nil and placeable:getName()) or "fabrik"
|
||||
local message = string.format("'%s': %s har ingen ønskede produktioner (tændte eller auto-slukkede pga. mangel) med varer vognen kan bære; bruger dit varevalg", tostring(ADRuns.vehicleLabel(driver)), tostring(name))
|
||||
if factoryFallbackLogged[driver] ~= message then
|
||||
factoryFallbackLogged[driver] = message
|
||||
@@ -639,6 +669,20 @@ function ADSmartPickup.setEnRoute(vehicle, wayPointId, fillType, liters)
|
||||
enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters}
|
||||
end
|
||||
|
||||
function ADSmartPickup.getEnRoute(vehicle)
|
||||
return enRoute[vehicle]
|
||||
end
|
||||
|
||||
-- Fyldt run-traktor -> "på vej" til mål targetId, så mål-valget for de næste fordeler dem.
|
||||
function ADSmartPickup.registerCargo(adEnv, vehicle, targetId, ingredients)
|
||||
if targetId == nil or ADUnloadWait == nil or ADUnloadWait.getRestLoad == nil then return false end
|
||||
local restFillType, restLiters = ADUnloadWait.getRestLoad(adEnv, vehicle)
|
||||
local fillType, liters = ADSupplyPlanner.cargoFor(restFillType, restLiters, ingredients)
|
||||
if fillType == nil then return false end
|
||||
ADSmartPickup.setEnRoute(vehicle, targetId, fillType, liters)
|
||||
return true
|
||||
end
|
||||
|
||||
function ADSmartPickup.clearEnRoute(vehicle)
|
||||
enRoute[vehicle] = nil
|
||||
end
|
||||
@@ -658,6 +702,15 @@ function ADSmartPickup.getPending(wayPointId, excludeVehicle)
|
||||
return pending
|
||||
end
|
||||
|
||||
-- Antal ANDRE traktorer på vej til målet (målvalget fordeler vognene på flere drops).
|
||||
function ADSmartPickup.countEnRoute(wayPointId, excludeVehicle)
|
||||
local count = 0
|
||||
for vehicle, entry in pairs(enRoute) do
|
||||
if vehicle ~= excludeVehicle and entry.wayPointId == wayPointId then count = count + 1 end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- pending (valgfri): læs på vej til målet, lægges oven i (fanen og mål-valget mellem stalde).
|
||||
function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
||||
local needs = getTargetNeeds(station, fillTypes, farmId, {})
|
||||
@@ -676,7 +729,7 @@ function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
||||
end
|
||||
|
||||
-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den.
|
||||
local function getLoadCapacity(adEnv, driver)
|
||||
getLoadCapacity = function(adEnv, driver)
|
||||
if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -744,7 +797,7 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetWayPoi
|
||||
-- bedste foder først: lavere foder kun når truget er nødlidende (ADSupplyPlanner.isFoodAllowed)
|
||||
local bestFood = ADSupplyPlanner.bestFoodPriority ~= nil and ADSupplyPlanner.bestFoodPriority(ranked) or nil
|
||||
for _, need in ipairs(ranked) do
|
||||
local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId) or nil
|
||||
local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId, loadCapacity) or nil
|
||||
if choice ~= nil then
|
||||
return {
|
||||
markerIndex = choice.markerIndex,
|
||||
@@ -1095,6 +1148,9 @@ local function installHook(adEnv)
|
||||
if ADOutbound ~= nil and ADOutbound.install ~= nil then
|
||||
ADOutbound.install(adEnv)
|
||||
end
|
||||
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
||||
ADHitchCreep.install(adEnv)
|
||||
end
|
||||
|
||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
||||
end
|
||||
@@ -1146,10 +1202,33 @@ local function insertInGameMenuPage(frame, pageName, iconFile, iconUVs, predicat
|
||||
end
|
||||
inGameMenu[pageName] = frame
|
||||
inGameMenu.pagingElement:addElement(frame)
|
||||
-- Lige over spillets Indstillinger (brugerønske 29/9). Siden flyttes på ALLE tre lister med samme indeks
|
||||
-- (sider, pagerens sider, pageFrames) som FS25_FarmOperationsDashboard — at flytte kun nogle af dem gav
|
||||
-- tidligere forskudt fane↔side. Uden Indstillinger-side: sidst som før.
|
||||
local target = 0
|
||||
for i, child in ipairs(inGameMenu.pagingElement.elements) do
|
||||
if inGameMenu.pageSettings ~= nil and child == inGameMenu.pageSettings then
|
||||
target = i
|
||||
break
|
||||
end
|
||||
end
|
||||
local function moveTo(list, matches)
|
||||
if target == 0 or list == nil then return end
|
||||
for i, child in ipairs(list) do
|
||||
if matches(child) then
|
||||
table.remove(list, i)
|
||||
table.insert(list, target, child)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
moveTo(inGameMenu.pagingElement.elements, function(child) return child == frame end)
|
||||
moveTo(inGameMenu.pagingElement.pages, function(child) return child.element == frame end)
|
||||
inGameMenu.pagingElement:updateAbsolutePosition()
|
||||
inGameMenu.pagingElement:updatePageMapping()
|
||||
inGameMenu:registerPage(frame, position, predicate)
|
||||
inGameMenu:addPageTab(frame, iconFile, iconUVs)
|
||||
moveTo(inGameMenu.pageFrames, function(child) return child == frame end)
|
||||
if frame.updateAbsolutePosition ~= nil then
|
||||
frame:updateAbsolutePosition()
|
||||
end
|
||||
@@ -1184,11 +1263,17 @@ function ADSmartPickup:loadMap()
|
||||
end
|
||||
|
||||
function ADSmartPickup:update(dt)
|
||||
if ADProfiler ~= nil and ADProfiler.installed then
|
||||
local now = ADProfiler.clock()
|
||||
if now ~= nil then pcall(ADProfiler.frame, now) end
|
||||
end
|
||||
if not isHooked then
|
||||
local adEnv = getAutoDriveEnv()
|
||||
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
|
||||
installHook(adEnv)
|
||||
isHooked = true
|
||||
if ADProfiler ~= nil then pcall(ADProfiler.install) end
|
||||
if ADCache ~= nil then ADCache.enabled = true end
|
||||
if ADRunsController ~= nil then
|
||||
pcall(ADRunsController.load, adEnv)
|
||||
pcall(ADRunsController.installSaveHook)
|
||||
@@ -1206,6 +1291,9 @@ function ADSmartPickup:update(dt)
|
||||
if ADOutbound ~= nil and ADOutbound.releaseInactive ~= nil then
|
||||
pcall(ADOutbound.releaseInactive)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.processStartQueue ~= nil then
|
||||
pcall(ADRunsController.processStartQueue, getAutoDriveEnv(), dt)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
|
||||
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
|
||||
end
|
||||
@@ -1215,6 +1303,26 @@ function ADSmartPickup:update(dt)
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
||||
end
|
||||
end
|
||||
if ADFieldSilos ~= nil then
|
||||
local okSilos, errSilos = pcall(ADFieldSilos.tick, getAutoDriveEnv(), dt)
|
||||
if not okSilos then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
||||
end
|
||||
end
|
||||
if ADFlowWagons ~= nil then
|
||||
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
|
||||
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
|
||||
end
|
||||
if ADHitch ~= nil then
|
||||
local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt)
|
||||
if not okHitch then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kobling fejlede: %s", tostring(errHitch))
|
||||
end
|
||||
end
|
||||
if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
|
||||
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
|
||||
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
|
||||
end
|
||||
local ok, err = pcall(restoreStoppedVehicles)
|
||||
if not ok then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
ADSources = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADSources.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
|
||||
ADSources.MAX_DISTANCE = 40
|
||||
@@ -17,10 +24,6 @@ local function getHusbandryPalletSpawners(placeable)
|
||||
return spec ~= nil and spec.fillTypeIndexToPalletSpawner or nil
|
||||
end
|
||||
|
||||
local function firstSpawnerNode(spawner)
|
||||
return spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode or nil
|
||||
end
|
||||
|
||||
|
||||
local function distance2(ax, az, bx, bz)
|
||||
local dx, dz = ax - bx, az - bz
|
||||
@@ -36,6 +39,8 @@ local function getNodePosition(node)
|
||||
end
|
||||
|
||||
local function getPlaceableName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then
|
||||
@@ -123,7 +128,8 @@ local function findPlaceableNear(wayPoint, farmId, predicate)
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
-- (afstand i meter som 2. værdi)
|
||||
return best, best ~= nil and math.sqrt(bestDistance) or nil
|
||||
end
|
||||
|
||||
local function isObjectStorage(placeable)
|
||||
@@ -136,6 +142,12 @@ end
|
||||
|
||||
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
|
||||
function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
return cacheGet("identify", tostring(wayPointId) .. ":" .. tostring(farmId), 10000, function()
|
||||
return ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||||
end)
|
||||
end
|
||||
|
||||
function ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
if wayPoint == nil then
|
||||
return nil
|
||||
@@ -164,9 +176,15 @@ function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
end
|
||||
-- Det strenge opslag (trigger bag næsen) passer til silorækker; en mælketank eller fabrik ligger
|
||||
-- ofte ved siden af markøren. Løst opslag inden for 40 m — men aldrig for siloer.
|
||||
-- stald med egne pallespawnere (hønsehus: æg) — markøren står ved spawneren, ingen læssestation
|
||||
local palletBarn, palletBarnDistance = findPlaceableNear(wayPoint, farmId, function(placeable) return placeable.getHusbandryFillLevel ~= nil and getHusbandryPalletSpawners(placeable) ~= nil end)
|
||||
if placeable == nil and ADSmartPickup.getLoadStationNearWayPointId ~= nil then
|
||||
local loose = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE)
|
||||
local loose, looseDistance = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE)
|
||||
local loosePlaceable = loose ~= nil and loose.owningPlaceable or nil
|
||||
-- et nærmere hønsehus vinder over en fjernere læssestation (gyllestation 23 m fra "Æg Pickup 1")
|
||||
if palletBarn ~= nil and looseDistance ~= nil and palletBarnDistance ~= nil and palletBarnDistance < looseDistance then
|
||||
loosePlaceable = nil
|
||||
end
|
||||
if loosePlaceable ~= nil and loosePlaceable.spec_silo == nil then
|
||||
if getProductionPoint(loosePlaceable) ~= nil then
|
||||
return {kind = "factory", placeable = loosePlaceable, station = loose, wayPointId = wayPointId, name = getPlaceableName(loosePlaceable, markerName)}
|
||||
@@ -175,8 +193,6 @@ function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- stald med egne pallespawnere (hønsehus: æg) — markøren står ved spawneren, ingen læssestation
|
||||
local palletBarn = findPlaceableNear(wayPoint, farmId, function(placeable) return placeable.getHusbandryFillLevel ~= nil and getHusbandryPalletSpawners(placeable) ~= nil end)
|
||||
if palletBarn ~= nil then
|
||||
return {kind = "husbandry", placeable = palletBarn, station = nil, wayPointId = wayPointId, name = getPlaceableName(palletBarn, markerName)}
|
||||
end
|
||||
@@ -241,29 +257,99 @@ local function getPalletContent(vehicle)
|
||||
return nil, 0
|
||||
end
|
||||
|
||||
-- Liter af varen på paller der står ved fabrikkens spawner (pallespawneren tømmer lageret
|
||||
-- ud på jorden løbende, så det er dér varen fysisk er).
|
||||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawnerNode)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil
|
||||
local node = spawnerNode or (spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode) or placeable.rootNode
|
||||
local sx, sz = getNodePosition(node)
|
||||
if sx == nil then
|
||||
return 0
|
||||
-- Spawnerens område som segmenter {ax, az, bx, bz}: hver spawnPlace fra start til slut (startNode/endNode,
|
||||
-- eller PlacementUtil-felterne startX/startZ + dirX/dirZ*width). Et hønsehus har to rækker; måles kun
|
||||
-- til første række, ligger den anden række tættere på naboens start end husets egen.
|
||||
local function spawnerSegments(spawner)
|
||||
local segments = {}
|
||||
for _, place in ipairs(spawner ~= nil and spawner.spawnPlaces or {}) do
|
||||
local ax, az = getNodePosition(place.startNode)
|
||||
local bx, bz = getNodePosition(place.endNode)
|
||||
if ax == nil and place.startX ~= nil and place.startZ ~= nil then
|
||||
ax, az = place.startX, place.startZ
|
||||
if place.dirX ~= nil and place.dirZ ~= nil and place.width ~= nil then
|
||||
bx, bz = ax + place.dirX * place.width, az + place.dirZ * place.width
|
||||
end
|
||||
end
|
||||
if ax ~= nil then table.insert(segments, {ax, az, bx or ax, bz or az}) end
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Kvadreret afstand fra (x, z) til nærmeste punkt på et af segmenterne.
|
||||
local function segmentsDistance2(segments, x, z)
|
||||
local best = math.huge
|
||||
for _, s in ipairs(segments) do
|
||||
local ax, az, bx, bz = s[1], s[2], s[3], s[4]
|
||||
local dx, dz = bx - ax, bz - az
|
||||
local len2 = dx * dx + dz * dz
|
||||
local t = len2 > 0 and math.max(0, math.min(1, ((x - ax) * dx + (z - az) * dz) / len2)) or 0
|
||||
local d = distance2(x, z, ax + dx * t, az + dz * t)
|
||||
if d < best then best = d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Alle pallespawnere der kan lave varen (stalde med spawner for fillType, fabrikkers pallespawner) som
|
||||
-- {placeable, segments}. Står bygningerne tættere end PALLET_RADIUS (hønsehuse på række), tilhører en
|
||||
-- palle den spawner hvis område er nærmest — ellers tæller et tomt hus naboernes paller med.
|
||||
local function findRivalSpawners(fillType)
|
||||
local rivals = {}
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
local husbandrySpawners = getHusbandryPalletSpawners(placeable)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local spawner = husbandrySpawners ~= nil and husbandrySpawners[fillType] or (productionPoint ~= nil and productionPoint.palletSpawner or nil)
|
||||
local segments = spawnerSegments(spawner)
|
||||
if #segments > 0 then table.insert(rivals, {placeable = placeable, segments = segments}) end
|
||||
end
|
||||
return rivals
|
||||
end
|
||||
|
||||
local function isNearestSpawner(rivals, placeable, own, x, z)
|
||||
for _, rival in ipairs(rivals) do
|
||||
if rival.placeable ~= placeable and segmentsDistance2(rival.segments, x, z) < own then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
ADSources.lastPalletNote = {}
|
||||
|
||||
-- Liter af varen på paller ved spawneren (pallespawneren tømmer lageret ud på jorden løbende, så
|
||||
-- det er dér varen fysisk er). spawner = staldens spawner for varen; nil = fabrikkens pallespawner.
|
||||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawner)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local segments = spawnerSegments(spawner or (productionPoint ~= nil and productionPoint.palletSpawner or nil))
|
||||
if #segments == 0 then
|
||||
local rx, rz = getNodePosition(placeable.rootNode)
|
||||
if rx == nil then return 0, 0 end
|
||||
segments = {{rx, rz, rx, rz}}
|
||||
end
|
||||
local rivals = cacheGet("palletSpawners", fillType, 10000, function() return findRivalSpawners(fillType) end)
|
||||
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
|
||||
local total, count = 0, 0
|
||||
local total, count, lost = 0, 0, 0
|
||||
local radius2 = ADSources.PALLET_RADIUS * ADSources.PALLET_RADIUS
|
||||
for _, vehicle in pairs(vehicles) do
|
||||
if isPalletVehicle(vehicle) and (vehicle.getOwnerFarmId == nil or vehicle:getOwnerFarmId() == farmId) then
|
||||
local palletFillType, level = getPalletContent(vehicle)
|
||||
local x, z = getNodePosition(vehicle.rootNode)
|
||||
if palletFillType == fillType and x ~= nil and distance2(sx, sz, x, z) <= radius2 then
|
||||
total = total + level
|
||||
count = count + 1
|
||||
local own = (palletFillType == fillType and x ~= nil) and segmentsDistance2(segments, x, z) or math.huge
|
||||
if own <= radius2 then
|
||||
if isNearestSpawner(rivals, placeable, own, x, z) then
|
||||
total = total + level
|
||||
count = count + 1
|
||||
else
|
||||
lost = lost + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- diagnose: paller inden for rækkevidde, men alle tildelt en nabo (højst én gang pr. minut pr. bygning)
|
||||
local now = g_currentMission ~= nil and g_currentMission.time or nil
|
||||
if count == 0 and lost > 0 and now ~= nil and (ADSources.lastPalletNote[placeable] or -math.huge) + 60000 < now then
|
||||
ADSources.lastPalletNote[placeable] = now
|
||||
Logging.info(ADSources.LOG_PREFIX .. "%s: %d paller inden for %d m tilhører nabo-spawnere (0 egne)", getPlaceableName(placeable, "?"), lost, ADSources.PALLET_RADIUS)
|
||||
end
|
||||
return total, count
|
||||
end
|
||||
|
||||
@@ -395,7 +481,7 @@ local function getHusbandryPalletProducts(source, farmId)
|
||||
local products = {}
|
||||
local spec = source.placeable.spec_husbandryPallets
|
||||
for fillType, spawner in pairs(getHusbandryPalletSpawners(source.placeable) or {}) do
|
||||
local liters, count = getPalletsNearSpawner(source.placeable, fillType, farmId, firstSpawnerNode(spawner))
|
||||
local liters, count = getPalletsNearSpawner(source.placeable, fillType, farmId, spawner)
|
||||
local capacity = spec ~= nil and spec.capacities ~= nil and spec.capacities[fillType] or 0
|
||||
local product = newProduct(fillType, liters, liters, math.max(capacity or 0, liters), true)
|
||||
product.palletLiters, product.palletCount = liters, count
|
||||
@@ -552,6 +638,10 @@ end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||||
function ADSources.getSellMarkers(adEnv, farmId)
|
||||
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
||||
end
|
||||
|
||||
function ADSources.findSellMarkers(adEnv, farmId)
|
||||
local result = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||
@@ -852,16 +942,17 @@ local function addAnimalProduction(rates, placeable)
|
||||
end
|
||||
|
||||
-- {[fillType] = {consumption, production}} i liter pr. døgn for gårdens fabrikker og stalde.
|
||||
function ADSources.getFarmRates(adEnv, farmId)
|
||||
-- feedOnly: kun staldenes forbrug (foder, halm, vand) — marksilo-flowets foderreserve.
|
||||
function ADSources.getFarmRates(adEnv, farmId, feedOnly)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
local rates = {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
if isOwnedBy(placeable, farmId) then
|
||||
if getProductionPoint(placeable) ~= nil then
|
||||
addFactoryRates(rates, placeable)
|
||||
if not feedOnly then addFactoryRates(rates, placeable) end
|
||||
elseif placeable.getHusbandryFillLevel ~= nil then
|
||||
addHusbandryRates(rates, placeable)
|
||||
pcall(addAnimalProduction, rates, placeable)
|
||||
if not feedOnly then pcall(addAnimalProduction, rates, placeable) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -999,6 +1090,10 @@ end
|
||||
-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
|
||||
-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
|
||||
function ADSources.getFarmInventory(adEnv, farmId)
|
||||
return cacheGet("inventory", tostring(farmId), 2000, function() return ADSources.buildFarmInventory(adEnv, farmId) end)
|
||||
end
|
||||
|
||||
function ADSources.buildFarmInventory(adEnv, farmId)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
local result = {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
|
||||
@@ -108,3 +108,215 @@ function ADStorageStats.filterKind(inventory, kind)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- vareoversigt (v1.52): én række pr. vare, der folder sig ud til bygningerne ----
|
||||
|
||||
-- Tærskler for farverne: døgn til tom (gårdens netto), fyldning i % (produceret vare / stald-trug).
|
||||
ADStorageStats.DAYS_RED, ADStorageStats.DAYS_YELLOW = 1, 3
|
||||
ADStorageStats.FULL_RED, ADStorageStats.FULL_YELLOW = 95, 85
|
||||
ADStorageStats.BARN_LOW_RED, ADStorageStats.BARN_LOW_YELLOW = 10, 25
|
||||
-- "Sælg nu": prisen er mindst 90 % af årets top, og partiet er værd at køre med
|
||||
ADStorageStats.SELL_RATIO, ADStorageStats.SELL_MIN_VALUE = 0.9, 10000
|
||||
|
||||
local function percentOf(level, capacity)
|
||||
return (capacity or 0) > 0 and math.floor(100 * (level or 0) / capacity + 0.5) or nil
|
||||
end
|
||||
|
||||
-- inventory (fanens bygninger) -> { {fillType, title, level, capacity, percent, count, isOutput,
|
||||
-- buildings = {{name, placeable, level, capacity, percent, count, slots, isOutput}}} } — varer efter titel,
|
||||
-- bygninger med mest først. count (paller) summeres kun når alle bygninger melder paller.
|
||||
function ADStorageStats.groupByFillType(inventory, titleOf)
|
||||
local byType, groups = {}, {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
for _, item in ipairs(building.entries or {}) do
|
||||
local group = byType[item.fillType]
|
||||
if group == nil then
|
||||
group = {fillType = item.fillType, title = tostring(titleOf(item.fillType) or item.fillType),
|
||||
level = 0, capacity = 0, count = 0, isOutput = false, buildings = {}}
|
||||
byType[item.fillType] = group
|
||||
table.insert(groups, group)
|
||||
end
|
||||
group.level = group.level + (item.level or 0)
|
||||
group.capacity = group.capacity + (item.capacity or 0)
|
||||
group.count = (group.count ~= nil and item.count ~= nil) and group.count + item.count or nil
|
||||
group.isOutput = group.isOutput or item.isOutput == true
|
||||
-- skur uden pladsgrænse har kapacitet 0: så er varens samlede procent ukendt
|
||||
if (item.capacity or 0) <= 0 and (item.level or 0) > 0 then group.unbounded = true end
|
||||
local percent = percentOf(item.level, item.capacity)
|
||||
if item.count ~= nil and (item.slots or 0) > 0 then percent = percentOf(item.count, item.slots) end
|
||||
table.insert(group.buildings, {name = building.name, placeable = building.placeable, level = item.level or 0,
|
||||
capacity = item.capacity or 0, percent = percent, count = item.count, slots = item.slots, isOutput = item.isOutput == true})
|
||||
end
|
||||
end
|
||||
for _, group in ipairs(groups) do
|
||||
group.percent = not group.unbounded and percentOf(group.level, group.capacity) or nil
|
||||
table.sort(group.buildings, function(a, b)
|
||||
if a.level ~= b.level then return a.level > b.level end
|
||||
return tostring(a.name) < tostring(b.name)
|
||||
end)
|
||||
end
|
||||
table.sort(groups, function(a, b)
|
||||
if a.title ~= b.title then return a.title < b.title end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
return groups
|
||||
end
|
||||
|
||||
-- inventory -> {[fillType] = kapacitet i liter} for hele gården
|
||||
function ADStorageStats.farmCapacities(inventory)
|
||||
local totals = {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
for _, entry in ipairs(building.entries or {}) do
|
||||
totals[entry.fillType] = (totals[entry.fillType] or 0) + (entry.capacity or 0)
|
||||
end
|
||||
end
|
||||
return totals
|
||||
end
|
||||
|
||||
function ADStorageStats.emptyLevel(days)
|
||||
if days == nil then return nil end
|
||||
if days < ADStorageStats.DAYS_RED then return "red" end
|
||||
if days < ADStorageStats.DAYS_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Fuldt er kun et problem for noget der bliver produceret (fyldes løbende); en fuld lagersilo er bare lager.
|
||||
function ADStorageStats.fullLevel(percent, producing)
|
||||
if not producing or percent == nil then return nil end
|
||||
if percent >= ADStorageStats.FULL_RED then return "red" end
|
||||
if percent >= ADStorageStats.FULL_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
|
||||
local SEVERITY_RANK = {red = 2, yellow = 1}
|
||||
|
||||
function ADStorageStats.worst(a, b)
|
||||
return (SEVERITY_RANK[a] or 0) >= (SEVERITY_RANK[b] or 0) and a or b
|
||||
end
|
||||
|
||||
-- Én bygnings bjælke: stald-trug der tømmes (ikke mælk/gylle) er rødt når næsten tomt; produceret
|
||||
-- vare/fabriksoutput er rødt når næsten fuldt.
|
||||
function ADStorageStats.buildingLevel(building, kind, isStableOutput)
|
||||
local percent = building.percent
|
||||
if percent == nil then return nil end
|
||||
if kind == "husbandry" and not isStableOutput then
|
||||
if percent <= ADStorageStats.BARN_LOW_RED then return "red" end
|
||||
if percent <= ADStorageStats.BARN_LOW_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
return ADStorageStats.fullLevel(percent, building.isOutput == true or isStableOutput)
|
||||
end
|
||||
|
||||
-- group fra groupByFillType; farm = {level, capacity} for varen på hele gården (nil = kun fanens tal);
|
||||
-- rate = {consumption, production} pr. døgn; quote = {price, top} pr. liter.
|
||||
function ADStorageStats.groupStats(group, farm, rate, quote)
|
||||
local farmLevel = farm ~= nil and farm.level or group.level
|
||||
local farmCapacity = farm ~= nil and farm.capacity or group.capacity
|
||||
local consumption = rate ~= nil and rate.consumption or 0
|
||||
local production = rate ~= nil and rate.production or 0
|
||||
local net = production - consumption
|
||||
local stat = {}
|
||||
for key, value in pairs(group) do stat[key] = value end
|
||||
stat.farmLevel, stat.farmCapacity = farmLevel, farmCapacity
|
||||
stat.consumption, stat.production, stat.net = consumption, production, net
|
||||
stat.days = consumption > production and farmLevel / (consumption - production) or nil
|
||||
stat.daysToFull = net > 0 and (farmCapacity or 0) > 0 and math.max(0, farmCapacity - farmLevel) / net or nil
|
||||
stat.producing = group.isOutput == true or net > 0
|
||||
if quote ~= nil and quote.price ~= nil then
|
||||
stat.valueNow = math.floor(group.level * quote.price + 0.5)
|
||||
stat.valueTop = math.floor(group.level * (quote.top or quote.price) + 0.5)
|
||||
stat.priceRatio = (quote.top or 0) > 0 and quote.price / quote.top or nil
|
||||
end
|
||||
stat.emptyLevel = ADStorageStats.emptyLevel(stat.days)
|
||||
stat.fullLevel = ADStorageStats.fullLevel(group.percent, stat.producing)
|
||||
stat.severity = ADStorageStats.worst(stat.emptyLevel, stat.fullLevel)
|
||||
return stat
|
||||
end
|
||||
|
||||
-- Rangering: rød tom > rød fuld > gul tom > gul fuld > sælg nu (største beløb først).
|
||||
-- Kortest tid til tom / mest fyldt vinder inden for samme trin.
|
||||
local function attentionEntries(stat)
|
||||
local entries = {}
|
||||
if stat.emptyLevel ~= nil then
|
||||
local base = stat.emptyLevel == "red" and 4000 or 2000
|
||||
table.insert(entries, {kind = "empty", level = stat.emptyLevel, stat = stat, score = base - math.min(999, (stat.days or 0) * 100)})
|
||||
end
|
||||
if stat.fullLevel ~= nil then
|
||||
local base = stat.fullLevel == "red" and 3000 or 1000
|
||||
table.insert(entries, {kind = "full", level = stat.fullLevel, stat = stat, score = base + (stat.percent or 0)})
|
||||
end
|
||||
-- sælg kun det gården ikke selv æder af
|
||||
if stat.priceRatio ~= nil and stat.priceRatio >= ADStorageStats.SELL_RATIO and (stat.valueNow or 0) >= ADStorageStats.SELL_MIN_VALUE
|
||||
and (stat.net or 0) >= 0 then
|
||||
table.insert(entries, {kind = "sell", level = "green", stat = stat, score = math.min(999, stat.valueNow / 1e6)})
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function ADStorageStats.attentionList(stats, max)
|
||||
local list = {}
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.score ~= b.score then return a.score > b.score end
|
||||
return tostring(a.stat.title) < tostring(b.stat.title)
|
||||
end)
|
||||
local result = {}
|
||||
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADStorageStats.kpis(stats)
|
||||
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
kpi.valueNow = kpi.valueNow + (stat.valueNow or 0)
|
||||
kpi.gain = kpi.gain + math.max(0, (stat.valueTop or 0) - (stat.valueNow or 0))
|
||||
if stat.emptyLevel ~= nil then kpi.emptyCount = kpi.emptyCount + 1 end
|
||||
if stat.fullLevel ~= nil then kpi.fullCount = kpi.fullCount + 1 end
|
||||
end
|
||||
return kpi
|
||||
end
|
||||
|
||||
-- procent -> bjælkens brøk i [0,1]
|
||||
function ADStorageStats.fraction(percent)
|
||||
if percent == nil then return 0 end
|
||||
return math.max(0, math.min(1, percent / 100))
|
||||
end
|
||||
|
||||
-- ---- "kræver handling" -> flow ----
|
||||
|
||||
-- Bygninger der BRUGER varen (fabrik-input, stald-foder) — silo/objektlager lagrer kun. -> {{name, placeable, level}}
|
||||
function ADStorageStats.consumersOf(inventory, fillType)
|
||||
local result = {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
if building.kind == "factory" or building.kind == "husbandry" then
|
||||
for _, entry in ipairs(building.entries or {}) do
|
||||
if entry.fillType == fillType and not entry.isOutput then
|
||||
table.insert(result, {name = building.name, placeable = building.placeable, level = entry.level or 0})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Hjælper et flow? Et flow flytter kun varer: det nytter når varen ligger andre steder end hos forbrugerne.
|
||||
-- s = {farmLevel, consumerLevel, perDay, consumerCount, existingRunId} -> "open" | "flow" | "produce" | "none"
|
||||
ADStorageStats.MIN_FLOW_LITERS = 5000
|
||||
ADStorageStats.MIN_FLOW_DAY_SHARE = 0.25 -- mindst en kvart dags forbrug skal kunne flyttes
|
||||
|
||||
function ADStorageStats.flowAdvice(s)
|
||||
if (s.consumerCount or 0) == 0 then return "none" end
|
||||
if s.existingRunId ~= nil then return "open" end
|
||||
local elsewhere = (s.farmLevel or 0) - (s.consumerLevel or 0)
|
||||
local needed = math.max(ADStorageStats.MIN_FLOW_LITERS, (s.perDay or 0) * ADStorageStats.MIN_FLOW_DAY_SHARE)
|
||||
return elsewhere >= needed and "flow" or "produce"
|
||||
end
|
||||
|
||||
-- "Sælg nu" -> udkørsels-flow: s = {sourceCount, existingRunId} -> "open" | "flow" | "none"
|
||||
function ADStorageStats.sellAdvice(s)
|
||||
if (s.sourceCount or 0) == 0 then return "none" end
|
||||
if s.existingRunId ~= nil then return "open" end
|
||||
return "flow"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
-- AD Stuck Diag
|
||||
-- Diagnose: et aktivt AutoDrive-køretøj der har holdt stille længe, logges med AutoDrives egen grund
|
||||
-- (forhindring, AD-trafik på ruten + hvem, pause, mål nået) og om Smart Pickup holder det (Wait/udkørsel).
|
||||
-- Kun læsning — ændrer intet. Logges én gang pr. ændret tilstand.
|
||||
|
||||
ADStuckDiag = {}
|
||||
|
||||
ADStuckDiag.CHECK_INTERVAL_MS = 10000
|
||||
ADStuckDiag.STILL_MS = 20000
|
||||
ADStuckDiag.STILL_SPEED = 0.00028 -- samme grænse som AD's egen stoppedTimer (m/ms)
|
||||
|
||||
local stillMs = setmetatable({}, {__mode = "k"})
|
||||
local lastText = setmetatable({}, {__mode = "k"})
|
||||
local timerMs = 0
|
||||
|
||||
local function name(vehicle)
|
||||
if vehicle == nil then return "-" end
|
||||
local ok, text = pcall(vehicle.getName, vehicle)
|
||||
return ok and tostring(text) or "?"
|
||||
end
|
||||
|
||||
local function taskName(vehicle)
|
||||
local taskModule = vehicle.ad.taskModule
|
||||
local task = taskModule ~= nil and taskModule.activeTask or nil
|
||||
if task == nil then return "ingen" end
|
||||
return tostring(task.taskType or "ukendt")
|
||||
end
|
||||
|
||||
-- Tekst der beskriver hvorfor køretøjet står (nil hvis ukendt/ikke relevant).
|
||||
function ADStuckDiag.describe(vehicle)
|
||||
local ad = vehicle.ad
|
||||
local cdm, dpm, sdm = ad.collisionDetectionModule, ad.drivePathModule, ad.specialDrivingModule
|
||||
local wayPoints, current = nil, nil
|
||||
if dpm ~= nil and dpm.getWayPoints ~= nil then wayPoints, current = dpm:getWayPoints() end
|
||||
local parts = {
|
||||
string.format("opgave=%s", taskName(vehicle)),
|
||||
string.format("mål=%s", tostring(ad.stateModule.getFirstMarkerId ~= nil and ad.stateModule:getFirstMarkerId() or "?")),
|
||||
string.format("wp=%s/%s", tostring(current), tostring(wayPoints ~= nil and #wayPoints or nil)),
|
||||
string.format("pause=%s", tostring(dpm ~= nil and dpm.isPaused)),
|
||||
string.format("målNået=%s", tostring(dpm ~= nil and dpm.isTargetReached ~= nil and dpm:isTargetReached())),
|
||||
string.format("forhindring=%s", tostring(cdm ~= nil and cdm.detectedObstable)),
|
||||
string.format("trafik=%s", cdm ~= nil and cdm.trafficVehicle ~= nil and name(cdm.trafficVehicle) or "-"),
|
||||
string.format("stopper=%s blokeret=%s", tostring(sdm ~= nil and sdm.isStoppingVehicle ~= nil and sdm:isStoppingVehicle()),
|
||||
tostring(sdm ~= nil and sdm.isBlocked)),
|
||||
}
|
||||
local wait = ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) or nil
|
||||
local hold = ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) or nil
|
||||
local pickup = ADSmartPickup ~= nil and ADSmartPickup.getActivePickup ~= nil and ADSmartPickup.getActivePickup(vehicle) or nil
|
||||
table.insert(parts, string.format("SP: wait=%s hold=%s pickup=%s",
|
||||
wait ~= nil and tostring(wait.markerId or wait.wayPointId or "ja") or "-",
|
||||
hold ~= nil and tostring(hold.markerId or "ja") or "-",
|
||||
pickup ~= nil and tostring(pickup.wayPointId or pickup.markerId or "ja") or "-"))
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
function ADStuckDiag.tick(dt)
|
||||
timerMs = timerMs + (dt or 0)
|
||||
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
|
||||
local elapsed = timerMs
|
||||
timerMs = 0
|
||||
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
|
||||
for _, vehicle in pairs(vehicles) do
|
||||
local ad = vehicle.ad
|
||||
local active = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive()
|
||||
if active and (vehicle.lastSpeedReal or 0) < ADStuckDiag.STILL_SPEED then
|
||||
stillMs[vehicle] = (stillMs[vehicle] or 0) + elapsed
|
||||
else
|
||||
stillMs[vehicle], lastText[vehicle] = nil, nil
|
||||
end
|
||||
if (stillMs[vehicle] or 0) >= ADStuckDiag.STILL_MS then
|
||||
local ok, text = pcall(ADStuckDiag.describe, vehicle)
|
||||
text = ok and text or ("diagnose fejlede: " .. tostring(text))
|
||||
if text ~= lastText[vehicle] then
|
||||
lastText[vehicle] = text
|
||||
Logging.info("ADSmartPickup: STÅR STILLE %d s '%s': %s", math.floor(stillMs[vehicle] / 1000), name(vehicle), text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,15 +29,32 @@ function ADSupplyPlanner.prefersFullestSource(fillTypeNames)
|
||||
return true
|
||||
end
|
||||
|
||||
-- "Rigelig" kilde: kan levere mindst MIN_LOAD_SHARE af et læs (mindst MIN_AMPLE_LITERS).
|
||||
ADSupplyPlanner.MIN_LOAD_SHARE = 0.25
|
||||
ADSupplyPlanner.MIN_AMPLE_LITERS = 5000
|
||||
|
||||
function ADSupplyPlanner.ampleLevel(loadCapacity)
|
||||
return math.max(ADSupplyPlanner.MIN_AMPLE_LITERS, (loadCapacity or 0) * ADSupplyPlanner.MIN_LOAD_SHARE)
|
||||
end
|
||||
|
||||
-- Er en kilde med dette niveau bedre end den hidtil bedste (nil = ingen endnu)?
|
||||
function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest)
|
||||
-- Lagervarer: mindst fyldte først, men kun blandt rigelige kilder — en næsten tom tank der fyldes løbende
|
||||
-- (gæringssilo) ville ellers altid vinde og give et par tusind liter pr. tur (in-game 29/9). Ingen rigelige:
|
||||
-- den fyldeste. ample = nil: gammel regel.
|
||||
function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest, ample)
|
||||
if bestLevel == nil then
|
||||
return true
|
||||
end
|
||||
if prefersFullest then
|
||||
return level > bestLevel
|
||||
end
|
||||
return level < bestLevel
|
||||
if ample == nil then
|
||||
return level < bestLevel
|
||||
end
|
||||
local isAmple, bestIsAmple = level >= ample, bestLevel >= ample
|
||||
if isAmple ~= bestIsAmple then return isAmple end
|
||||
if isAmple then return level < bestLevel end
|
||||
return level > bestLevel
|
||||
end
|
||||
|
||||
function ADSupplyPlanner.copyList(list)
|
||||
@@ -345,3 +362,14 @@ function ADSupplyPlanner.parallelFoodShare(groups, fillLevels, capacity, fillTyp
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Læs der allerede ligger på vognen, når et run startes (eller AutoDrive genstarter traktoren efter
|
||||
-- indlæsning): tæller som "på vej" til målet, hvis varen hører til run'et. Returnerer fillType, liter
|
||||
-- eller nil. (in-game 27/9: tre fyldte biogas-traktorer kørte alle til Drop 1 — ingen var registreret.)
|
||||
function ADSupplyPlanner.cargoFor(fillType, liters, ingredients)
|
||||
if fillType == nil or type(liters) ~= "number" or liters <= 0 then return nil end
|
||||
for _, ingredient in ipairs(ingredients or {}) do
|
||||
if ingredient == fillType then return fillType, liters end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -255,6 +255,9 @@ end
|
||||
-- AD ser — vognens indhold, målets fri plads, AD's aflæsningstilstand og vognens dischargeState.
|
||||
ADUnloadWait.DEBUG_INTERVAL_MS = 15000
|
||||
local debugTimers = setmetatable({}, {__mode = "k"})
|
||||
local debugStuck = setmetatable({}, {__mode = "k"}) -- vehicle -> {level, since}: læsset uændret siden
|
||||
ADUnloadWait.STUCK_WITH_ROOM_MS = 30000
|
||||
|
||||
local function debugAtTarget(adEnv, task, dt)
|
||||
local vehicle = task.vehicle
|
||||
local targetReached = vehicle.ad.drivePathModule:isTargetReached()
|
||||
@@ -268,8 +271,15 @@ local function debugAtTarget(adEnv, task, dt)
|
||||
return
|
||||
end
|
||||
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
|
||||
-- kun den tilstand vi vil se: målet fuldt/afvist, og traktoren venter ikke ved et Wait-punkt
|
||||
if waits[vehicle] ~= nil or (free ~= nil and free >= ADUnloadWait.MIN_FREE_TO_UNLOAD) then
|
||||
-- ved målet MED plads, men læsset falder ikke (in-game 29/9: EDK 650 ved TMR Unload/Biogas Drop) — også det
|
||||
-- vil vi se. Ellers kun: målet fuldt/afvist, og traktoren venter ikke ved et Wait-punkt.
|
||||
local stuck = debugStuck[vehicle]
|
||||
if stuck == nil or rest < stuck.level - 1 then
|
||||
stuck = {level = rest, since = g_time or 0}
|
||||
debugStuck[vehicle] = stuck
|
||||
end
|
||||
local stuckWithRoom = targetReached and (g_time or 0) - stuck.since >= ADUnloadWait.STUCK_WITH_ROOM_MS
|
||||
if waits[vehicle] ~= nil or (free ~= nil and free >= ADUnloadWait.MIN_FREE_TO_UNLOAD and not stuckWithRoom) then
|
||||
return
|
||||
end
|
||||
local trailerModule = vehicle.ad.trailerModule
|
||||
@@ -295,6 +305,12 @@ function ADUnloadWait.beforeUpdate(adEnv, task, dt)
|
||||
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle ~= nil and ADOutbound.isOutboundVehicle(task.vehicle) then
|
||||
return
|
||||
end
|
||||
-- Tømning efter stop (ADFlowWagons): ingen Wait-pulje eller læs-bytte — den skal tømme og hjem
|
||||
-- (in-game 29/9: Goat Food blev sendt til Cow Wait efter Stop alt)
|
||||
if ADFlowWagons ~= nil and ADFlowWagons.isEmptying ~= nil and ADFlowWagons.isEmptying(task.vehicle) then
|
||||
if waits[task.vehicle] ~= nil then release(task.vehicle) end
|
||||
return
|
||||
end
|
||||
pcall(debugAtTarget, adEnv, task, dt)
|
||||
local wait = waits[task.vehicle]
|
||||
if wait ~= nil then
|
||||
@@ -309,7 +325,7 @@ end
|
||||
-- stående for evigt. Her genoptages ventningen for aktive traktorer i et forsynings-run hvis
|
||||
-- aflæsningsopgave peger på en Wait-markør: hjem = run'ets mål, og reservationen sættes igen.
|
||||
-- Kaldes fra ADSmartPickup:update (throttlet).
|
||||
ADUnloadWait.ADOPT_INTERVAL_MS = 5000
|
||||
ADUnloadWait.ADOPT_INTERVAL_MS = 5300
|
||||
local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS
|
||||
|
||||
function ADUnloadWait.adoptOrphans(adEnv)
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
ADWaitPool = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
|
||||
ADWaitPool.STATE_DRIVING = 2
|
||||
@@ -14,6 +21,10 @@ ADWaitPool.claims = setmetatable({}, {__mode = "v"})
|
||||
|
||||
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
|
||||
function ADWaitPool.getCandidates(adEnv, poolGroup)
|
||||
return cacheGet("waitCandidates", tostring(poolGroup or ""), 10000, function() return ADWaitPool.findCandidates(adEnv, poolGroup) end)
|
||||
end
|
||||
|
||||
function ADWaitPool.findCandidates(adEnv, poolGroup)
|
||||
local candidates = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
|
||||
@@ -57,26 +68,47 @@ ADWaitPool.OCCUPIED_RADIUS = 6
|
||||
|
||||
-- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et
|
||||
-- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning).
|
||||
-- Alle motoriserede køretøjers positioner, delt mellem opslag i 0,5 s (ikke én runde pr. vogn pr. opslag).
|
||||
local function allVehiclePositions()
|
||||
return cacheGet("vehiclePositions", "all", 500, function()
|
||||
local list = {}
|
||||
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
||||
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
||||
if other.spec_motorized ~= nil and other.rootNode ~= nil then
|
||||
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
||||
if ok and x ~= nil then table.insert(list, {vehicle = other, x = x, z = z}) end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end)
|
||||
end
|
||||
|
||||
local function otherVehiclePositions(vehicle)
|
||||
local positions = {}
|
||||
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
||||
local own = {}
|
||||
if vehicle.getChildVehicles ~= nil then
|
||||
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
||||
for _, child in pairs(ok and children or {}) do own[child] = true end
|
||||
end
|
||||
own[vehicle] = true
|
||||
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
||||
if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then
|
||||
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
||||
if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, entry in ipairs(allVehiclePositions()) do
|
||||
if not own[entry.vehicle] then table.insert(positions, entry) end
|
||||
end
|
||||
return positions
|
||||
end
|
||||
|
||||
function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable)
|
||||
-- Midtpunkt af punkter {x, z} (fx flowets kilde-markører) -> x, z eller nil.
|
||||
function ADWaitPool.anchorOf(points)
|
||||
if points == nil or #points == 0 then return nil end
|
||||
local sx, sz = 0, 0
|
||||
for _, point in ipairs(points) do sx, sz = sx + point.x, sz + point.z end
|
||||
return sx / #points, sz / #points
|
||||
end
|
||||
|
||||
-- anchorX/anchorZ (valgfri): vælg nærmest dette punkt i stedet for traktoren (flow med "All": nærmest kilderne).
|
||||
function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable, anchorX, anchorZ)
|
||||
local x, z = getVehiclePosition(vehicle)
|
||||
if anchorX ~= nil and anchorZ ~= nil then x, z = anchorX, anchorZ end
|
||||
if x == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -124,11 +156,42 @@ local function graphReaches(adEnv, vehicle, targetId)
|
||||
return false
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil then
|
||||
return true
|
||||
-- Rutesvar gemmes ROUTE_CACHE_MS pr. mål og 10 m-område: holdende vogne spørger hvert 5. s for hver
|
||||
-- kandidat, og AD's stifinding (+ reserve-søgningen) gennem hele vejnettet gav et hak hvert 5. s
|
||||
-- (in-game 26/9). Vejnettet ændrer sig ikke så hurtigt.
|
||||
ADWaitPool.ROUTE_CACHE_MS = 30000
|
||||
ADWaitPool.ROUTE_CELL_M = 10
|
||||
ADWaitPool.SLOW_ROUTE_MS = 3
|
||||
local routeCache = {}
|
||||
local routeCacheSize = 0
|
||||
ADWaitPool.ROUTE_CACHE_MAX = 2000
|
||||
|
||||
local function remember(key, result, at)
|
||||
if routeCache[key] == nil then
|
||||
routeCacheSize = routeCacheSize + 1
|
||||
if routeCacheSize > ADWaitPool.ROUTE_CACHE_MAX then
|
||||
routeCache, routeCacheSize = {}, 1
|
||||
end
|
||||
end
|
||||
routeCache[key] = {result = result, at = at}
|
||||
end
|
||||
|
||||
local function clockMs()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function routeKey(vehicle, wayPointId)
|
||||
if vehicle.rootNode == nil or getWorldTranslation == nil then return nil end
|
||||
local ok, x, _, z = pcall(getWorldTranslation, vehicle.rootNode)
|
||||
if not ok or x == nil then return nil end
|
||||
local cell = ADWaitPool.ROUTE_CELL_M
|
||||
return string.format("%s:%d:%d", tostring(wayPointId), math.floor(x / cell), math.floor(z / cell))
|
||||
end
|
||||
|
||||
local function computeRoute(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId) then
|
||||
return true
|
||||
end
|
||||
@@ -142,13 +205,73 @@ function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
return false
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil then
|
||||
return true
|
||||
end
|
||||
local now = clockMs()
|
||||
local key = now ~= nil and routeKey(vehicle, wayPointId) or nil
|
||||
local cached = key ~= nil and routeCache[key] or nil
|
||||
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = computeRoute(adEnv, vehicle, wayPointId)
|
||||
if key ~= nil then
|
||||
local spent = (clockMs() or now) - now
|
||||
remember(key, result, now)
|
||||
if spent > ADWaitPool.SLOW_ROUTE_MS and Logging ~= nil and Logging.info ~= nil
|
||||
and (ADWaitPool.lastSlowLog == nil or now - ADWaitPool.lastSlowLog > 60000) then
|
||||
ADWaitPool.lastSlowLog = now
|
||||
Logging.info("ADSmartPickup: rutetjek til markør %s tog %.1f ms (%s) — gemmes nu i %d s", tostring(wayPointId), spent, result and "vej" or "ingen vej", ADWaitPool.ROUTE_CACHE_MS / 1000)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Rute mellem to markører: afhænger ikke af vognens position, så svaret gemmes længe (5 min). Profiler
|
||||
-- 26/9: "rutetjek frem+tilbage" tog 30-39 ms pr. kald og blev regnet igen hver gang vognen havde flyttet sig
|
||||
-- 10 m — det gav hak på 44-67 ms hvert minut.
|
||||
ADWaitPool.MARKER_ROUTE_MS = 300000
|
||||
local markerRoutes = {}
|
||||
function ADWaitPool.hasMarkerRoute(adEnv, fromWayPointId, toWayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.pathFromTo == nil then return true end
|
||||
local key = tostring(fromWayPointId) .. ">" .. tostring(toWayPointId)
|
||||
local now = clockMs()
|
||||
local cached = markerRoutes[key]
|
||||
if cached ~= nil and now ~= nil and now - cached.at < ADWaitPool.MARKER_ROUTE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = pathReaches(graph:pathFromTo(fromWayPointId, toWayPointId), toWayPointId)
|
||||
if now ~= nil then markerRoutes[key] = {result = result, at = now + math.random() * ADWaitPool.MARKER_ROUTE_MS * 0.5} end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADWaitPool.resetMarkerRoutes()
|
||||
markerRoutes = {}
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil or graph.pathFromTo == nil then
|
||||
return true
|
||||
end
|
||||
return pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
|
||||
-- kendt hjem (kilden): vognen kører hjem -> mål -> hjem; markør til markør, gemt længe
|
||||
if homeWayPointId ~= nil then
|
||||
return ADWaitPool.hasMarkerRoute(adEnv, homeWayPointId, toWayPointId)
|
||||
and ADWaitPool.hasMarkerRoute(adEnv, toWayPointId, homeWayPointId)
|
||||
end
|
||||
local now = clockMs()
|
||||
local key = now ~= nil and routeKey(vehicle, tostring(toWayPointId) .. ">" .. tostring(homeWayPointId)) or nil
|
||||
local cached = key ~= nil and routeCache[key] or nil
|
||||
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
|
||||
and pathReaches(graph:pathFromTo(toWayPointId, homeWayPointId), homeWayPointId)
|
||||
if key ~= nil then remember(key, result, now) end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Står AD midt i en (blokeret) af-/pålæsning, stoppes den pænt først — ellers kører
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
-- Smart Pickup — "Kræver handling" -> flow (v1.60)
|
||||
-- En "løber tør"-linje i lagerfanerne kan klikkes: findes der et flow der leverer varen til forbrugerne, åbnes
|
||||
-- det; ligger varen andre steder på gården, åbnes "Nyt flow" udfyldt (ind til forbrugerne, varen valgt); ellers
|
||||
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
end
|
||||
|
||||
-- Forsyningsflow der allerede leverer varen til en af forbrugerne -> run eller nil.
|
||||
local function existingSupplyRun(consumerIds, name)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if run.kind ~= "outbound" then
|
||||
local carries = run.ingredientMode ~= "manual"
|
||||
for _, ingredient in ipairs(run.ingredients or {}) do
|
||||
if ingredient == name then carries = true end
|
||||
end
|
||||
if carries then
|
||||
for _, id in ipairs(run.targetBuildingIds or {}) do
|
||||
if consumerIds[id] then return run end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Udkørselsflow der allerede sælger varen fra en af bygningerne -> run eller nil.
|
||||
local function existingSellRun(sourceIds, name)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if run.kind == "outbound" then
|
||||
local sells = false
|
||||
for _, product in ipairs(run.products or {}) do
|
||||
if product.fillType == name and (product.policy == "sell" or product.policy == "sellElseStore") then sells = true end
|
||||
end
|
||||
if sells then
|
||||
for _, id in ipairs(run.sourceBuildingIds or {}) do
|
||||
if sourceIds[id] then return run end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- "Sælg nu": kilder = fanens bygninger med varen.
|
||||
local function sellAdviceFor(stat)
|
||||
local sourceIds, ids = {}, {}
|
||||
for _, building in ipairs(stat.buildings or {}) do
|
||||
local id = building.placeable ~= nil and ADBuildings.getId(building.placeable) or nil
|
||||
if id ~= nil and not sourceIds[id] then
|
||||
sourceIds[id] = true
|
||||
table.insert(ids, id)
|
||||
end
|
||||
end
|
||||
local name = fillTypeName(stat.fillType)
|
||||
local run = existingSellRun(sourceIds, name)
|
||||
local advice = ADStorageStats.sellAdvice({sourceCount = #ids, existingRunId = run ~= nil and run.id or nil})
|
||||
return advice, {buildingIds = ids, run = run, name = name, stat = stat, sell = true}
|
||||
end
|
||||
|
||||
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
|
||||
function SmartPickupFrame:attentionAdvice(entry)
|
||||
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
|
||||
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
|
||||
local stat = entry.stat
|
||||
local consumers = ADStorageStats.consumersOf(self.lastFullInventory, stat.fillType)
|
||||
local consumerIds, consumerLevel, ids = {}, 0, {}
|
||||
for _, consumer in ipairs(consumers) do
|
||||
consumerLevel = consumerLevel + consumer.level
|
||||
local id = consumer.placeable ~= nil and ADBuildings.getId(consumer.placeable) or nil
|
||||
if id ~= nil and not consumerIds[id] then
|
||||
consumerIds[id] = true
|
||||
table.insert(ids, id)
|
||||
end
|
||||
end
|
||||
local name = fillTypeName(stat.fillType)
|
||||
local run = existingSupplyRun(consumerIds, name)
|
||||
local advice = ADStorageStats.flowAdvice({farmLevel = stat.farmLevel or stat.level, consumerLevel = consumerLevel,
|
||||
perDay = stat.consumption, consumerCount = #ids, existingRunId = run ~= nil and run.id or nil})
|
||||
return advice, {consumers = consumers, buildingIds = ids, run = run, name = name, stat = stat}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:attentionHint(advice, info)
|
||||
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
|
||||
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
|
||||
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
|
||||
return ""
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onAttentionClick(entry)
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
local advice, info = frame:attentionAdvice(entry)
|
||||
if adEnv == nil or advice == "none" then return end
|
||||
if advice == "open" then
|
||||
frame.selectedRunId = info.run.id
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
frame:openEditPanel("flow", info.run.id)
|
||||
return
|
||||
end
|
||||
if advice == "produce" then
|
||||
local text = string.format(frame.i18n:getText("spu_attention_produce"), tostring(info.stat.title),
|
||||
SmartPickupFrame.formatCompact(info.stat.farmLevel or 0))
|
||||
frame.statusOverride, frame.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
|
||||
return
|
||||
end
|
||||
local guide
|
||||
if info.sell then
|
||||
-- udfyldt "Nyt flow": ud af bygningerne med varen, varen til salg
|
||||
local name = string.format(frame.i18n:getText("spu_attention_sellFlowName"), tostring(info.stat.title))
|
||||
guide = ADFlowGuide.openNew(name, {})
|
||||
guide.draft.kind = "outbound"
|
||||
guide.draft.sourceBuildingIds = info.buildingIds
|
||||
guide.draft.products = {{fillType = info.name, policy = "sell"}}
|
||||
else
|
||||
-- udfyldt "Nyt flow": ind til forbrugerne, kun denne vare; guiden åbnes på oversigten (Gem/Start eller [Ret])
|
||||
local name = string.format(frame.i18n:getText("spu_attention_flowName"), tostring(info.stat.title), tostring(info.consumers[1].name))
|
||||
guide = ADFlowGuide.openNew(name, {})
|
||||
guide.draft.targetBuildingIds = info.buildingIds
|
||||
guide.draft.ingredientMode = "manual"
|
||||
guide.draft.ingredients = {info.name}
|
||||
end
|
||||
guide = ADGuide.new(ADFlowGuide.DEF, guide.draft, frame:flowGuideContext(adEnv, guide.draft), {step = "summary"})
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
frame:openGuide("flow", guide)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,194 @@
|
||||
-- Smart Pickup — redigeringsvindue (v1.58)
|
||||
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
|
||||
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
|
||||
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS[kind] =
|
||||
-- {title, exists, fill, status, startStop, startStopText, canDelete, delete} (se Flows-/Fields-fanen).
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.80, 0.90 -- andel af listens flade
|
||||
SmartPickupFrame.EDIT_PANEL_HEADER_PX = 80
|
||||
SmartPickupFrame.EDIT_PANEL_ROW_PX = 860 -- rækkerne er ~835 px brede
|
||||
|
||||
function SmartPickupFrame:initEditPanelButtons(i18n)
|
||||
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
|
||||
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
|
||||
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
|
||||
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
|
||||
self.panelButtonsNoDelete = {self.panelCloseButtonInfo, self.panelStartButtonInfo}
|
||||
self.panelGuideButtonCache = setmetatable({}, {__mode = "k"})
|
||||
end
|
||||
|
||||
-- Vinduets knaplinje (uden Slet for arter der ikke kan slettes).
|
||||
function SmartPickupFrame:getPanelButtons()
|
||||
local hooks = self:editPanelHooks()
|
||||
return (hooks ~= nil and hooks.canDelete) and self.panelButtons or self.panelButtonsNoDelete
|
||||
end
|
||||
|
||||
function SmartPickupFrame:editPanelHooks()
|
||||
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
|
||||
end
|
||||
|
||||
-- Guidens knapper i vinduet: Tilbage (ESC) lukker trinnet/vinduet i stedet for hele menuen.
|
||||
function SmartPickupFrame:panelGuideButtons()
|
||||
local source = self:getGuideButtons()
|
||||
local cached = self.panelGuideButtonCache[source]
|
||||
if cached == nil then
|
||||
cached = {}
|
||||
for index, info in ipairs(source) do
|
||||
cached[index] = info == self.backButtonInfo and self.panelCloseButtonInfo or info
|
||||
end
|
||||
self.panelGuideButtonCache[source] = cached
|
||||
end
|
||||
return cached
|
||||
end
|
||||
|
||||
-- Sæt størrelse FØR position: setSize kan genberegne den absolutte position ud fra forælderen (review 29/9).
|
||||
-- -> true hvis elementet flyttede sig.
|
||||
local function place(element, x, y, w, h)
|
||||
if w ~= nil then element:setSize(w, h) end
|
||||
local pos = element.absPosition
|
||||
local moved = pos == nil or math.abs(pos[1] - x) > 1e-6 or math.abs(pos[2] - y) > 1e-6
|
||||
element:setAbsolutePosition(x, y)
|
||||
return moved
|
||||
end
|
||||
|
||||
-- Vinduet dækker listens flade; panelet er centreret i den (absolutte koordinater, y opad). Kaldes ved åbning,
|
||||
-- ved hver opbygning og hvert sekund, så en layout-gennemgang i menuen ikke flytter det væk.
|
||||
function SmartPickupFrame:layoutEditPanel()
|
||||
local box = self.boxLayout
|
||||
if box == nil or box.absPosition == nil or box.absSize == nil then return end
|
||||
local x, y, w, h = box.absPosition[1], box.absPosition[2], box.absSize[1], box.absSize[2]
|
||||
place(self.editBackdrop, x, y, w, h)
|
||||
-- så bredt som rækkerne (~835 px) + margen, ikke bredere end listen
|
||||
local pw = math.min(w * SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_ROW_PX * g_pixelSizeScaledX)
|
||||
local ph = h * SmartPickupFrame.EDIT_PANEL_HEIGHT
|
||||
local px, py = x + (w - pw) / 2, y + (h - ph) / 2
|
||||
place(self.editPanel, px, py, pw, ph)
|
||||
local header = SmartPickupFrame.EDIT_PANEL_HEADER_PX * g_pixelSizeScaledY
|
||||
local margin = 20 * g_pixelSizeScaledX
|
||||
place(self.editTitle, px + margin, py + ph - header * 0.45)
|
||||
place(self.editStatus, px + margin, py + ph - header * 0.85)
|
||||
if place(self.editLayout, px, py, pw, ph - header) then
|
||||
self.editLayout:invalidateLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openEditPanel(kind, id)
|
||||
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil or self.editOverlay == nil then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
|
||||
self:closeGuide()
|
||||
self.editOverlay:setVisible(true)
|
||||
-- listen skjules bag vinduet: dens tekster skinnede igennem panelet (in-game 29/9)
|
||||
self.boxLayout:setVisible(false)
|
||||
self:layoutEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Lukker uden genopbygning (fanebyt, lukket menu, slettet post).
|
||||
function SmartPickupFrame:hideEditPanel()
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self:clearPanelRows()
|
||||
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
|
||||
if self.boxLayout ~= nil then self.boxLayout:setVisible(true) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:closeEditPanel()
|
||||
if not self.editPanelOpen then return end
|
||||
self:closeGuide()
|
||||
self:hideEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
|
||||
function SmartPickupFrame:fillEditPanel(adEnv)
|
||||
self:clearPanelRows()
|
||||
local hooks = self:editPanelHooks()
|
||||
if hooks == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then
|
||||
self.statusOverride = self.i18n:getText("spu_panel_gone")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
self:closeGuide()
|
||||
self:hideEditPanel()
|
||||
return
|
||||
end
|
||||
self:layoutEditPanel()
|
||||
self.editTitle:setText(hooks.title(self, self.editPanelId))
|
||||
self:withRowTarget(self.editLayout, function(frame)
|
||||
if frame.guide ~= nil then
|
||||
frame:fillGuideRows(adEnv)
|
||||
else
|
||||
hooks.fill(frame, adEnv, frame.editPanelId)
|
||||
end
|
||||
end)
|
||||
self:colorLayout(self.editLayout)
|
||||
self.editLayout:invalidateLayout()
|
||||
self:refreshEditPanel(adEnv)
|
||||
end
|
||||
|
||||
-- Hvert sekund: status i niveaufarve + knaptekster.
|
||||
function SmartPickupFrame:refreshEditPanel(adEnv)
|
||||
local hooks = self:editPanelHooks()
|
||||
if hooks == nil or adEnv == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then
|
||||
self.statusOverride = self.i18n:getText("spu_panel_gone")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
self:closeEditPanel()
|
||||
return
|
||||
end
|
||||
self:layoutEditPanel()
|
||||
local status = hooks.status(self, adEnv, self.editPanelId)
|
||||
self.editStatus:setText(tostring(status.text or ""))
|
||||
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
|
||||
if self.editStatus.setTextColor ~= nil then
|
||||
self.editStatus:setTextColor(color[1], color[2], color[3], color[4])
|
||||
end
|
||||
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
|
||||
self.panelStartButtonInfo.disabled = hooks.canStart ~= nil and not hooks.canStart(self, self.editPanelId)
|
||||
end
|
||||
|
||||
-- ESC mens vinduet er åbent: menuen spørger requestClose; vinduet lukkes (trin først), menuen bliver.
|
||||
-- Kalder spillet OGSÅ knappens callback for samme tryk, må det ikke lukke to trin (panelClosedAt).
|
||||
SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS = 250
|
||||
|
||||
function SmartPickupFrame:requestClose(callback)
|
||||
if self.editPanelOpen then
|
||||
self:onClickPanelClose()
|
||||
return false
|
||||
end
|
||||
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
|
||||
return false
|
||||
end
|
||||
return SmartPickupFrame:superClass().requestClose(self, callback)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelClose()
|
||||
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
|
||||
return
|
||||
end
|
||||
self.panelClosedAt = g_time
|
||||
self:safe(function(frame)
|
||||
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
|
||||
if frame.guide ~= nil then
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
return
|
||||
end
|
||||
frame:closeEditPanel()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelStartStop()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelHooks()
|
||||
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelDelete()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelHooks()
|
||||
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,242 @@
|
||||
-- Smart Pickup — guiden "Opsæt mark" og fanen Markers knapper
|
||||
-- Henter det spil-afhængige (afgrøder der kan sås, slåafgrøder, halm, maskiner, marksiloer, siloer i nærheden)
|
||||
-- som `context` til ADFieldGuide og gemmer guidens resultat via ADFieldJobs. Selve trin-skærmen er fælles
|
||||
-- (gui/SmartPickupGuide.lua). Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
-- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer)
|
||||
SmartPickupFrame.FIELD_STORE_ROWS = 8
|
||||
|
||||
function SmartPickupFrame:initFieldButtons(i18n)
|
||||
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
|
||||
self.fieldSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_field_open"), callback = function() self:onClickFieldSetup() end}
|
||||
self.fieldFleetButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_open"), callback = function() self:onClickFieldFleet(true) end}
|
||||
self.fieldFleetCloseButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_close"), callback = function() self:onClickFieldFleet(false) end}
|
||||
-- v1.59: listen har Start/Stop pr. række; Opsæt mark ligger i redigeringsvinduet
|
||||
self.fieldButtons = {self.backButtonInfo, self.fieldFleetButtonInfo}
|
||||
self.fieldFleetButtons = {self.backButtonInfo, self.fieldFleetCloseButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:getFieldButtons()
|
||||
return self.fieldView == "fleet" and self.fieldFleetButtons or self.fieldButtons
|
||||
end
|
||||
|
||||
-- ---- context til ADFieldGuide ----
|
||||
|
||||
-- {[fieldId] = rigIds} for alle marker
|
||||
function SmartPickupFrame.rigIdsByField()
|
||||
local byField = {}
|
||||
for fieldId, field in pairs(ADFieldJobs.state.fields) do byField[fieldId] = field.plan ~= nil and field.plan.rigIds or {} end
|
||||
return byField
|
||||
end
|
||||
|
||||
local function fruitTypes()
|
||||
return g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
|
||||
end
|
||||
|
||||
-- Afgrøder der kan sås, slåafgrøder (navne) og afgrøder med halm (skår efter mejetærskeren).
|
||||
function SmartPickupFrame:fruitContext()
|
||||
local sowable, straw, mowable = {}, {}, {}
|
||||
for _, fruit in ipairs(fruitTypes()) do
|
||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
||||
table.insert(sowable, {name = fruit.name, title = tostring(fruit.fillType.title or fruit.name)})
|
||||
end
|
||||
local ok, hasStraw = pcall(function() return fruit.hasWindrow == true or fruit.windrowName ~= nil end)
|
||||
if ok and hasStraw and fruit.name ~= nil then straw[fruit.name] = true end
|
||||
end
|
||||
if ADFields ~= nil and g_fruitTypeManager ~= nil then
|
||||
for index in pairs(ADFields.getMowable()) do
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(index)
|
||||
if fruit ~= nil and fruit.name ~= nil then mowable[fruit.name] = true end
|
||||
end
|
||||
end
|
||||
return sowable, mowable, straw
|
||||
end
|
||||
|
||||
-- Markens vare ud fra kladdens plan (ensilage -> skår, hø -> tørskår, høst -> afgrøden); nil på bar mark.
|
||||
local function fieldFillType(info, plan)
|
||||
if info.kind == "mowable" then
|
||||
return ADFieldModel.product(plan, "mowable") == "hay" and info.hay or info.windrow
|
||||
end
|
||||
if info.kind == "harvest" then return info.harvestFillType end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Siloer der tager markens vare: marksilo først, gæringssiloer, så de nærmeste (samme regel som før).
|
||||
function SmartPickupFrame:fieldStoreContext(adEnv, info, plan, chosenIds)
|
||||
local fillType = fieldFillType(info, plan)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local fieldSilos, silosById = {}, {}
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
|
||||
table.insert(fieldSilos, {id = building.id, name = tostring(building.name),
|
||||
text = markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)})
|
||||
silosById[building.id] = true
|
||||
end
|
||||
if fillType == nil then return nil, 0, fieldSilos end
|
||||
local chosen = {}
|
||||
for _, id in ipairs(chosenIds or {}) do chosen[id] = true end
|
||||
local fermenterIds = {}
|
||||
if info.kind == "mowable" and ADFieldModel.product(plan, "mowable") == "silage" then
|
||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
||||
end
|
||||
local entries = {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
||||
local distance = nil
|
||||
local node = building.placeable ~= nil and building.placeable.rootNode or nil
|
||||
if node ~= nil and info.x ~= nil then
|
||||
local bx, _, bz = getWorldTranslation(node)
|
||||
distance = math.sqrt((bx - info.x) ^ 2 + (bz - info.z) ^ 2)
|
||||
end
|
||||
table.insert(entries, {id = building.id, building = building, distance = distance, chosen = chosen[building.id] == true,
|
||||
fieldSilo = silosById[building.id] == true, fermenter = fermenterIds[building.id] == true})
|
||||
end
|
||||
end
|
||||
local shown, hidden = ADFieldView.storeRows(entries, SmartPickupFrame.FIELD_STORE_ROWS)
|
||||
local stores = {}
|
||||
for _, entry in ipairs(shown) do
|
||||
local building = entry.building
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
|
||||
-- det der skelner rækkerne først (rækken afkorter bagfra): markør, afstand, art — så bygningens navn
|
||||
local label = (markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)) .. ", " .. ADFieldView.distanceText(entry.distance)
|
||||
if entry.fieldSilo then label = label .. " (" .. self.i18n:getText("spu_field_fieldSilo") .. ")" end
|
||||
if entry.fermenter then label = label .. " (" .. self.i18n:getText("spu_field_fermenter") .. ")" end
|
||||
if markers[1] ~= nil then label = label .. " — " .. tostring(building.name) end
|
||||
table.insert(stores, {id = building.id, text = label})
|
||||
end
|
||||
return stores, hidden, fieldSilos
|
||||
end
|
||||
|
||||
-- Markerne til trin 1 ("F22 — Hvede — Venter").
|
||||
function SmartPickupFrame:fieldPickList()
|
||||
local fields = {}
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do
|
||||
local crop = ADFieldView.cropText(row.info, function(key) return self.i18n:getText(key) end)
|
||||
table.insert(fields, {id = row.info.id, text = string.format("F%d — %s — %s", row.info.id, crop, self:fieldStatusText(row.field, row.info, true))})
|
||||
end
|
||||
return fields
|
||||
end
|
||||
|
||||
-- context for en mark (info) med kladdens plan; info nil = trin 1 uden valgt mark.
|
||||
function SmartPickupFrame:fieldGuideContext(adEnv, info, plan, chosenStoreIds)
|
||||
local sowable, mowable, straw = self:fruitContext()
|
||||
local context = {info = info or {}, sowable = sowable, mowable = mowable, straw = straw,
|
||||
rigIdsByField = SmartPickupFrame.rigIdsByField(), fieldSilos = {}, storeHidden = 0, fields = {}, rigs = {}}
|
||||
-- marklisten bruges kun i trin 1 (guiden åbnet uden valgt mark)
|
||||
if info == nil then context.fields = self:fieldPickList() end
|
||||
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do
|
||||
table.insert(context.rigs, {id = rig.id, role = rig.role, text = string.format("%s — %s", tostring(rig.label), self:roleText(rig.role))})
|
||||
end
|
||||
if info ~= nil then
|
||||
context.stores, context.storeHidden, context.fieldSilos = self:fieldStoreContext(adEnv, info, plan or {}, chosenStoreIds)
|
||||
end
|
||||
return context
|
||||
end
|
||||
|
||||
-- ---- hooks til den fælles guide-skærm ----
|
||||
|
||||
local function chosenStoreIds(guide)
|
||||
local ids = {}
|
||||
for _, id in ipairs(guide.draft.storeIds or {}) do table.insert(ids, id) end
|
||||
for _, id in ipairs(guide.draft.plan ~= nil and guide.draft.plan.storeBuildingIds or {}) do table.insert(ids, id) end
|
||||
return ids
|
||||
end
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
SmartPickupFrame.GUIDE_HOOKS.field = {
|
||||
model = ADFieldGuide,
|
||||
tab = SmartPickupFrame.TAB_FIELDS,
|
||||
cannotAdvanceKey = "spu_guide_pickField",
|
||||
heading = function(frame, guide)
|
||||
if guide.draft.fieldId == nil then return frame.i18n:getText("spu_guide_field_titleNone") end
|
||||
return string.format(frame.i18n:getText("spu_guide_field_title"), guide.draft.fieldId)
|
||||
end,
|
||||
context = function(frame, adEnv, guide)
|
||||
local fieldId = guide.draft.fieldId
|
||||
if fieldId == nil then return frame:fieldGuideContext(adEnv, nil) end
|
||||
local info = ADFields.getInfo(fieldId)
|
||||
-- marken er solgt/bygget over mens guiden var åben
|
||||
if info == nil or info.cultivable == false then return nil end
|
||||
return frame:fieldGuideContext(adEnv, info, guide.draft.plan, chosenStoreIds(guide))
|
||||
end,
|
||||
choose = function(frame, id, value)
|
||||
if id ~= "field" then return false end
|
||||
local info = ADFields.getInfo(value)
|
||||
if info == nil then return true end
|
||||
local field = ADFieldJobs.getField(value)
|
||||
frame.selectedFieldId = value
|
||||
frame.statusOverride = nil
|
||||
frame.guide = ADFieldGuide.pickField(frame.guide, field, frame:fieldGuideContext(frame.getAdEnv(), info, field.plan, field.plan.storeBuildingIds))
|
||||
return true
|
||||
end,
|
||||
canAdvance = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
startKey = function() return "spu_guide_runAuto" end,
|
||||
-- mål-afgrøde kører altid automatisk: ingen "Gem uden at starte"
|
||||
showSaveOnly = function(guide) return (guide.draft.plan.targetFruit or "") == "" end,
|
||||
save = function(frame, guide, start) frame:saveFieldGuide(guide, start) end,
|
||||
afterRows = function(frame, adEnv, guide)
|
||||
if guide.step ~= "summary" or guide.draft.fieldId == nil then return end
|
||||
local info = guide.context.info
|
||||
if info ~= nil and info.markerId == nil then
|
||||
frame:addTextRow(" " .. string.format(frame.i18n:getText("spu_guide_noMarker"), guide.draft.fieldId))
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- Gem: mål-afgrøde (hvis ændret), planen, og faste maskiner (fjernes fra andre marker).
|
||||
function SmartPickupFrame:saveFieldGuide(guide, start)
|
||||
local result = ADFieldGuide.result(guide, start)
|
||||
if result.fieldId == nil then return end
|
||||
if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
|
||||
ADFieldJobs.setPlan(result.fieldId, result.plan)
|
||||
-- markens form ændret siden kurserne blev lavet: gemte Courseplay-kurser til marken glemmes
|
||||
local fieldInfo = ADFields.getInfo(result.fieldId)
|
||||
if fieldInfo ~= nil and ADCourseCache ~= nil then ADCourseCache.checkField(result.fieldId, fieldInfo.shapeSig) end
|
||||
local byField = SmartPickupFrame.rigIdsByField()
|
||||
for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
|
||||
if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
|
||||
ADFieldJobs.setPlan(fieldId, {rigIds = ids})
|
||||
end
|
||||
end
|
||||
ADFieldJobs.log(result.fieldId, "guide gemt: mode=%s mål=%s faste=%s lager=%s", tostring(result.plan.mode), result.targetFruit ~= "" and result.targetFruit or "-",
|
||||
table.concat(result.rigIds, " "), table.concat(result.plan.storeBuildingIds or {}, " "))
|
||||
self.selectedFieldId = result.fieldId
|
||||
self.statusOverride = string.format(self.i18n:getText("spu_guide_saved"), result.fieldId)
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
end
|
||||
|
||||
-- ---- knapper og [Ret] ----
|
||||
|
||||
-- [Opsæt mark]: den valgte mark med de nuværende valg, ellers trin 1 (vælg mark).
|
||||
function SmartPickupFrame:onClickFieldSetup()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local info = frame.selectedFieldId ~= nil and ADFields.getInfo(frame.selectedFieldId) or nil
|
||||
frame.fieldView = nil
|
||||
if info == nil then
|
||||
frame:openGuide("field", ADFieldGuide.openPicker(frame:fieldGuideContext(adEnv, nil)))
|
||||
return
|
||||
end
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
frame:openGuide("field", ADFieldGuide.open(field, frame:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)))
|
||||
end)
|
||||
end
|
||||
|
||||
-- [Ret] på en resumé-linje i fanens oversigt: kun det ene trin; [Gem] gemmer og går tilbage.
|
||||
function SmartPickupFrame:openFieldStep(fieldId, step)
|
||||
local adEnv = self.getAdEnv()
|
||||
local info = ADFields.getInfo(fieldId)
|
||||
if adEnv == nil or info == nil then return end
|
||||
local field = ADFieldJobs.getField(fieldId)
|
||||
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
|
||||
self:openGuide("field", ADFieldGuide.open(field, context, {step = step, single = true, returnTo = "close"}))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickFieldFleet(show)
|
||||
self:safe(function(frame)
|
||||
frame.fieldView = show and "fleet" or nil
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,117 @@
|
||||
-- Smart Pickup — fanen Marksiloer
|
||||
-- Opsætning (til/fra, maks. vogne, mindste mængde, kør til produktion + foderreserve, gårdlagre) og status for marksilo-flowet.
|
||||
-- Klik går til onClickFieldSiloRow; reglerne ligger i ADFieldSilos/ADFieldSiloPlanner.
|
||||
|
||||
SmartPickupFrame.FIELDSILO_ROW_KINDS = {fsEnabled = true, fsMaxVehicles = true, fsMinLiters = true, fsStore = true,
|
||||
fsToProduction = true, fsReserveDays = true}
|
||||
|
||||
local function numberTexts(from, to)
|
||||
local texts = {}
|
||||
for n = from, to do table.insert(texts, tostring(n)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function literTexts()
|
||||
local texts = {}
|
||||
for _, liters in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do table.insert(texts, string.format("%d l", liters)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function dayTexts(i18n)
|
||||
local texts = {}
|
||||
for _, days in ipairs(ADFieldSiloConfig.RESERVE_DAYS_CHOICES) do table.insert(texts, string.format(i18n:getText("spu_fs_days"), days)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function indexOf(list, value)
|
||||
for index, entry in ipairs(list) do if entry == value then return index end end
|
||||
return 1
|
||||
end
|
||||
|
||||
-- Valg med flere værdier: productPrefab (MultiTextOption). ingredientPrefab er en BinaryOption med kun to sider.
|
||||
function SmartPickupFrame:addFieldSiloChoiceRow(kind, title, texts, state)
|
||||
local element = self:addRow(self.productPrefab, {kind = kind})
|
||||
local option = element:getDescendantByName("option")
|
||||
option:setTexts(texts)
|
||||
option:setState(state, false)
|
||||
element:getDescendantByName("title"):setText(title)
|
||||
local tooltip = option:getDescendantByName("ignore")
|
||||
if tooltip ~= nil then tooltip:setText("") end
|
||||
return option
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFieldSiloRows(adEnv)
|
||||
local i18n, cfg = self.i18n, ADFieldSilos.config
|
||||
local onOff = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||
self:addToggleRow("fsEnabled", i18n:getText("spu_fs_enabled"), onOff, cfg.enabled)
|
||||
self:addFieldSiloChoiceRow("fsMaxVehicles", i18n:getText("spu_fs_maxVehicles"), numberTexts(1, ADFieldSiloConfig.MAX_VEHICLES), cfg.maxVehicles)
|
||||
self:addFieldSiloChoiceRow("fsMinLiters", i18n:getText("spu_fs_minLiters"), literTexts(), indexOf(ADFieldSiloConfig.MIN_LITERS_CHOICES, cfg.minLiters))
|
||||
self:addToggleRow("fsToProduction", i18n:getText("spu_fs_toProduction"), onOff, cfg.toProduction)
|
||||
if cfg.toProduction then
|
||||
self:addFieldSiloChoiceRow("fsReserveDays", i18n:getText("spu_fs_reserveDays"), dayTexts(i18n), indexOf(ADFieldSiloConfig.RESERVE_DAYS_CHOICES, cfg.reserveDays))
|
||||
end
|
||||
|
||||
local plan = ADFieldSilos.lastPlan
|
||||
local waiting = 0
|
||||
for _, entry in ipairs(plan ~= nil and plan.status or {}) do if entry.state == "ok" then waiting = waiting + 1 end end
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_status"), ADFieldSilos.loanCount(), cfg.maxVehicles, waiting))
|
||||
if #cfg.storeIds == 0 then self:addTextRow(i18n:getText("spu_fs_noStores")) end
|
||||
local why = ADFieldSilos.lastVehicleReasons
|
||||
if waiting > 0 and ADFieldSilos.loanCount() < cfg.maxVehicles and why ~= nil and why.eligible == 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_noVehicle"), why.noPark, why.busy, why.controlled, why.notEnabled))
|
||||
end
|
||||
|
||||
if why ~= nil and (why.harvest or 0) > 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_harvestHeld"), why.harvest))
|
||||
end
|
||||
|
||||
self:addTextRow(i18n:getText("spu_fs_silosTitle"))
|
||||
if plan == nil or #plan.status == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_fs_nothing"))
|
||||
else
|
||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_fs_col_silo"), i18n:getText("spu_col_crop"), i18n:getText("spu_fs_col_liters"),
|
||||
i18n:getText("spu_fs_col_onWay"), i18n:getText("spu_fs_col_state"), ""})
|
||||
for _, entry in ipairs(plan.status) do
|
||||
local desc = g_fillTypeManager:getFillTypeByIndex(entry.fillType)
|
||||
local title = desc ~= nil and (desc.title or desc.name) or tostring(entry.fillType)
|
||||
local state = i18n:getText("spu_fs_state_" .. entry.state)
|
||||
if entry.state == "noFarmStore" then state = string.format(state, title) end
|
||||
if entry.toProduction then state = state .. " " .. i18n:getText("spu_fs_toFactory") end
|
||||
self:addCellRow(self.fieldPrefab, {tostring(entry.siloName), title, SmartPickupFrame.formatCompact(entry.liters or 0),
|
||||
SmartPickupFrame.formatCompact(entry.onWay or 0), state, ""})
|
||||
end
|
||||
end
|
||||
|
||||
self:addTextRow(i18n:getText("spu_fs_storesTitle"))
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
if building.kind == "silo" then
|
||||
local option = self:addToggleRow("fsStore", tostring(building.name), onOff, ADFieldSiloConfig.hasStore(cfg, building.id))
|
||||
self.rowData[option] = {kind = "fsStore", buildingId = building.id}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickFieldSiloRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local cfg = ADFieldSilos.config
|
||||
if data.kind == "fsEnabled" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {enabled = state == SmartPickupFrame.STATE_ON})
|
||||
elseif data.kind == "fsMaxVehicles" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {maxVehicles = state})
|
||||
elseif data.kind == "fsMinLiters" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {minLiters = ADFieldSiloConfig.MIN_LITERS_CHOICES[state]})
|
||||
elseif data.kind == "fsToProduction" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {toProduction = state == SmartPickupFrame.STATE_ON})
|
||||
elseif data.kind == "fsReserveDays" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {reserveDays = ADFieldSiloConfig.RESERVE_DAYS_CHOICES[state]})
|
||||
elseif data.kind == "fsStore" then
|
||||
cfg = ADFieldSiloConfig.toggleStore(cfg, data.buildingId)
|
||||
end
|
||||
ADFieldSilos.setConfig(cfg)
|
||||
pcall(ADFieldSilos.save)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv ~= nil then pcall(ADFieldSilos.planNow, adEnv) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -1,30 +1,20 @@
|
||||
-- Smart Pickup — fanen Marker
|
||||
-- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række
|
||||
-- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin,
|
||||
-- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben.
|
||||
-- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView.
|
||||
-- Oversigt: tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en
|
||||
-- række viser markens resumé (én linje pr. emne — klik på en linje for at rette netop det trin), hele
|
||||
-- statusteksten og markøren. [Opsæt mark] åbner guiden (gui/SmartPickupFieldGuide.lua + SmartPickupGuide.lua),
|
||||
-- [Markflåde] viser den fælles markflåde. Al logik ligger i ADFieldJobs/ADFieldModel/ADFieldView/ADFieldGuide.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.FIELD_ROW_KINDS = {
|
||||
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
|
||||
fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
|
||||
fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
|
||||
}
|
||||
-- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow.
|
||||
SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true, fleetManual = true}
|
||||
SmartPickupFrame.ROLE_KEYS = {
|
||||
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
|
||||
collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader",
|
||||
mulcher = "spu_field_role_mulcher", limer = "spu_field_role_limer", plow = "spu_field_role_plow",
|
||||
cultivator = "spu_field_role_cultivator", seeder = "spu_field_role_seeder", roller = "spu_field_role_roller",
|
||||
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
|
||||
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
|
||||
}
|
||||
SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
||||
-- simple Ja/Nej-valg på planen: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_TOGGLES = {
|
||||
fieldRake = "rake", fieldMulch = "mulch", fieldLime = "lime", fieldSow = "sow", fieldRoll = "roll",
|
||||
fieldFertilize = "fertilize", fieldWeed = "weed",
|
||||
}
|
||||
-- valg med flere muligheder: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit"}
|
||||
|
||||
-- ---- tekster ----
|
||||
|
||||
@@ -50,11 +40,32 @@ function SmartPickupFrame:soilText(info)
|
||||
return table.concat(texts, ", ")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldStatusText(field, info)
|
||||
-- Kort årsag til tabellens statuskolonne ("bredde ukendt" i stedet for hele forklaringen, som står i
|
||||
-- markens detaljer). Fritekst-årsager (fx "… er tom for X") forkortes til "se detaljer".
|
||||
function SmartPickupFrame:fieldReasonShort(reason)
|
||||
if type(reason) ~= "table" then return nil end
|
||||
if reason.text ~= nil then return self.i18n:getText("spu_field_reasonShort_text") end
|
||||
local shortKey = reason.key ~= nil and string.gsub(reason.key, "^spu_field_reason_", "spu_field_reasonShort_") or nil
|
||||
if shortKey ~= nil then
|
||||
local okHas, has = pcall(function() return self.i18n.hasText == nil or self.i18n:hasText(shortKey) end)
|
||||
if okHas and has then return self.i18n:getText(shortKey) end
|
||||
end
|
||||
return self:fieldReasonText(reason)
|
||||
end
|
||||
|
||||
-- short = tabellens korte udgave (trin + kort årsag); ellers hele teksten (markens detaljer).
|
||||
function SmartPickupFrame:fieldStatusText(field, info, short)
|
||||
local text = self.i18n:getText(ADFieldView.statusKey(field, info))
|
||||
if info.markerId == nil then
|
||||
return string.format(text, info.id)
|
||||
end
|
||||
if short then
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||
end
|
||||
local reason = self:fieldReasonShort(field.reason)
|
||||
return reason ~= nil and (text .. ": " .. reason) or text
|
||||
end
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||
end
|
||||
@@ -62,12 +73,24 @@ function SmartPickupFrame:fieldStatusText(field, info)
|
||||
if decision ~= nil and decision.care ~= nil and not ADFieldModel.isWorking(field) then
|
||||
text = text .. " — " .. string.format(self.i18n:getText("spu_field_careNeed"), self.i18n:getText("spu_field_stage_" .. decision.care))
|
||||
end
|
||||
local target = field.plan.targetFruit
|
||||
if target ~= nil and target ~= "" and info.kind == "bare" and field.stage == "idle" and not ADFieldJobs.environment().plantable(target) then
|
||||
text = text .. " — " .. string.format(self.i18n:getText("spu_field_waitSowing"), self:fruitTitle(target))
|
||||
end
|
||||
local reason = self:fieldReasonText(field.reason)
|
||||
if reason ~= nil then text = text .. ": " .. reason end
|
||||
return text
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fruitTitle(name)
|
||||
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldPlanText(field, info)
|
||||
if field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
||||
return "-> " .. self:fruitTitle(field.plan.targetFruit)
|
||||
end
|
||||
local text = self.i18n:getText(ADFieldView.planKey(field.plan, info.kind))
|
||||
if field.plan.rake and info.kind == "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_rake") end
|
||||
if field.plan.sow and info.kind ~= "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_sow") end
|
||||
@@ -81,187 +104,135 @@ function SmartPickupFrame:fieldCells(row)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
local name = string.format("%s%sF%d", info.id == self.selectedFieldId and "> " or "", info.markerId == nil and "! " or "", info.id)
|
||||
local crop = info.kind == "bare" and self.i18n:getText("spu_field_bare") or (info.cropTitle or "-")
|
||||
local name = string.format("%sF%d", info.markerId == nil and "! " or "", info.id)
|
||||
local crop = ADFieldView.cropText(info, function(key) return self.i18n:getText(key) end)
|
||||
return {
|
||||
name, crop, self:fieldPlanText(field, info),
|
||||
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
||||
self:fieldStatusText(field, info),
|
||||
self:fieldStatusText(field, info, true) .. ((ADFieldJobs.state.paused and not ADFieldJobs.isFieldResumed(info.id)
|
||||
and ADFieldModel.isWorking(field)) and (" " .. self.i18n:getText("spu_field_pausedSuffix")) or ""),
|
||||
ADFlowView.joinCounted(names, ", ", "-"),
|
||||
self:fieldActionText(info.id),
|
||||
}
|
||||
end
|
||||
|
||||
-- ---- rækker ----
|
||||
|
||||
-- Valg med flere muligheder (klon af productPrefab): values[i] er værdien for texts[i].
|
||||
function SmartPickupFrame:addChoiceRow(kind, title, texts, values, current)
|
||||
local row = self:addRow(self.productPrefab, {kind = kind, values = values})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts(texts)
|
||||
local index = 1
|
||||
for position, value in ipairs(values) do
|
||||
if value == current then index = position end
|
||||
end
|
||||
option:setState(index, false)
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
return option
|
||||
end
|
||||
|
||||
local function numberTexts(from, to)
|
||||
local texts, values = {}, {}
|
||||
for value = from, to do
|
||||
table.insert(texts, tostring(value))
|
||||
table.insert(values, value)
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- Afgrøder der kan sås (første valg: samme som den høstede).
|
||||
function SmartPickupFrame:sowFruitChoices()
|
||||
local texts, values = {self.i18n:getText("spu_field_sowSame")}, {""}
|
||||
for _, fruit in ipairs(g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}) do
|
||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
||||
table.insert(texts, tostring(fruit.fillType.title or fruit.name))
|
||||
table.insert(values, fruit.name)
|
||||
end
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- ---- opbygning ----
|
||||
|
||||
function SmartPickupFrame:fillFieldRows(adEnv)
|
||||
self.fieldRows = {}
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FIELDS) and self:fillGuideRows(adEnv) then return end
|
||||
if self.fieldView == "fleet" then
|
||||
self:fillFleetRows(adEnv)
|
||||
return
|
||||
end
|
||||
local i18n = self.i18n
|
||||
local infos = ADFields.list()
|
||||
if #infos == 0 then
|
||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||
else
|
||||
self:addTextRow(i18n:getText("spu_field_hint"))
|
||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors")})
|
||||
-- Stop alt-pausen gemmes med savegamet: sig det tydeligt her (in-game 29/9: "jeg har ikke trykket stop")
|
||||
if ADFieldJobs.state.paused then self:addTextRow(i18n:getText("spu_field_pausedBanner")) end
|
||||
-- markflåden på "kun med flueben" uden flueben: intet kan sendes ud (in-game 26/9 — alle marker "no machine")
|
||||
if ADFieldJobs.state.fleetMode == "ticked" and next(ADFieldJobs.state.fleet or {}) == nil then
|
||||
self:addTextRow(i18n:getText("spu_field_fleetEmpty"))
|
||||
end
|
||||
self:addCellRow(self.fieldListPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors"), ""})
|
||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields)) do
|
||||
local cells = self:addCellRow(self.fieldPrefab, self:fieldCells(row))
|
||||
local cells = self:addCellRow(self.fieldListPrefab, self:fieldCells(row))
|
||||
local fieldId = row.info.id
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
local actionCell = cells[7]
|
||||
self:makeRowClickable(cells[1].parent, function(frame)
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:requestRebuild()
|
||||
-- klik på [Start]/[Stop]-feltet = handling (kun denne mark); ellers åbn redigeringsvinduet
|
||||
if frame.lastClickX ~= nil and actionCell ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||
frame:toggleField(fieldId)
|
||||
else
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:openEditPanel("field", fieldId)
|
||||
end
|
||||
end)
|
||||
end
|
||||
table.insert(self.fieldRows, {id = fieldId, cells = cells})
|
||||
end
|
||||
end
|
||||
local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||
if selected ~= nil then
|
||||
self:fillFieldDetail(adEnv, selected)
|
||||
end
|
||||
self:fillFleetRows(adEnv)
|
||||
self:refreshFieldLevels()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFieldDetail(adEnv, info)
|
||||
-- Prik og statusfarve pr. mark i listen.
|
||||
function SmartPickupFrame:refreshFieldLevels()
|
||||
local unpackFn = table.unpack or unpack
|
||||
for _, entry in ipairs(self.fieldRows or {}) do
|
||||
local color = ADFlowView.LEVEL_COLORS[self:fieldLevel(entry.id)]
|
||||
local row = entry.cells[1] ~= nil and entry.cells[1].parent or nil
|
||||
local dot = row ~= nil and row:getDescendantByName("dot") or nil
|
||||
if dot ~= nil and dot.setImageColor ~= nil then dot:setImageColor(nil, unpackFn(color)) end
|
||||
if entry.cells[5] ~= nil and entry.cells[5].setTextColor ~= nil then entry.cells[5]:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Arbejder marken reelt (ikke bare et plan-trin under Stop alt-pause)?
|
||||
function SmartPickupFrame:fieldIsActive(id, field)
|
||||
field = field or ADFieldJobs.getField(id)
|
||||
return field ~= nil and ADFieldView.isActiveWork({working = ADFieldModel.isWorking(field),
|
||||
paused = ADFieldJobs.state.paused == true, resumed = ADFieldJobs.isFieldResumed(id)})
|
||||
end
|
||||
|
||||
-- [Start] kræver en markør (Stop/Overtag altid muligt når marken arbejder).
|
||||
function SmartPickupFrame:fieldCanStart(id)
|
||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
||||
if field == nil or info == nil then return false end
|
||||
return ADFieldView.canStart({hasMarker = info.markerId ~= nil,
|
||||
working = self:fieldIsActive(id, field) or ADFieldJobs.awaitingPlayer(id) ~= nil})
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldLevel(id)
|
||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
||||
if field == nil or info == nil then return "stopped" end
|
||||
local reason = type(field.reason) == "table" and field.reason.key or nil
|
||||
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
|
||||
paused = ADFieldJobs.state.paused == true, resumed = ADFieldJobs.isFieldResumed(id),
|
||||
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil,
|
||||
startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id) or false,
|
||||
reasonKey = reason})
|
||||
end
|
||||
|
||||
-- forButton: knaplinjens tekst (Start/Stop) i stedet for listens [Start]/[Stop].
|
||||
function SmartPickupFrame:fieldActionText(id, forButton)
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
|
||||
local isWorking = self:fieldIsActive(id, field)
|
||||
if forButton then return self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start") end
|
||||
return self.i18n:getText(isWorking and "spu_flowList_stop" or "spu_flowList_start")
|
||||
end
|
||||
|
||||
-- Den valgte marks resumé: én klikbar linje pr. emne ([Ret] åbner kun det trin), hele status, markør og
|
||||
-- advarsel om marksilo der ikke tømmes hjem.
|
||||
function SmartPickupFrame:fillFieldOverview(adEnv, info)
|
||||
local i18n = self.i18n
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
local plan = field.plan
|
||||
local yesNo = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||
local crop = info.kind == "bare" and i18n:getText("spu_field_bare") or tostring(info.cropTitle or "-")
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
|
||||
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
|
||||
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
|
||||
if info.kind == "mowable" then
|
||||
local isHay = ADFieldModel.product(plan, "mowable") == "hay"
|
||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
|
||||
else
|
||||
if info.kind == "harvest" then
|
||||
local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
|
||||
self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
|
||||
end
|
||||
self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
|
||||
self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
|
||||
local soilTexts = {}
|
||||
for _, value in ipairs(SmartPickupFrame.SOIL_VALUES) do table.insert(soilTexts, i18n:getText("spu_field_soil_" .. value)) end
|
||||
self:addChoiceRow("fieldSoil", i18n:getText("spu_field_soil"), soilTexts, SmartPickupFrame.SOIL_VALUES, plan.soil or "auto")
|
||||
self:addToggleRow("fieldSow", i18n:getText("spu_field_sow"), yesNo, plan.sow == true)
|
||||
if plan.sow then
|
||||
local fruitTexts, fruitValues = self:sowFruitChoices()
|
||||
self:addChoiceRow("fieldSowFruit", i18n:getText("spu_field_sowFruit"), fruitTexts, fruitValues, plan.sowFruit or "")
|
||||
self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
|
||||
end
|
||||
local crop = ADFieldView.cropText(info, function(key) return i18n:getText(key) end)
|
||||
if not self.editPanelOpen then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_field_section_care"))
|
||||
self:addToggleRow("fieldFertilize", i18n:getText("spu_field_fertilize"), yesNo, plan.fertilize ~= false)
|
||||
self:addToggleRow("fieldLime", i18n:getText("spu_field_lime"), yesNo, plan.lime ~= false)
|
||||
if info.kind ~= "mowable" then
|
||||
self:addToggleRow("fieldWeed", i18n:getText("spu_field_weed"), yesNo, plan.weed ~= false)
|
||||
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
|
||||
local guide = ADFieldGuide.open(field, context)
|
||||
local getText = function(key) return i18n:getText(key) end
|
||||
local fieldId = info.id
|
||||
for _, line in ipairs(ADFieldGuide.summary(guide)) do
|
||||
local step = line.step
|
||||
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||
frame:openFieldStep(fieldId, step)
|
||||
end)
|
||||
end
|
||||
local soil = self:soilText(info)
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_soilState"), soil ~= "" and soil or i18n:getText("spu_field_soilOk")))
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), self:fieldStatusText(field, info)))
|
||||
if info.markerId ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
|
||||
else
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_noMarker"), info.id))
|
||||
end
|
||||
if field.mowWidth ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_mowWidth"), field.mowWidth))
|
||||
end
|
||||
if field.plan.rake and field.windrowOffset ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_windrowOffset"), math.abs(field.windrowOffset),
|
||||
i18n:getText(math.abs(field.windrowOffset) <= 0.05 and "spu_field_side_center" or (field.windrowOffset > 0 and "spu_field_side_left" or "spu_field_side_right"))))
|
||||
end
|
||||
self:fillFieldStoreRows(adEnv, info, field)
|
||||
end
|
||||
|
||||
-- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer.
|
||||
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
local i18n = self.i18n
|
||||
local product = ADFieldModel.product(field.plan, info.kind)
|
||||
local fillType
|
||||
if info.kind == "mowable" then
|
||||
fillType = product == "hay" and info.hay or info.windrow
|
||||
else
|
||||
fillType = info.harvestFillType
|
||||
end
|
||||
if fillType == nil then return end
|
||||
self:addTextRow(i18n:getText("spu_field_section_store"))
|
||||
local chosen = {}
|
||||
for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local fieldSilos = {}
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do fieldSilos[building.id] = building end
|
||||
local fermenterIds = {}
|
||||
if info.kind == "mowable" and product == "silage" then
|
||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
||||
end
|
||||
local count = 0
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
||||
count = count + 1
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "fieldStore", buildingId = building.id})
|
||||
local suffix = fieldSilos[building.id] ~= nil and (" (" .. i18n:getText("spu_field_fieldSilo") .. ")") or ""
|
||||
if fermenterIds[building.id] then suffix = suffix .. " (" .. i18n:getText("spu_field_fermenter") .. ")" end
|
||||
row:getDescendantByName("title"):setText(" " .. tostring(building.name) .. suffix)
|
||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
if count == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_noStore"))
|
||||
elseif #field.plan.storeBuildingIds == 0 then
|
||||
self:addTextRow(" " .. i18n:getText((info.kind == "mowable" and product == "silage") and "spu_field_storeAutoSilage" or "spu_field_storeAuto"))
|
||||
end
|
||||
-- marksiloer skal tømmes hjem af et udkørselsflow
|
||||
for id, building in pairs(fieldSilos) do
|
||||
local emptied = false
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
for _, sourceId in ipairs(run.kind == "outbound" and run.sourceBuildingIds or {}) do
|
||||
if sourceId == id then emptied = true end
|
||||
end
|
||||
end
|
||||
if not emptied then
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_fieldSiloNotEmptied"), tostring(building.name)))
|
||||
end
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_guide_noMarker"), info.id))
|
||||
end
|
||||
-- marksilo-advarsler vises ikke her: de gælder ikke marken, og Field Silos-fanen tømmer marksiloerne selv
|
||||
end
|
||||
|
||||
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
||||
@@ -277,16 +248,28 @@ function SmartPickupFrame:rigDetails(rig)
|
||||
elseif rig.role == "weeder" then
|
||||
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
||||
end
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||
-- samme grænse som udsendelsen (50,0 m med afrunding er ikke "over 50 m")
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||
if (rig.width or 0) < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[rig.role] then table.insert(parts, i18n:getText("spu_field_widthUnknownShort")) end
|
||||
-- fast på en mark (valgt under markens "Faste maskiner") og hvor den arbejder nu
|
||||
for fieldId, field in pairs(ADFieldJobs.state.fields) do
|
||||
for _, id in ipairs(field.plan ~= nil and field.plan.rigIds or {}) do
|
||||
if id == rig.id then table.insert(parts, string.format(i18n:getText("spu_field_dedicatedElsewhere"), tostring(fieldId))) end
|
||||
end
|
||||
end
|
||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||
if assignment ~= nil then table.insert(parts, string.format("F%d", assignment.fieldId)) end
|
||||
if assignment ~= nil then table.insert(parts, string.format(i18n:getText("spu_field_rig_working"), tostring(assignment.fieldId))) end
|
||||
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
|
||||
end
|
||||
|
||||
-- Markflåden (egen visning): alle maskiner med flueben, "fast på F22", "i gang på F108", bredde-advarsler.
|
||||
function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_field_section_fleet"))
|
||||
local rigs = ADFieldJobs.getRigs(adEnv)
|
||||
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
|
||||
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
|
||||
local rigs = {}
|
||||
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do table.insert(rigs, rig) end
|
||||
table.sort(rigs, function(a, b)
|
||||
if a.role ~= b.role then return a.role < b.role end
|
||||
return ADFlowView.naturalLess(a.label, b.label)
|
||||
@@ -296,15 +279,55 @@ function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
local title = string.format(" %s — %s, %.1f m%s", tostring(rig.label), self:roleText(rig.role), rig.width or 0, self:rigDetails(rig))
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
row:getDescendantByName("option"):setState(rig.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
if rig.role == "harvester" then
|
||||
-- Ja = må køre selv på vejen (som resten af flåden: ja = må sendes ud); Nej = manuel transport —
|
||||
-- for bred med skærebord, spilleren kører den til marken (in-game 27/9: omvendt betydning forvirrede)
|
||||
local option = self:addToggleRow("fleetManual", string.format(i18n:getText("spu_field_manualTransport"), tostring(rig.label)),
|
||||
{i18n:getText("ui_no"), i18n:getText("ui_yes")}, not rig.manual)
|
||||
self.rowData[option] = {kind = "fleetManual", vehicleId = rig.id}
|
||||
end
|
||||
end
|
||||
if #rigs == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
|
||||
end
|
||||
if ADPool ~= nil and ADPool.overview ~= nil then self:fillPoolRows(adEnv) end
|
||||
end
|
||||
|
||||
-- Maskinpuljen: solo-traktorer (flueben som resten af flåden) og redskaber med status og hvorfor de ikke er med.
|
||||
function SmartPickupFrame:fillPoolRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
local ok, implements, tractors = pcall(ADPool.overview, adEnv)
|
||||
if not ok then return end
|
||||
local byLabel = function(a, b) return ADFlowView.naturalLess(a.label, b.label) end
|
||||
table.sort(tractors, byLabel)
|
||||
table.sort(implements, function(a, b)
|
||||
if a.role ~= b.role then return a.role < b.role end
|
||||
return byLabel(a, b)
|
||||
end)
|
||||
self:addTextRow(i18n:getText("spu_pool_section_tractors"))
|
||||
for _, tractor in ipairs(tractors) do
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
||||
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg))
|
||||
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", tostring(tractor.label), hp, status))
|
||||
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
||||
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
||||
for _, implement in ipairs(implements) do
|
||||
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg))
|
||||
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status))
|
||||
end
|
||||
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
||||
end
|
||||
|
||||
-- ---- live ----
|
||||
|
||||
function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
if (not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FIELDS)) or self.fieldView == "fleet" then
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
return
|
||||
end
|
||||
local rowsById = {}
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
|
||||
local working, total = 0, 0
|
||||
@@ -313,43 +336,32 @@ function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
if row ~= nil then
|
||||
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
||||
total = total + 1
|
||||
if ADFieldModel.isWorking(row.field) then working = working + 1 end
|
||||
if self:fieldIsActive(entry.id, row.field) then working = working + 1 end
|
||||
end
|
||||
end
|
||||
self:refreshFieldLevels()
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_field_status"), working, total))
|
||||
local selected = self.selectedFieldId ~= nil and ADFieldJobs.getField(self.selectedFieldId) or nil
|
||||
local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||
local isWorking = selected ~= nil and ADFieldModel.isWorking(selected)
|
||||
self.fieldStartButtonInfo.text = self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start")
|
||||
local awaiting = self.selectedFieldId ~= nil and ADFieldJobs.awaitingPlayer(self.selectedFieldId) ~= nil
|
||||
self.fieldStartButtonInfo.text = self.i18n:getText(awaiting and "spu_field_button_takeover"
|
||||
or (isWorking and "spu_field_button_stop" or "spu_field_button_start"))
|
||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
|
||||
end
|
||||
|
||||
-- ---- klik ----
|
||||
-- ---- klik (markflåden) ----
|
||||
|
||||
function SmartPickupFrame:onClickFieldRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local isOn = state == SmartPickupFrame.STATE_ON
|
||||
local id = frame.selectedFieldId
|
||||
if data.kind == "fleetVehicle" then
|
||||
ADFieldJobs.toggleFleet(data.vehicleId)
|
||||
elseif id == nil then
|
||||
return
|
||||
elseif SmartPickupFrame.PLAN_CHOICES[data.kind] ~= nil then
|
||||
local value = data.values ~= nil and data.values[state] or nil
|
||||
if value ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_CHOICES[data.kind]] = value}) end
|
||||
elseif data.kind == "fieldProduct" then
|
||||
ADFieldJobs.setPlan(id, {product = isOn and "hay" or "silage"})
|
||||
elseif data.kind == "fieldMode" then
|
||||
ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"})
|
||||
elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then
|
||||
ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn})
|
||||
elseif data.kind == "fieldStore" then
|
||||
local ids = {}
|
||||
for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do
|
||||
if buildingId ~= data.buildingId then table.insert(ids, buildingId) end
|
||||
end
|
||||
if isOn then table.insert(ids, data.buildingId) end
|
||||
ADFieldJobs.setPlan(id, {storeBuildingIds = ids})
|
||||
elseif data.kind == "fleetMode" then
|
||||
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
|
||||
elseif data.kind == "fleetManual" then
|
||||
local wantManual = not isOn -- Ja = kører selv på vejen
|
||||
if ((ADFieldJobs.state.manual or {})[data.vehicleId] == true) ~= wantManual then ADFieldJobs.toggleManual(data.vehicleId) end
|
||||
end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
@@ -357,13 +369,29 @@ end
|
||||
|
||||
-- Start nu: pleje hvis marken trænger (og vokser), ellers hele kæden (høst/slå, eller klargøring på bar mark).
|
||||
function SmartPickupFrame:onClickFieldStartStop()
|
||||
self:toggleField(self.selectedFieldId)
|
||||
end
|
||||
|
||||
-- Start/Stop/Overtag for én mark (rækkens felt, vinduets knap og knaplinjen).
|
||||
function SmartPickupFrame:toggleField(id)
|
||||
self:safe(function(frame)
|
||||
local id = frame.selectedFieldId
|
||||
if id == nil then return end
|
||||
if id == nil or not frame:fieldCanStart(id) then return end
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldModel.isWorking(field) then
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then
|
||||
-- manuel transport: overtag nu (skærebordet tjekkes stadig)
|
||||
ADFieldJobs.requestTakeover(id)
|
||||
frame.statusOverride = string.format(frame.i18n:getText("spu_field_takeoverRequested"), id)
|
||||
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
elseif frame:fieldIsActive(id, field) then
|
||||
ADFieldJobs.stopField(frame.getAdEnv(), id)
|
||||
ADFieldJobs.setFieldResumed(id, false)
|
||||
elseif ADFieldJobs.state.paused and ADFieldModel.isWorking(field) then
|
||||
-- Stop alt-pause: kun denne mark genoptages fra sit trin (de andre venter på Start alle)
|
||||
ADFieldJobs.setFieldResumed(id, true)
|
||||
frame.statusOverride = string.format(frame.i18n:getText("spu_field_startRequested"), id)
|
||||
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
else
|
||||
if ADFieldJobs.state.paused then ADFieldJobs.setFieldResumed(id, true) end
|
||||
local decision = ADFieldJobs.lastDecision[id]
|
||||
if decision == nil or decision.care == nil then
|
||||
ADFieldJobs.forceReady(id)
|
||||
@@ -375,3 +403,25 @@ function SmartPickupFrame:onClickFieldStartStop()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Redigeringsvinduet for en mark (gui/SmartPickupEditPanel.lua).
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
||||
canDelete = false,
|
||||
exists = function(frame, id) return ADFields.getInfo(id) ~= nil and ADFieldJobs.getField(id) ~= nil end,
|
||||
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
|
||||
fill = function(frame, adEnv, id)
|
||||
frame:fillFieldOverview(adEnv, ADFields.getInfo(id))
|
||||
frame:addLinkRow(" " .. frame.i18n:getText("spu_guide_field_open"), function(f)
|
||||
f.selectedFieldId = id
|
||||
f:onClickFieldSetup()
|
||||
end)
|
||||
end,
|
||||
status = function(frame, adEnv, id)
|
||||
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), true), level = frame:fieldLevel(id)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id, true) end,
|
||||
startStop = function(frame, id) frame:toggleField(id) end,
|
||||
canStart = function(frame, id) return frame:fieldCanStart(id) end,
|
||||
delete = function() end,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
-- Smart Pickup — fanen Flows: tavle + resumé, og guiden "Opsæt flow"
|
||||
-- Tavlen øverst (én række pr. flow), det valgte flows resumé (klik på en linje = ret ét trin; Navn = omdøb)
|
||||
-- og status. [Opsæt flow] / [Nyt flow] åbner guiden på den fælles trin-skærm (gui/SmartPickupGuide.lua).
|
||||
-- Henter det spil-afhængige som `context` til ADFlowGuide og gemmer via ADRunsController.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
function SmartPickupFrame:initFlowButtons(i18n)
|
||||
self.flowSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_flow_open"), callback = function() self:onClickFlowSetup() end}
|
||||
self.flowNewButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_new"), callback = function() self:onClickFlowNew() end}
|
||||
-- Flows-fanen: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; omdøb ligger på resuméets Navn-linje
|
||||
-- v1.58: listen har Start/Stop pr. række; Opsæt/Slet ligger i redigeringsvinduet
|
||||
self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}
|
||||
end
|
||||
|
||||
-- ---- traktor-tekst (også til guidens traktorliste) ----
|
||||
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — OK" / "… — X ingen vogn".
|
||||
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local parts = {label}
|
||||
if validation.isAutoload and validation.capacity == nil then
|
||||
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
|
||||
elseif validation.capacity ~= nil then
|
||||
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
|
||||
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
|
||||
if carriable ~= nil then
|
||||
table.insert(parts, carriable)
|
||||
end
|
||||
end
|
||||
if validation.ok then
|
||||
table.insert(parts, "OK")
|
||||
else
|
||||
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
|
||||
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
|
||||
reason = string.format(reason, tostring(validation.missingBuilding))
|
||||
end
|
||||
table.insert(parts, "X " .. reason)
|
||||
end
|
||||
return table.concat(parts, " — ")
|
||||
end
|
||||
|
||||
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
|
||||
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
|
||||
local canCarry = {}
|
||||
for _, fillType in ipairs(carriable or {}) do
|
||||
canCarry[fillType] = true
|
||||
end
|
||||
local titles = {}
|
||||
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
|
||||
if canCarry[fillType] then
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||||
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
|
||||
end
|
||||
end
|
||||
if #titles == 0 then
|
||||
return nil
|
||||
end
|
||||
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
|
||||
end
|
||||
|
||||
-- ---- context til ADFlowGuide ----
|
||||
|
||||
local function trim(text)
|
||||
return (string.gsub(tostring(text or ""), "^%s+", ""))
|
||||
end
|
||||
|
||||
local function idSet(list)
|
||||
local set = {}
|
||||
for _, id in ipairs(list or {}) do set[id] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowBuildingContext(adEnv, draft, farmId)
|
||||
local isOut = draft.kind == "outbound"
|
||||
local role = isOut and "load" or "unload"
|
||||
local chosen = idSet(isOut and draft.sourceBuildingIds or draft.targetBuildingIds)
|
||||
local buildings, found = {}, {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local fits = (isOut and building.canBeSource) or (not isOut and building.canBeTarget)
|
||||
if fits or chosen[building.id] then
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}
|
||||
found[building.id] = true
|
||||
table.insert(buildings, {id = building.id, name = tostring(building.name), label = markers[1] ~= nil and tostring(markers[1].name) or nil,
|
||||
hasMarker = markers[1] ~= nil, text = trim(self:describeBuilding(building, role))})
|
||||
end
|
||||
end
|
||||
-- valgte bygninger der ikke længere findes (solgt): kan fravælges
|
||||
for id in pairs(chosen) do
|
||||
if not found[id] then
|
||||
local gone = self.i18n:getText("spu_buildingGone")
|
||||
table.insert(buildings, {id = id, name = gone, hasMarker = false, text = gone})
|
||||
end
|
||||
end
|
||||
return buildings
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowStoreContext(adEnv, preview, farmId)
|
||||
local goods = ADRunsController.getStoreGoods(adEnv, preview)
|
||||
local chosen = idSet(preview.storeBuildingIds)
|
||||
local result = {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isStoreKind = building.kind == "silo" or building.kind == "factory" or building.kind == "objectStorage"
|
||||
if isStoreKind and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, goods)) then
|
||||
table.insert(result, {id = building.id, name = tostring(building.name), text = trim(self:describeBuilding(building, "unload"))})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Varer (fillType-indeks) flowet kører med: udkørsel = valgte produkter, forsyning = valgte/målets varer.
|
||||
-- Tom liste = ukendt endnu (ingen mål valgt) -> enhver vogn med last-enheder tæller.
|
||||
local function flowFillTypes(adEnv, preview)
|
||||
local names = {}
|
||||
if preview.kind == "outbound" then
|
||||
for _, product in ipairs(preview.products or {}) do table.insert(names, product.fillType) end
|
||||
elseif preview.ingredientMode == "manual" then
|
||||
names = preview.ingredients or {}
|
||||
else
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do table.insert(names, fillType.name) end
|
||||
end
|
||||
local result = {}
|
||||
for _, name in ipairs(names) do
|
||||
local index = g_fillTypeManager:getFillTypeIndexByName(tostring(name))
|
||||
if index ~= nil then table.insert(result, index) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function carriesAny(carriable, wanted)
|
||||
if #carriable == 0 then return false end
|
||||
if #wanted == 0 then return true end
|
||||
for _, a in ipairs(carriable) do
|
||||
for _, b in ipairs(wanted) do if a == b then return true end end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Ledige vogne til solo-traktorer: frakoblet, egen AD-parkering med "Connect"-markør, ikke holdt af et andet flow
|
||||
-- og kan bære flowets varer. Bedste først (navnematch, så "Disponibel"). In-game 29/9: flowet kunne ikke oprettes
|
||||
-- uden at omdøbe en vogns parkering til flowets navn.
|
||||
function SmartPickupFrame:flowWagonContext(adEnv, preview)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local wanted = flowFillTypes(adEnv, preview)
|
||||
local candidates = {}
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local ok, entry = pcall(function()
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
if any.spec_motorized ~= nil or any.spec_attachable == nil or type(index) ~= "number" or index < 1 then return nil end
|
||||
if any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil then return nil end
|
||||
if any.getOwnerFarmId == nil or any:getOwnerFarmId() ~= farmId then return nil end
|
||||
local id = ADRunsController.getVehicleId(any)
|
||||
local park = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
if id == nil or park == nil or ADPool.connectFor(park.name, markers) == nil then return nil end
|
||||
local owner = ADFlowWagons.ownerOf(ADRunsController.store.runs, id)
|
||||
if owner ~= nil and owner.id ~= preview.id then return nil end
|
||||
if not carriesAny(ADRunsController.getCarriableFillTypes(adEnv, any), wanted) then return nil end
|
||||
return {id = id, label = tostring(park.name)}
|
||||
end)
|
||||
if ok and entry ~= nil then table.insert(candidates, entry) end
|
||||
end
|
||||
return ADFlowWagons.rankWagons(preview.name, candidates)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowVehicleContext(adEnv, preview, wagons)
|
||||
local result = {}
|
||||
local hasWagons = wagons ~= nil and #wagons > 0
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local ok, entry = pcall(function()
|
||||
local text = self:describeVehicle(adEnv, preview, vehicle, false)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, preview, vehicle)
|
||||
local other = ADRunsController.getRunForVehicle(vehicle)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
-- solo-traktor (ingen vogn bagpå, kan trække): valgbar når en ledig vogn findes — den hentes ved Connect
|
||||
local solo = validation.reason == "noTrailer" and ADPool.canPull(ADPool.ownRole(vehicle))
|
||||
if solo and hasWagons then text = string.format(self.i18n:getText("spu_guide_flow_soloText"), tostring(label)) end
|
||||
local blocked = SmartPickupFrame.VEHICLE_BLOCKING_REASONS[validation.reason] == true and not (solo and hasWagons)
|
||||
return {id = ADRunsController.getVehicleId(vehicle), text = text, label = label, otherRunId = other ~= nil and other.id or nil,
|
||||
otherRunName = other ~= nil and other.name or nil, blocked = blocked, solo = solo}
|
||||
end)
|
||||
if ok and entry.id ~= nil then
|
||||
table.insert(result, entry)
|
||||
elseif not ok then
|
||||
Logging.warning(SmartPickupFrame.LOG_PREFIX .. "traktor '%s' kunne ikke vises i flowets traktorliste: %s",
|
||||
tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), tostring(entry))
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- draft: kladden (eller selve run'et — samme feltnavne).
|
||||
function SmartPickupFrame:flowGuideContext(adEnv, draft)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local preview = ADFlowGuide.previewRun(draft, function(buildingId, role)
|
||||
local building = ADBuildings.find(adEnv, farmId, buildingId)
|
||||
local ids = {}
|
||||
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}) do
|
||||
table.insert(ids, marker.id)
|
||||
end
|
||||
return ids
|
||||
end)
|
||||
local context = {buildings = self:flowBuildingContext(adEnv, draft, farmId), fillTypes = {}, products = {}, storeBuildings = {},
|
||||
waitPools = ADRunsController.getWaitPools(adEnv)}
|
||||
context.wagons = self:flowWagonContext(adEnv, preview)
|
||||
context.vehicles = self:flowVehicleContext(adEnv, preview, context.wagons)
|
||||
if draft.kind == "outbound" then
|
||||
for _, product in ipairs(ADRunsController.getRunProducts(adEnv, preview)) do
|
||||
local title = tostring(product.title)
|
||||
table.insert(context.products, {name = product.name, title = title,
|
||||
text = string.format("%s — %s l", title, SmartPickupFrame.formatCompact(product.available or 0))})
|
||||
end
|
||||
context.storeBuildings = self:flowStoreContext(adEnv, preview, farmId)
|
||||
else
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do
|
||||
table.insert(context.fillTypes, {name = fillType.name, title = tostring(fillType.title)})
|
||||
end
|
||||
end
|
||||
return context
|
||||
end
|
||||
|
||||
-- ---- fanen: tavle + resumé ----
|
||||
|
||||
-- Flows: det valgte flow øverst (indstillinger som "ret"-linjer + status), derunder en kort liste til at vælge
|
||||
-- et andet flow. Tavlen med alle flows står i Oversigt (in-game 27/9: de to faner så ens ud).
|
||||
function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
|
||||
self.flowListRows = {}
|
||||
local i18n = self.i18n
|
||||
if #ADRunsController.store.runs == 0 then
|
||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||
return
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
for _, run in ipairs(group.runs) do
|
||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
|
||||
local runId = run.id
|
||||
local actionCell = entry.cells[5]
|
||||
self:makeRowClickable(row, function(frame)
|
||||
-- klik på [Start]/[Stop]-feltet = handling (kun dette flow); ellers åbn redigeringsvinduet
|
||||
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||
frame:toggleRun(frame.getAdEnv(), runId)
|
||||
frame:requestRebuild()
|
||||
else
|
||||
frame.selectedRunId = runId
|
||||
frame:openEditPanel("flow", runId)
|
||||
end
|
||||
end)
|
||||
table.insert(self.flowListRows, entry)
|
||||
end
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
end
|
||||
|
||||
-- Listens celler, prik og statusfarve (hvert sekund og efter opbygning).
|
||||
function SmartPickupFrame:refreshFlowListRows(adEnv)
|
||||
local unpackFn = table.unpack or unpack
|
||||
for _, entry in ipairs(self.flowListRows or {}) do
|
||||
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
|
||||
if run ~= nil then
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||
entry.cells[1]:setText(tostring(run.name))
|
||||
entry.cells[2]:setText(tostring(cells[2]))
|
||||
entry.cells[3]:setText(tostring(cells[5]))
|
||||
entry.cells[4]:setText(tostring(cells[6]))
|
||||
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
|
||||
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
|
||||
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Start/Stop for ét flow (rækkens felt og vinduets knap). Husker om sidste start blev helt afvist (rød).
|
||||
function SmartPickupFrame:toggleRun(adEnv, runId)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if adEnv == nil or run == nil then return end
|
||||
self.lastStartFailed = self.lastStartFailed or {}
|
||||
if ADRunsController.getRunStatus(adEnv, run).canStop then
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
self.lastStartFailed[run.id] = nil
|
||||
self.statusOverride = nil
|
||||
else
|
||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||
self.lastStartFailed[run.id] = started == 0 and skipped > 0
|
||||
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
|
||||
self:showOverride(text, reasons)
|
||||
-- statuslinjen kan ligge uden for skærmen: sprunget over vises også som besked i højre side
|
||||
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
|
||||
end
|
||||
end
|
||||
ADRunsController.save()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowLevel(adEnv, run)
|
||||
local row = ADRunsController.getFlowRow(adEnv, run)
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
-- en gammel afvisning gælder ikke længere når flowet kører (fx startet fra Oversigt bagefter)
|
||||
if row.running > 0 and self.lastStartFailed ~= nil then self.lastStartFailed[run.id] = nil end
|
||||
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
|
||||
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
|
||||
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFlowSummary(adEnv, run)
|
||||
local i18n = self.i18n
|
||||
if not self.editPanelOpen then
|
||||
self:addTextRow(string.format(i18n:getText("spu_guide_flow_section"), tostring(run.name)))
|
||||
end
|
||||
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||
local guide = ADFlowGuide.open(run, self:flowGuideContext(adEnv, run))
|
||||
local getText = function(key) return i18n:getText(key) end
|
||||
local runId = run.id
|
||||
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||
local step = line.step
|
||||
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||
if step == "name" then frame:onClickRename() else frame:openFlowStep(runId, step) end
|
||||
end)
|
||||
end
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), tostring(cells[6])))
|
||||
self:addLinkRow(" " .. i18n:getText("spu_guide_flow_open"), function(frame)
|
||||
frame.selectedRunId = runId
|
||||
frame:onClickFlowSetup()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Redigeringsvinduet for et flow (gui/SmartPickupEditPanel.lua).
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
|
||||
canDelete = true,
|
||||
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
|
||||
title = function(frame, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
|
||||
end,
|
||||
fill = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
|
||||
end,
|
||||
status = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
|
||||
end,
|
||||
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
|
||||
delete = function(frame, id)
|
||||
frame.selectedRunId = id
|
||||
frame:onClickDelete()
|
||||
end,
|
||||
}
|
||||
|
||||
-- Hvert sekund: tavlens celler, statuslinje og knapper.
|
||||
function SmartPickupFrame:refreshFlowTab(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
return
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
local running, total = 0, 0
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
total = total + 1
|
||||
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
|
||||
end
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
|
||||
end
|
||||
|
||||
-- ---- hooks til den fælles guide-skærm ----
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
SmartPickupFrame.GUIDE_HOOKS.flow = {
|
||||
model = ADFlowGuide,
|
||||
tab = SmartPickupFrame.TAB_FLOWS,
|
||||
heading = function(frame, guide)
|
||||
return string.format(frame.i18n:getText("spu_guide_flow_title"), tostring(guide.draft.name))
|
||||
end,
|
||||
context = function(frame, adEnv, guide)
|
||||
-- flowet er slettet mens guiden var åben
|
||||
if not guide.draft.isNew and ADRuns.getRun(ADRunsController.store, guide.draft.id) == nil then return nil end
|
||||
return frame:flowGuideContext(adEnv, guide.draft)
|
||||
end,
|
||||
choose = function(frame, id)
|
||||
if id ~= "rename" then return false end
|
||||
TextInputDialog.show(SmartPickupFrame.onGuideNameResult, frame, frame.guide.draft.name, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
||||
return true
|
||||
end,
|
||||
startKey = function() return "spu_guide_flow_start" end,
|
||||
save = function(frame, guide, start) frame:saveFlowGuide(guide, start) end,
|
||||
}
|
||||
|
||||
function SmartPickupFrame:onGuideNameResult(value, clickOk)
|
||||
self:safe(function(frame)
|
||||
if not clickOk or frame.guide == nil or frame.guideKind ~= "flow" then return end
|
||||
frame.guide = ADFlowGuide.choose(frame.guide, "name", value)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Gem: nyt flow oprettes først nu; felterne via updateRun; traktorer tildeles/fjernes; markører findes.
|
||||
-- start = true: flowet startes bagefter ([Start flow]).
|
||||
function SmartPickupFrame:saveFlowGuide(guide, start)
|
||||
local adEnv = self.getAdEnv()
|
||||
local result = ADFlowGuide.result(guide)
|
||||
local runId = result.runId
|
||||
local oldIds = {}
|
||||
if result.isNew then
|
||||
runId = ADRunsController.createRun(result.name, result.kind).id
|
||||
else
|
||||
local existing = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if existing == nil then return end
|
||||
oldIds = existing.vehicleIds
|
||||
end
|
||||
ADRunsController.updateRun(runId, result.changes)
|
||||
local added, removed = ADFlowGuide.vehicleChanges(oldIds, result.vehicleIds)
|
||||
for _, id in ipairs(removed) do
|
||||
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||
if vehicle ~= nil then
|
||||
ADRunsController.unassignVehicle(vehicle)
|
||||
else
|
||||
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, id)
|
||||
end
|
||||
end
|
||||
for _, id in ipairs(added) do
|
||||
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||
if vehicle ~= nil then ADRunsController.assignVehicle(runId, vehicle) end
|
||||
end
|
||||
-- solo-traktorernes vogne fra guiden: flowet husker dem straks og henter dem ved Connect ved start
|
||||
local savedRun = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if savedRun ~= nil then
|
||||
local wagons = ADRuns.copyWagons(savedRun.wagons)
|
||||
for tractorId, wagonId in pairs(result.wagons or {}) do wagons[tractorId] = wagonId end
|
||||
savedRun.wagons = wagons
|
||||
end
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, runId))
|
||||
self.selectedRunId = runId
|
||||
local saved = string.format(self.i18n:getText("spu_guide_flow_saved"), tostring(result.name))
|
||||
if start and adEnv ~= nil and not ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runId)).isRunning then
|
||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, runId))
|
||||
self:showOverride(saved .. " — " .. string.format(self.i18n:getText("spu_status_started"), started, skipped), reasons)
|
||||
else
|
||||
self:showOverride(saved)
|
||||
end
|
||||
ADRunsController.save()
|
||||
end
|
||||
|
||||
-- ---- knapper og [Ret] ----
|
||||
|
||||
function SmartPickupFrame:onClickFlowSetup()
|
||||
self:safe(function(frame)
|
||||
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||
if adEnv == nil or run == nil then return end
|
||||
frame:openGuide("flow", ADFlowGuide.open(run, frame:flowGuideContext(adEnv, run)))
|
||||
end)
|
||||
end
|
||||
|
||||
-- [Nyt flow] (også fra fanen Oversigt): guiden på Flows-fanen; intet oprettes før [Gem].
|
||||
function SmartPickupFrame:onClickFlowNew()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local name = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
||||
local guide = ADFlowGuide.openNew(name, {})
|
||||
guide = ADGuide.withContext(guide, frame:flowGuideContext(adEnv, guide.draft))
|
||||
if frame.currentTab ~= SmartPickupFrame.TAB_FLOWS then frame:switchTab(SmartPickupFrame.TAB_FLOWS) end
|
||||
frame:openGuide("flow", guide)
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openFlowStep(runId, step)
|
||||
local adEnv = self.getAdEnv()
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if adEnv == nil or run == nil then return end
|
||||
self:openGuide("flow", ADFlowGuide.open(run, self:flowGuideContext(adEnv, run), {step = step, single = true, returnTo = "close"}))
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
-- Smart Pickup — fanen Flows
|
||||
-- Tavle øverst: én tekstrække pr. flow (Vare · Fra → Til · Regel · Traktorer · Status), det valgte
|
||||
-- markeret med ">". Under tavlen: detail-blok for det valgte flow — Retning, Fra/Til som BYGNINGER
|
||||
-- (markøren findes automatisk), Varer, Regler, Vent ved, Traktorer, Status — og Wait-puljerne.
|
||||
-- Tavlen (én række pr. flow, det valgte markeret med ">") bruges af fanerne Oversigt og Flows; Flows-fanens
|
||||
-- resumé og guiden "Opsæt flow" ligger i gui/SmartPickupFlowGuide.lua. Her også bygnings-teksten og de
|
||||
-- gamle klik-callbacks for retning/bygninger.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
-- ---- tavlen ----
|
||||
@@ -49,7 +49,11 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
||||
end
|
||||
local rule
|
||||
if row.kind == "outbound" then
|
||||
rule = string.format(i18n:getText("spu_flow_ruleOut"), row.sellAtPercent or 90, row.storeAbovePercent or 80)
|
||||
local sells, stores = ADFlowView.policyUse(row.policies)
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
||||
rule = table.concat(parts, ", ")
|
||||
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
||||
else
|
||||
rule = row.loops ~= nil and row.loops > 0 and string.format(i18n:getText("spu_flow_ruleInLoops"), row.loops) or i18n:getText("spu_flow_ruleIn")
|
||||
@@ -80,8 +84,12 @@ function SmartPickupFrame:makeRowClickable(row, action)
|
||||
row.mouseEvent = function(element, posX, posY, isDown, isUp, button, eventUsed)
|
||||
local used = baseMouseEvent(element, posX, posY, isDown, isUp, button, eventUsed)
|
||||
-- også inden for listens synlige udsnit: rækker scrollet ud af syne må ikke fange klik
|
||||
if not used and not eventUsed and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
||||
and isInside(element, posX, posY) and isInside(frame.boxLayout, posX, posY) then
|
||||
-- klik tæller inde i rækkens eget layout (listen eller redigeringsvinduet); listen er død mens vinduet er åbent
|
||||
local layout = element.spuLayout or frame.boxLayout
|
||||
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
|
||||
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
||||
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
|
||||
frame.lastClickX = posX
|
||||
frame.pendingClick = action
|
||||
return true
|
||||
end
|
||||
@@ -137,115 +145,6 @@ function SmartPickupFrame:describeBuilding(building, role)
|
||||
return string.format(" %s — %s", building.name, table.concat(parts, " · "))
|
||||
end
|
||||
|
||||
-- Afkrydsning pr. bygning: kilder (udkørsel, role "load") eller mål (forsyning, role "unload").
|
||||
function SmartPickupFrame:fillBuildingRows(adEnv, run, role)
|
||||
if ADBuildings == nil then
|
||||
self:addTextRow(self.i18n:getText("spu_status_noAd"))
|
||||
return
|
||||
end
|
||||
local chosen = {}
|
||||
local ids = role == "load" and run.sourceBuildingIds or role == "store" and run.storeBuildingIds or run.targetBuildingIds
|
||||
local rowKind = role == "load" and "sourceBuilding" or role == "store" and "storeBuilding" or "targetBuilding"
|
||||
for _, id in ipairs(ids or {}) do chosen[id] = true end
|
||||
local count = 0
|
||||
-- kandidater -> valgte først, så med markør; uden markør kun med "Vis alle bygninger"
|
||||
local candidates = {}
|
||||
-- "Lager til": kun bygninger der kan tage flowets lagervarer (æg-paller -> palleskure der tager æg)
|
||||
local storeGoods = role == "store" and ADRunsController.getStoreGoods(adEnv, run) or nil
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, g_currentMission:getFarmId())) do
|
||||
local isStoreKind = building.kind == "silo" or building.kind == "factory" or building.kind == "objectStorage"
|
||||
local fits = (role == "load" and building.canBeSource) or (role == "unload" and building.canBeTarget)
|
||||
or (role == "store" and isStoreKind and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, storeGoods)))
|
||||
if fits then
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role == "store" and "unload" or role) or {}
|
||||
table.insert(candidates, {id = building.id, name = building.name, hasMarker = markers[1] ~= nil, building = building})
|
||||
end
|
||||
end
|
||||
local shown = ADFlowView.buildingRows(candidates, chosen, self.showAllBuildings == true)
|
||||
local hiddenBuildings = ADFlowView.hiddenCount(candidates, shown)
|
||||
if hiddenBuildings > 0 or self.showAllBuildings then
|
||||
self:addToggleRow("showAllBuildings", string.format(self.i18n:getText("spu_showAllBuildings"), hiddenBuildings), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllBuildings == true)
|
||||
end
|
||||
for _, entry in ipairs(shown) do
|
||||
local building = entry.building
|
||||
count = count + 1
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = building.id})
|
||||
row:getDescendantByName("title"):setText(self:describeBuilding(building, role == "store" and "unload" or role))
|
||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
-- valgte bygninger der ikke længere findes (solgt)
|
||||
for _, id in ipairs(ids or {}) do
|
||||
if ADBuildings.find(adEnv, g_currentMission:getFarmId(), id) == nil then
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = id})
|
||||
row:getDescendantByName("title"):setText(string.format(" %s — %s", tostring(id), self.i18n:getText("spu_buildingGone")))
|
||||
row:getDescendantByName("option"):setState(SmartPickupFrame.STATE_ON, false)
|
||||
end
|
||||
end
|
||||
if count == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_noBuildings"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:rebuildFlowDetail(adEnv, run)
|
||||
ADRunsController.resolveMarkers(adEnv, run)
|
||||
run = self:getRun()
|
||||
local isOut = run.kind == "outbound"
|
||||
self:addTextRow(string.format(self.i18n:getText("spu_section_flow"), run.name))
|
||||
self.directionRowOption = self:addToggleRow("direction", self.i18n:getText("spu_direction"), {self.i18n:getText("spu_direction_in"), self.i18n:getText("spu_direction_out")}, isOut)
|
||||
if isOut then
|
||||
self:addTextRow(self.i18n:getText("spu_section_from"))
|
||||
self:fillBuildingRows(adEnv, run, "load")
|
||||
self:addTextRow(self.i18n:getText("spu_section_products"))
|
||||
self:fillProductRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_rules"))
|
||||
self.sellAtOption:setState(SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT), false)
|
||||
self.storeAboveOption:setState(SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT), false)
|
||||
self.collectRoundRowOption = self:addToggleRow("collectRound", self.i18n:getText("spu_collectRound"), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, run.collectRound == true)
|
||||
self:addTextRow(self.i18n:getText("spu_section_store"))
|
||||
self:fillBuildingRows(adEnv, run, "store")
|
||||
self:fillOutboundStatusRows(adEnv, run)
|
||||
else
|
||||
self:addTextRow(self.i18n:getText("spu_section_to"))
|
||||
self:fillBuildingRows(adEnv, run, "unload")
|
||||
self:addTextRow(self.i18n:getText("spu_section_targets"))
|
||||
self.ingredientModeRowOption = self:addToggleRow("ingredientMode", self.i18n:getText("spu_ingredientMode"), {self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")}, run.ingredientMode == "manual")
|
||||
self:fillIngredientRows(adEnv, run)
|
||||
self:fillNeedsRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_rules"))
|
||||
self.loopsOption:setState(run.loops + 1, false)
|
||||
self.returnBelowOption:setState(self:getReturnBelowIndex(run), false)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- callbacks ----
|
||||
|
||||
function SmartPickupFrame:onClickDirection(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {kind = state == SmartPickupFrame.STATE_ON and "outbound" or "supply"})
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickBuilding(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
if data.kind == "sourceBuilding" then
|
||||
ADRunsController.store = ADRuns.toggleSourceBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
elseif data.kind == "storeBuilding" then
|
||||
ADRunsController.store = ADRuns.toggleStoreBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
local updatedStore = ADRuns.getRun(ADRunsController.store, run.id)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': storeBuildings=%s", tostring(run.name), table.concat(updatedStore.storeBuildingIds or {}, " "))
|
||||
frame:requestRebuild()
|
||||
return
|
||||
else
|
||||
ADRunsController.store = ADRuns.toggleTargetBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
end
|
||||
local updated = ADRuns.getRun(ADRunsController.store, run.id)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': %s=%s", tostring(run.name), data.kind == "sourceBuilding" and "sourceBuildings" or "targetBuildings", table.concat(data.kind == "sourceBuilding" and updated.sourceBuildingIds or updated.targetBuildingIds, " "))
|
||||
ADRunsController.resolveMarkers(frame.getAdEnv(), updated)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -11,17 +11,21 @@ SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
|
||||
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
||||
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
||||
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
|
||||
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS = 1, 2, 3
|
||||
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5
|
||||
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
|
||||
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
||||
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
||||
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
||||
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
||||
SmartPickupFrame.TAB_KINDS = {[4] = "factory", [5] = "silo", [6] = "husbandry", [7] = "objectStorage"}
|
||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
||||
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
||||
|
||||
function SmartPickupFrame.getTabKind(tab)
|
||||
return SmartPickupFrame.TAB_KINDS[tab]
|
||||
end
|
||||
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||
-- Afvisningsgrunde der handler om VOGNEN (ikke run'ets opsætning): traktoren kan ikke vælges.
|
||||
SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true}
|
||||
SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true, autoloadOn = true}
|
||||
|
||||
function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
|
||||
@@ -32,6 +36,7 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
self.runIds, self.targetIds, self.poolGroups = {}, {}, {}
|
||||
self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...}
|
||||
self.clonedRows = {}
|
||||
self.panelRows = {}
|
||||
self.refreshTimer = 0
|
||||
self.statusOverride = nil
|
||||
self.statusRow = nil -- Text-elementet i den klonede statusrække
|
||||
@@ -49,11 +54,21 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
-- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow.
|
||||
self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end}
|
||||
self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end}
|
||||
self.monthButtons = {self.backButtonInfo}
|
||||
-- bygningsfanerne (Siloer/Stalde/Fabrikker/Palleskure): omdøb den valgte bygning
|
||||
self.renameBuildingButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_renameBuilding"), callback = function() self:onClickRenameBuilding() end}
|
||||
self.storageButtons = {self.backButtonInfo, self.renameBuildingButtonInfo}
|
||||
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
|
||||
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
|
||||
-- Stop alt og parkér: flows + markarbejde + marksilo stoppes, køretøjer til parkering, pause til Start alle
|
||||
self.stopParkButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_stopPark"), callback = function() self:onClickStopAndPark() end}
|
||||
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.stopParkButtonInfo, self.newButtonInfo}
|
||||
-- Marker: Start nu / Stop for den valgte mark
|
||||
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
|
||||
self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo}
|
||||
-- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
|
||||
self:initFieldButtons(i18n)
|
||||
self:initGuideButtons(i18n)
|
||||
-- Flows: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet] (gui/SmartPickupFlowGuide.lua)
|
||||
self:initFlowButtons(i18n)
|
||||
self:initEditPanelButtons(i18n)
|
||||
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
|
||||
self.menuButtonInfo = self.overviewButtons
|
||||
self:setMenuButtonInfo(self.menuButtonInfo)
|
||||
@@ -71,7 +86,7 @@ end
|
||||
|
||||
-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker.
|
||||
function SmartPickupFrame:initialize()
|
||||
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "fieldPrefab"}) do
|
||||
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "flowListPrefab", "fieldPrefab", "fieldListPrefab", "monthPrefab", "kpiPrefab", "alertPrefab", "itemPrefab", "subPrefab"}) do
|
||||
local prefab = self[name]
|
||||
if prefab ~= nil then
|
||||
prefab:unlinkElement()
|
||||
@@ -197,6 +212,9 @@ end
|
||||
|
||||
function SmartPickupFrame:onFrameClose()
|
||||
self.frameOpen = false
|
||||
-- et åbent redigeringsvindue og en åben guide kasseres (intet er gemt før [Gem])
|
||||
self:hideEditPanel()
|
||||
self:closeGuide()
|
||||
SmartPickupFrame:superClass().onFrameClose(self)
|
||||
end
|
||||
|
||||
@@ -243,6 +261,11 @@ function SmartPickupFrame:requestRebuild()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:switchTab(tabIndex)
|
||||
if self.editPanelOpen then
|
||||
self:closeGuide()
|
||||
self:hideEditPanel()
|
||||
end
|
||||
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
|
||||
self.currentTab = tabIndex
|
||||
for index, tab in ipairs(self.subCategoryTabs) do
|
||||
tab:setSelected(index == tabIndex)
|
||||
@@ -263,7 +286,8 @@ end
|
||||
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
||||
-- Kør til lager for udkørsel. Fanen Lagre viser ingen.
|
||||
function SmartPickupFrame:setStaticRowsVisible(tab, run)
|
||||
local isFlows = tab == SmartPickupFrame.TAB_FLOWS
|
||||
-- Flows-fanen bruger tavle + resumé + guide (v1.32); de faste XML-rækker vises ikke længere
|
||||
local isFlows = false
|
||||
local hasRun = isFlows and run ~= nil
|
||||
local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound"
|
||||
local visibility = {
|
||||
@@ -312,6 +336,7 @@ function SmartPickupFrame:clearRows()
|
||||
self.productRows, self.outboundStatusRows = {}, {}
|
||||
self.storageRows = {}
|
||||
self.storageDetailRows = {}
|
||||
self.itemView = nil
|
||||
self.boardRows = {}
|
||||
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
|
||||
end
|
||||
@@ -328,9 +353,11 @@ function SmartPickupFrame:addToggleRow(kind, title, texts, isOn)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addRow(prefab, data)
|
||||
local row = prefab:clone(self.boxLayout)
|
||||
local layout = self.rowTarget or self.boxLayout
|
||||
local row = prefab:clone(layout)
|
||||
row.spuLayout = layout
|
||||
FocusManager:loadElementFromCustomValues(row)
|
||||
table.insert(self.clonedRows, row)
|
||||
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
|
||||
local option = row:getDescendantByName("option")
|
||||
if option ~= nil then
|
||||
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
||||
@@ -339,23 +366,56 @@ function SmartPickupFrame:addRow(prefab, data)
|
||||
return row
|
||||
end
|
||||
|
||||
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
|
||||
function SmartPickupFrame:withRowTarget(layout, fn)
|
||||
local previous = self.rowTarget
|
||||
self.rowTarget = layout
|
||||
local ok, err = pcall(fn, self)
|
||||
self.rowTarget = previous
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:clearPanelRows()
|
||||
local focused = SmartPickupFrame.getFocusedElement()
|
||||
for _, row in ipairs(self.panelRows or {}) do
|
||||
if focused ~= nil and SmartPickupFrame.isDescendantOf(focused, row) then
|
||||
self.focusLost = true
|
||||
end
|
||||
FocusManager:removeElement(row)
|
||||
row:delete()
|
||||
end
|
||||
self.panelRows = {}
|
||||
end
|
||||
|
||||
-- Tekstrække uden valgmuligheder: sektionstitler, Wait-pulje-linjer og statuslinjen.
|
||||
-- Tegn pr. tekstrække før teksten ombrydes (rækken afkorter med "..." ved ca. 70 tegn).
|
||||
SmartPickupFrame.TEXT_ROW_CHARS = 90
|
||||
|
||||
-- Lange tekster fordeles på flere rækker i stedet for at blive afkortet. Returnerer første rækkes titel.
|
||||
function SmartPickupFrame:addTextRow(text)
|
||||
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||
title:setText(text)
|
||||
return title
|
||||
local first = nil
|
||||
for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||
title:setText(line)
|
||||
first = first or title
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
-- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning,
|
||||
-- fordi rækkerne først findes på det tidspunkt.
|
||||
function SmartPickupFrame:colorRows()
|
||||
self:colorLayout(self.boxLayout)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:colorLayout(layout)
|
||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
||||
if colors == nil or self.boxLayout == nil then
|
||||
if colors == nil or layout == nil then
|
||||
return
|
||||
end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local isEvenRow = false
|
||||
for _, element in ipairs(self.boxLayout.elements) do
|
||||
for _, element in ipairs(layout.elements) do
|
||||
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
||||
end
|
||||
@@ -390,10 +450,33 @@ function SmartPickupFrame:rebuild()
|
||||
self.statusRow = self:addTextRow("")
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||
self:refreshLive()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
if adEnv ~= nil then
|
||||
self:fillFieldSiloRows(adEnv)
|
||||
end
|
||||
self.monthRebuildTicks = SmartPickupFrame.FIELDSILO_REBUILD_TICKS
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
self:refreshButtons()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
||||
if adEnv ~= nil then
|
||||
self:fillMonthRows(adEnv)
|
||||
end
|
||||
self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
self:refreshButtons()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
if adEnv ~= nil then
|
||||
self:fillOverviewRows(adEnv)
|
||||
@@ -407,46 +490,17 @@ function SmartPickupFrame:rebuild()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
self:fillRunOption(adEnv)
|
||||
local hasRun = run ~= nil and adEnv ~= nil
|
||||
for _, element in ipairs({self.waitPoolOption, self.loopsOption, self.returnBelowOption, self.sellAtOption, self.storeAboveOption}) do
|
||||
if element ~= nil then
|
||||
element:setDisabled(not hasRun)
|
||||
end
|
||||
end
|
||||
if hasRun then
|
||||
self:fillWaitPoolOption(adEnv, run)
|
||||
self:rebuildFlowDetail(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_vehicles"))
|
||||
self:fillVehicleRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_status"))
|
||||
if adEnv ~= nil then
|
||||
self:fillFlowTabRows(adEnv)
|
||||
end
|
||||
self.statusRow = self:addTextRow("")
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||
self:refreshLive()
|
||||
self:restoreFocusIfLost()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillRunOption(adEnv)
|
||||
local texts, index = {}, 1
|
||||
self.runIds = {}
|
||||
for position, run in ipairs(self:getRunsForTab()) do
|
||||
local status = adEnv ~= nil and ADRunsController.getRunStatus(adEnv, run) or {running = 0, total = #run.vehicleIds}
|
||||
table.insert(texts, string.format("%s (%d/%d)", run.name, status.running, status.total))
|
||||
table.insert(self.runIds, run.id)
|
||||
if run.id == self.selectedRunId then
|
||||
index = position
|
||||
end
|
||||
end
|
||||
if #texts == 0 then
|
||||
texts = {self.i18n:getText("spu_noRuns")}
|
||||
end
|
||||
self.runOption:setTexts(texts)
|
||||
self.runOption:setState(index, false)
|
||||
self.runOption:setDisabled(#self.runIds == 0)
|
||||
end
|
||||
|
||||
-- Overblik: "Cow 1 food: TMR 18.0k/27.8k · Silage 3.0k/…" (mest trængende først)
|
||||
function SmartPickupFrame:describeTargetLevels(target)
|
||||
if #target.levels == 0 then
|
||||
@@ -455,114 +509,6 @@ function SmartPickupFrame:describeTargetLevels(target)
|
||||
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillWaitPoolOption(adEnv, run)
|
||||
local texts, index = {self.i18n:getText("spu_waitPool_all")}, 1
|
||||
self.poolGroups = {""}
|
||||
for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do
|
||||
table.insert(texts, string.format("%s (%d)", pool.group, pool.count))
|
||||
table.insert(self.poolGroups, pool.group)
|
||||
if pool.group == run.waitPoolGroup then
|
||||
index = #texts
|
||||
end
|
||||
end
|
||||
self.waitPoolOption:setTexts(texts)
|
||||
self.waitPoolOption:setState(index, false)
|
||||
end
|
||||
|
||||
-- Traktorer: som standard flowets egne og de ledige der kan bruges; "Vis alle" tilføjer traktorer i andre
|
||||
-- flows og låste (vognen passer ikke) — de er tit mange og gør listen uoverskuelig.
|
||||
function SmartPickupFrame:fillVehicleRows(adEnv, run)
|
||||
local inRun = {}
|
||||
for _, id in ipairs(run.vehicleIds) do
|
||||
inRun[id] = true
|
||||
end
|
||||
local entries = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local otherRun = ADRunsController.getRunForVehicle(vehicle)
|
||||
local state = "free"
|
||||
if inRun[id] then
|
||||
state = "in"
|
||||
elseif otherRun ~= nil and otherRun.id ~= run.id then
|
||||
state = "other"
|
||||
elseif SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] then
|
||||
state = "blocked"
|
||||
end
|
||||
table.insert(entries, {id = id, label = ADRunsController.getVehicleLabel(adEnv, vehicle), state = state, vehicle = vehicle, otherRun = otherRun})
|
||||
end
|
||||
local rows = ADFlowView.vehicleRows(entries, self.showAllVehicles == true)
|
||||
local hidden = ADFlowView.hiddenCount(entries, rows)
|
||||
if hidden > 0 or self.showAllVehicles then
|
||||
self.showAllVehiclesOption = self:addToggleRow("showAllVehicles", string.format(self.i18n:getText("spu_showAllVehicles"), hidden), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllVehicles == true)
|
||||
end
|
||||
for _, entry in ipairs(rows) do
|
||||
local vehicle = entry.vehicle
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "vehicle", vehicle = vehicle, row = nil})
|
||||
local option, title = row:getDescendantByName("option"), row:getDescendantByName("title")
|
||||
self.rowData[option].row = row
|
||||
option:setState(entry.state == "in" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
if entry.state == "other" then
|
||||
-- én traktor = ét flow; rækken viser hvor den er
|
||||
option:setDisabled(true)
|
||||
self.rowData[option].locked = true
|
||||
title:setText(string.format("%s — %s", entry.label, string.format(self.i18n:getText("spu_inRun"), tostring(entry.otherRun.name))))
|
||||
else
|
||||
title:setText(self:describeVehicle(adEnv, run, vehicle, entry.state == "in"))
|
||||
if entry.state == "blocked" then
|
||||
option:setDisabled(true)
|
||||
self.rowData[option].locked = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — ✔" / "… — ✖ ingen vogn"; reservationsnote fra Start bevares.
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — ✔" / "… — ✖ ingen vogn".
|
||||
-- Vises for ALLE traktorer, så man kan se om vognen passer, før man siger Ja.
|
||||
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local parts = {label}
|
||||
if validation.isAutoload and validation.capacity == nil then
|
||||
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
|
||||
elseif validation.capacity ~= nil then
|
||||
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
|
||||
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
|
||||
if carriable ~= nil then
|
||||
table.insert(parts, carriable)
|
||||
end
|
||||
end
|
||||
if validation.ok then
|
||||
table.insert(parts, "OK")
|
||||
else
|
||||
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
|
||||
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
|
||||
reason = string.format(reason, tostring(validation.missingBuilding))
|
||||
end
|
||||
table.insert(parts, "X " .. reason)
|
||||
end
|
||||
return table.concat(parts, " — ")
|
||||
end
|
||||
|
||||
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
|
||||
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
|
||||
local canCarry = {}
|
||||
for _, fillType in ipairs(carriable or {}) do
|
||||
canCarry[fillType] = true
|
||||
end
|
||||
local titles = {}
|
||||
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
|
||||
if canCarry[fillType] then
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||||
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
|
||||
end
|
||||
end
|
||||
if #titles == 0 then
|
||||
return nil
|
||||
end
|
||||
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
|
||||
end
|
||||
|
||||
-- Felterne følger modellen: når et klik ikke er nået ind i run'et, springer feltet tilbage,
|
||||
-- og loggen ("run '…': waitPoolGroup=…") viser om klikket blev registreret.
|
||||
function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
|
||||
@@ -657,10 +603,19 @@ end
|
||||
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
|
||||
function SmartPickupFrame:refreshButtons()
|
||||
local wanted = self.flowButtons
|
||||
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
if self.editPanelOpen then
|
||||
wanted = self.guide ~= nil and self:panelGuideButtons() or self:getPanelButtons()
|
||||
elseif self:hasGuideOn(self.currentTab) then
|
||||
wanted = self:getGuideButtons()
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
wanted = self.overviewButtons
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||
wanted = self.fieldButtons
|
||||
wanted = self:getFieldButtons()
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
wanted = self.monthButtons
|
||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||
wanted = self.storageButtons
|
||||
self.renameBuildingButtonInfo.disabled = self.storageDetailPlaceable == nil
|
||||
end
|
||||
if self.menuButtonInfo ~= wanted then
|
||||
self.menuButtonInfo = wanted
|
||||
@@ -681,10 +636,17 @@ function SmartPickupFrame:refreshLive()
|
||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
||||
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
|
||||
if isOverview then
|
||||
self:refreshOverview(adEnv)
|
||||
end
|
||||
if self.editPanelOpen then self:refreshEditPanel(adEnv) end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||
self:refreshFieldRows(adEnv)
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
@@ -692,6 +654,13 @@ function SmartPickupFrame:refreshLive()
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FLOWS then
|
||||
self:refreshFlowTab(adEnv)
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
self:refreshStorageRows(adEnv)
|
||||
local isRunning = false
|
||||
if isOverview then
|
||||
@@ -701,8 +670,19 @@ function SmartPickupFrame:refreshLive()
|
||||
running, total = running + status.running, total + status.total
|
||||
if status.running > 0 then activeRuns = activeRuns + 1 end
|
||||
end
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs))
|
||||
local statusText = string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs)
|
||||
local queued = #(ADRunsController.startQueue or {})
|
||||
if queued > 0 then
|
||||
local totalQueued = ADRunsController.startQueueTotal or queued
|
||||
statusText = string.format(self.i18n:getText("spu_status_starting"), totalQueued - queued, totalQueued)
|
||||
elseif ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then
|
||||
statusText = self.i18n:getText("spu_status_paused")
|
||||
end
|
||||
self:setStatusText(self.statusOverride or statusText)
|
||||
self.stopAllButtonInfo.disabled = running == 0
|
||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||
-- lagerfanerne: ingen flow-status (in-game 29/9: 'Stoppet (1 traktorer)' fra det sidst valgte flow)
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
elseif run ~= nil then
|
||||
self:syncOptionsFromModel(adEnv, run)
|
||||
if run.kind == "outbound" then
|
||||
@@ -711,7 +691,7 @@ function SmartPickupFrame:refreshLive()
|
||||
self:refreshNeedsRows(adEnv, run)
|
||||
end
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
isRunning = status.isRunning == true
|
||||
isRunning = status.canStop == true
|
||||
local text = status.running > 0 and string.format(self.i18n:getText("spu_status_running"), status.running, status.total)
|
||||
or string.format(self.i18n:getText("spu_status_stopped"), status.total)
|
||||
self:setStatusText(self.statusOverride or text)
|
||||
@@ -747,27 +727,14 @@ function SmartPickupFrame:onClickRun(state)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickIngredient(state, element)
|
||||
-- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
|
||||
-- guiderne og markflåden deler prefab (og dermed onClick) med vare-rækkerne
|
||||
local data = self.rowData[element]
|
||||
if data ~= nil and data.kind == "target" then
|
||||
return self:onClickTarget(state, element)
|
||||
elseif data ~= nil and data.kind == "source" then
|
||||
return self:onClickSource(state, element)
|
||||
elseif data ~= nil and data.kind == "ingredientMode" then
|
||||
return self:onClickIngredientMode(state)
|
||||
elseif data ~= nil and data.kind == "collectRound" then
|
||||
return self:onClickCollectRound(state)
|
||||
elseif data ~= nil and data.kind == "direction" then
|
||||
return self:onClickDirection(state)
|
||||
elseif data ~= nil and (data.kind == "showAllVehicles" or data.kind == "showAllBuildings") then
|
||||
local isOn = state == SmartPickupFrame.STATE_ON
|
||||
if data.kind == "showAllVehicles" then self.showAllVehicles = isOn else self.showAllBuildings = isOn end
|
||||
self:requestRebuild()
|
||||
return
|
||||
if data ~= nil and data.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, data)
|
||||
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
|
||||
return self:onClickFieldSiloRow(state, data)
|
||||
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
||||
return self:onClickFieldRow(state, data)
|
||||
elseif data ~= nil and (data.kind == "sourceBuilding" or data.kind == "targetBuilding" or data.kind == "storeBuilding") then
|
||||
return self:onClickBuilding(state, element)
|
||||
end
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
@@ -793,12 +760,18 @@ end
|
||||
|
||||
function SmartPickupFrame:onClickVehicle(state, element)
|
||||
local clicked = self.rowData[element]
|
||||
-- markfanens maskinrækker (markflåden og markens faste maskiner) bruger samme række-type som flowets
|
||||
-- traktorliste; de hører til markfanen (fieldRig blev før behandlet som flow-traktor og aldrig gemt)
|
||||
if clicked ~= nil and clicked.kind == "fleetVehicle" then
|
||||
return self:onClickFieldRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, clicked)
|
||||
end
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil or data.locked then return end
|
||||
-- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres
|
||||
if run == nil or data == nil or data.locked or data.vehicle == nil then return end
|
||||
if state == SmartPickupFrame.STATE_ON then
|
||||
local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle)
|
||||
if previousRunId ~= nil then
|
||||
@@ -819,14 +792,7 @@ function SmartPickupFrame:onClickStartStop()
|
||||
self:safe(function(frame)
|
||||
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||
if adEnv == nil or run == nil then return end
|
||||
if ADRunsController.getRunStatus(adEnv, run).isRunning then
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
frame.statusOverride = nil
|
||||
else
|
||||
local started, skipped, reasons = frame:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_started"), started, skipped), reasons)
|
||||
end
|
||||
ADRunsController.save()
|
||||
frame:toggleRun(adEnv, run.id)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -866,11 +832,28 @@ function SmartPickupFrame:onClickStartAll()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local summary = ADRunsController.startAllRuns(adEnv)
|
||||
local _, skipped, reasons = frame:summarizeStartResults(summary.results)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d flows startet (%d traktorer), %d kørte allerede, %d sprunget over",
|
||||
summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped)
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_startedAll"), summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped), reasons)
|
||||
-- pausen (Stop alt og parkér) ophæves; traktorerne startes én ad gangen med mellemrum
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then ADFieldJobs.setPaused(false) end
|
||||
local summary = ADRunsController.queueStartAll(adEnv)
|
||||
local _, skipped, reasons = frame:summarizeStartResults(summary.skipped)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d traktorer i kø (én hvert %d. sekund), %d flows kørte allerede, %d sprunget over",
|
||||
summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped)
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_queuedAll"), summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped), reasons)
|
||||
ADRunsController.save()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickStopAndPark()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local summary = ADRunsController.stopAllAndPark(adEnv)
|
||||
local text = string.format(frame.i18n:getText("spu_status_stopParked"), summary.stopped, summary.parked)
|
||||
local notes = {}
|
||||
if #summary.noPark > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_noPark"), table.concat(summary.noPark, ", "))) end
|
||||
if #summary.manual > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_fetchManual"), table.concat(summary.manual, ", "))) end
|
||||
frame:showOverride(text, notes)
|
||||
ADRunsController.save()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
@@ -897,25 +880,28 @@ function SmartPickupFrame:openFlow(runId)
|
||||
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
end
|
||||
|
||||
-- Nyt run: navnet vælges FØR run'et oprettes (annulleres dialogen, oprettes intet).
|
||||
-- Forslaget tæller efter antal runs, ikke efter løbenummeret, så "Run 2" kan genbruges.
|
||||
-- Nyt flow: guiden "Opsæt flow" (gui/SmartPickupFlowGuide.lua) — intet oprettes før [Gem].
|
||||
function SmartPickupFrame:onClickNew()
|
||||
self:onClickFlowNew()
|
||||
end
|
||||
|
||||
-- Omdøb den valgte bygning i en bygningsfane (gemmes i ADSmartPickup_names.xml; tomt = butiksnavnet igen)
|
||||
function SmartPickupFrame:onClickRenameBuilding()
|
||||
self:safe(function(frame)
|
||||
local suggestion = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
||||
TextInputDialog.show(SmartPickupFrame.onNewRunName, frame, suggestion, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
||||
local placeable = frame.storageDetailPlaceable
|
||||
if placeable == nil then return end
|
||||
frame.renamingBuildingId = ADBuildings.getId(placeable)
|
||||
local current = ADNames.forPlaceable(placeable) or (placeable.getName ~= nil and placeable:getName()) or ""
|
||||
TextInputDialog.show(SmartPickupFrame.onRenameBuildingResult, frame, tostring(current), frame.i18n:getText("spu_renameBuildingTitle"), nil, 40, g_i18n:getText("button_ok"))
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onNewRunName(value, clickOk)
|
||||
function SmartPickupFrame:onRenameBuildingResult(value, clickOk)
|
||||
self:safe(function(frame)
|
||||
if not clickOk or value == nil or not value:match("%S") then
|
||||
return
|
||||
end
|
||||
local run = ADRunsController.createRun(value, "supply")
|
||||
frame.selectedRunId = run.id
|
||||
frame.statusOverride = nil
|
||||
frame.currentTab = SmartPickupFrame.TAB_FLOWS
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
if not clickOk or frame.renamingBuildingId == nil then return end
|
||||
ADNames.set(frame.renamingBuildingId, value)
|
||||
frame.renamingBuildingId = nil
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_storeAbove"/>
|
||||
</Bitmap>
|
||||
</ScrollingLayout>
|
||||
<!-- redigeringsvindue (v1.58): skjult; placering/størrelse sættes i gui/SmartPickupEditPanel.lua -->
|
||||
<GuiElement profile="baseReference" id="editOverlay" visible="false">
|
||||
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
|
||||
<Bitmap profile="spuOverlayPanel" id="editPanel"/>
|
||||
<Text profile="spuPanelTitle" id="editTitle"/>
|
||||
<Text profile="spuCell" id="editStatus" size="700px 30px" textMaxWidth="690px"/>
|
||||
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout"/>
|
||||
</GuiElement>
|
||||
|
||||
<!-- prefabs: klones pr. vare / traktor / tekstlinje og fjernes fra layoutet ved initialize -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="ingredientPrefab">
|
||||
@@ -81,6 +89,15 @@
|
||||
<Text profile="spuCell" name="c5" position="620px 0px" size="50px 40px" textMaxWidth="42px"/>
|
||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
</Bitmap>
|
||||
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
|
||||
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
|
||||
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
<!-- Marker: én række pr. mark (mark · afgrøde · plan · auto/man. · status · maskiner). Rækken er ~815 px bred. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
@@ -90,6 +107,26 @@
|
||||
<Text profile="spuCell" name="c5" position="440px 0px" size="230px 40px" textMaxWidth="222px"/>
|
||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
</Bitmap>
|
||||
<!-- Måned: mark · arbejde · afgrøde · færdig · næste · vindue (vinduet bredt nok til "OBS: sidste såmåned"). ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="monthPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
<Text profile="spuCell" name="c2" position="120px 0px" size="130px 40px" textMaxWidth="122px"/>
|
||||
<Text profile="spuCell" name="c3" position="250px 0px" size="110px 40px" textMaxWidth="102px"/>
|
||||
<Text profile="spuCell" name="c4" position="360px 0px" size="80px 40px" textMaxWidth="72px"/>
|
||||
<Text profile="spuCell" name="c5" position="440px 0px" size="150px 40px" textMaxWidth="142px"/>
|
||||
<Text profile="spuCell" name="c6" position="590px 0px" size="240px 40px" textMaxWidth="232px"/>
|
||||
</Bitmap>
|
||||
<!-- Fields-listen (v1.59): ● · mark · afgrøde · plan · kørsel · status · maskiner · [Start]/[Stop]. ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldListPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
|
||||
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
<!-- Lagre: statistik-tabel (vare · i bygningen · værdi nu · ved toppris · gården · rækker til · forbrug/prod.) -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="statPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
@@ -106,6 +143,41 @@
|
||||
<Text profile="spuCell" name="c2" position="290px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||
<Text profile="spuCell" name="c3" position="550px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||
</Bitmap>
|
||||
<!-- Lagre, vareoversigt (v1.52). Rækken er ~815 px bred. -->
|
||||
<!-- nøgletal: 4 felter (tal + label); højden sættes i Lua -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="kpiPrefab">
|
||||
<Text profile="spuKpiValue" name="v1" position="20px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l1" position="20px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v2" position="220px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l2" position="220px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v3" position="420px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l3" position="420px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v4" position="620px 0px" size="190px 40px" textMaxWidth="185px"/>
|
||||
<Text profile="spuKpiLabel" name="l4" position="620px 0px" size="190px 40px" textMaxWidth="185px"/>
|
||||
</Bitmap>
|
||||
<!-- kræver handling: farvet markør + tekst -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="alertPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCell" name="c1" position="40px 0px" size="770px 40px" textMaxWidth="765px"/>
|
||||
</Bitmap>
|
||||
<!-- vare: +/– navn · bjælke · mængde · værdi · netto/døgn og rækkevidde (klikbar) -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="itemPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="170px 40px" textMaxWidth="165px"/>
|
||||
<Bitmap profile="spuBarBg" name="barBg" position="190px 0px" size="140px 16px"/>
|
||||
<Bitmap profile="spuBar" name="bar" position="190px 0px" size="140px 16px"/>
|
||||
<Text profile="spuCell" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
|
||||
<Text profile="spuCell" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
|
||||
<Text profile="spuCell" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
|
||||
</Bitmap>
|
||||
<!-- bygning under en udfoldet vare: indrykket navn · tynd bjælke · mængde · værdi · procent -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="subPrefab">
|
||||
<Text profile="spuCellDim" name="c1" position="45px 0px" size="145px 40px" textMaxWidth="140px"/>
|
||||
<Bitmap profile="spuBarThinBg" name="barBg" position="190px 0px" size="140px 10px"/>
|
||||
<Bitmap profile="spuBarThin" name="bar" position="190px 0px" size="140px 10px"/>
|
||||
<Text profile="spuCellDim" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
|
||||
<Text profile="spuCellDim" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
|
||||
<Text profile="spuCellDim" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
|
||||
</Bitmap>
|
||||
<!-- udkørsel: én række pr. vare med politik Fra/Lager/Salg/Salg, ellers lager -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab">
|
||||
<MultiTextOption profile="fs25_settingsMultiTextOption" onClick="onClickProduct" name="option">
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
-- Smart Pickup — fælles guide-skærm ("Opsæt mark" / "Opsæt flow")
|
||||
-- Ét trin ad gangen: overskrift, "Trin 3 af 7 — …", kort forklaring, valgene som rækker og bundknapperne
|
||||
-- Næste/Gem, Tilbage, Annullér (+ Gem uden at starte på oversigts-trinnet). Al logik ligger i ADGuide og
|
||||
-- trin-definitionerne (ADFieldGuide/ADFlowGuide); her tegnes kun rækkerne og klik routes.
|
||||
-- Hver guide registrerer sig i SmartPickupFrame.GUIDE_HOOKS[kind] = {
|
||||
-- model, tab, heading(frame, guide), context(frame, adEnv, guide) -> context|nil (nil = luk guiden),
|
||||
-- choose(frame, id, value) -> handled, save(frame, guide, start), startKey(guide), showSaveOnly(guide),
|
||||
-- canAdvance(guide), afterRows(frame, adEnv, guide) }
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
|
||||
function SmartPickupFrame:initGuideButtons(i18n)
|
||||
self.guideNextButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_guide_next"), callback = function() self:onClickGuideNext(true) end}
|
||||
self.guideSaveOnlyButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_saveOnly"), callback = function() self:onClickGuideNext(false) end}
|
||||
self.guidePrevButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_back"), callback = function() self:onClickGuideBack() end}
|
||||
self.guideCancelButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_guide_cancel"), callback = function() self:onClickGuideCancel() end}
|
||||
self.guideButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
|
||||
self.guideSummaryButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guideSaveOnlyButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:guideHooks()
|
||||
return self.guideKind ~= nil and SmartPickupFrame.GUIDE_HOOKS[self.guideKind] or nil
|
||||
end
|
||||
|
||||
-- Er der en åben guide på fanen?
|
||||
function SmartPickupFrame:hasGuideOn(tab)
|
||||
local hooks = self:guideHooks()
|
||||
return self.guide ~= nil and hooks ~= nil and hooks.tab == tab
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openGuide(kind, guide)
|
||||
self.guideKind, self.guide = kind, guide
|
||||
self.statusOverride = nil
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kladden kasseres (Annullér, fanebyt, lukket menu).
|
||||
function SmartPickupFrame:closeGuide()
|
||||
self.guideKind, self.guide = nil, nil
|
||||
end
|
||||
|
||||
function SmartPickupFrame:guideText(label)
|
||||
return ADGuide.labelText(label, function(key) return self.i18n:getText(key) end)
|
||||
end
|
||||
|
||||
-- Klikbar tekstrække (ombrydes; alle linjer er klikbare). Handlingen køres fra update() (pendingClick).
|
||||
function SmartPickupFrame:addLinkRow(text, action)
|
||||
local first = nil
|
||||
for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||
local row = self:addRow(self.textPrefab, {kind = "text"})
|
||||
local title = row:getDescendantByName("title")
|
||||
title:setText(line)
|
||||
self:makeRowClickable(row, action)
|
||||
first = first or title
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
local function optionIndex(options, value)
|
||||
for index, option in ipairs(options) do
|
||||
if option.value == value then return index end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addGuideRow(row)
|
||||
local data = {kind = "guideRow", row = row}
|
||||
if row.type == "choice" then
|
||||
local element = self:addRow(self.productPrefab, data)
|
||||
local option = element:getDescendantByName("option")
|
||||
local texts = {}
|
||||
for _, entry in ipairs(row.options) do table.insert(texts, self:guideText(entry.label)) end
|
||||
option:setTexts(texts)
|
||||
option:setState(optionIndex(row.options, row.value), false)
|
||||
element:getDescendantByName("title"):setText(self:guideText(row.title))
|
||||
elseif row.type == "toggle" or row.type == "check" then
|
||||
local element = self:addRow(self.ingredientPrefab, data)
|
||||
local option = element:getDescendantByName("option")
|
||||
if row.type == "toggle" then option:setTexts({self:guideText(row.off), self:guideText(row.on)}) end
|
||||
option:setState(row.value and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
element:getDescendantByName("title"):setText((row.type == "check" and " " or "") .. self:guideText(row.title))
|
||||
elseif row.type == "link" then
|
||||
local text = row.label ~= nil and self:guideText(row.label)
|
||||
or (ADGuide.lineText(row.line, function(key) return self.i18n:getText(key) end) .. self.i18n:getText("spu_guide_editSuffix"))
|
||||
self:addLinkRow(" " .. text, function(frame) frame:onGuideLink(row) end)
|
||||
else
|
||||
self:addTextRow(self:guideText(row.label))
|
||||
end
|
||||
end
|
||||
|
||||
-- Tegner den åbne guide. false = guiden er lukket (fx marken findes ikke længere) — fanen viser oversigten.
|
||||
function SmartPickupFrame:fillGuideRows(adEnv)
|
||||
local hooks = self:guideHooks()
|
||||
if hooks == nil or self.guide == nil then return false end
|
||||
local context = hooks.context(self, adEnv, self.guide)
|
||||
if context == nil then
|
||||
self:closeGuide()
|
||||
self.statusOverride = self.i18n:getText("spu_guide_closed")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
return false
|
||||
end
|
||||
self.guide = ADGuide.withContext(self.guide, context)
|
||||
local guide, model = self.guide, hooks.model
|
||||
self:addTextRow(hooks.heading(self, guide))
|
||||
local stepTitle = self.i18n:getText(model.stepKey(guide.step, guide))
|
||||
local index, total = ADGuide.position(guide)
|
||||
local header
|
||||
if guide.single then
|
||||
header = string.format(self.i18n:getText("spu_guide_editStep"), stepTitle)
|
||||
elseif index ~= nil and total > 1 then
|
||||
header = string.format(self.i18n:getText("spu_guide_stepOf"), index, total, stepTitle)
|
||||
else
|
||||
header = string.format(self.i18n:getText("spu_guide_stepOne"), index or 1, stepTitle)
|
||||
end
|
||||
self:addTextRow(header)
|
||||
self:addTextRow(" " .. self.i18n:getText(model.hintKey(guide.step, guide)))
|
||||
for _, row in ipairs(model.rows(guide)) do
|
||||
self:addGuideRow(row)
|
||||
end
|
||||
if hooks.afterRows ~= nil then hooks.afterRows(self, adEnv, guide) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- ---- klik ----
|
||||
|
||||
function SmartPickupFrame:applyGuideChoice(id, value)
|
||||
local hooks = self:guideHooks()
|
||||
if hooks == nil or self.guide == nil then return end
|
||||
if hooks.choose ~= nil and hooks.choose(self, id, value) then return end
|
||||
self.guide = hooks.model.choose(self.guide, id, value)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local row = data ~= nil and data.row or nil
|
||||
if row == nil or frame.guide == nil then return end
|
||||
local value
|
||||
if row.type == "choice" then
|
||||
local option = row.options[state]
|
||||
if option == nil then return end
|
||||
value = option.value
|
||||
-- valg pr. vare (fx politik): modellen skal vide hvilken
|
||||
if row.item ~= nil then value = {item = row.item, value = option.value} end
|
||||
elseif row.type == "toggle" then
|
||||
value = state == SmartPickupFrame.STATE_ON
|
||||
elseif row.type == "check" then
|
||||
value = {item = row.item, on = state == SmartPickupFrame.STATE_ON}
|
||||
else
|
||||
return
|
||||
end
|
||||
frame:applyGuideChoice(row.id, value)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Klikbare linjer: [Ret] på oversigts-trinnet åbner ét trin; andre links er valg (fx mark i trin 1).
|
||||
function SmartPickupFrame:onGuideLink(row)
|
||||
if self.guide == nil then return end
|
||||
if row.id == "jump" then
|
||||
self.guide = ADGuide.jump(self.guide, row.value, "summary")
|
||||
else
|
||||
self:applyGuideChoice(row.id, row.value)
|
||||
end
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
local function finish(frame, hooks, guide, start)
|
||||
hooks.save(frame, guide, start)
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
end
|
||||
|
||||
-- Næste / Gem / Kør automatisk (start = true) og Gem uden at starte (start = false).
|
||||
function SmartPickupFrame:onClickGuideNext(start)
|
||||
self:safe(function(frame)
|
||||
local hooks, guide = frame:guideHooks(), frame.guide
|
||||
if hooks == nil or guide == nil then return end
|
||||
if hooks.canAdvance ~= nil and not hooks.canAdvance(guide) then
|
||||
frame:showOverride(frame.i18n:getText(hooks.cannotAdvanceKey or "spu_guide_cannotAdvance"))
|
||||
return
|
||||
end
|
||||
if guide.single and guide.returnTo == "close" then
|
||||
if start == false then return end
|
||||
return finish(frame, hooks, guide, nil)
|
||||
end
|
||||
if guide.step == "summary" and not guide.single then
|
||||
return finish(frame, hooks, guide, start)
|
||||
end
|
||||
if start == false then return end
|
||||
local nextGuide, event = ADGuide.next(guide)
|
||||
if event == "done" then return finish(frame, hooks, guide, true) end
|
||||
frame.guide = nextGuide
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideBack()
|
||||
self:safe(function(frame)
|
||||
if frame.guide == nil then return end
|
||||
local previous, event = ADGuide.back(frame.guide)
|
||||
if event == "cancel" then frame:closeGuide() else frame.guide = previous end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideCancel()
|
||||
self:safe(function(frame)
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Knapperne følger trinnet: Næste / OK (tilbage til oversigten) / Gem ([Ret] fra fanen) / Kør automatisk.
|
||||
function SmartPickupFrame:getGuideButtons()
|
||||
local hooks, guide = self:guideHooks(), self.guide
|
||||
local i18n = self.i18n
|
||||
local isSummary = guide.step == "summary" and not guide.single
|
||||
local key = "spu_guide_next"
|
||||
if guide.single then
|
||||
key = guide.returnTo == "close" and "spu_guide_save" or "spu_guide_ok"
|
||||
elseif isSummary then
|
||||
key = hooks.startKey ~= nil and hooks.startKey(guide) or "spu_guide_save"
|
||||
end
|
||||
self.guideNextButtonInfo.text = i18n:getText(key)
|
||||
self.guideNextButtonInfo.disabled = hooks.canAdvance ~= nil and not hooks.canAdvance(guide)
|
||||
if isSummary and (hooks.showSaveOnly == nil or hooks.showSaveOnly(guide)) then
|
||||
return self.guideSummaryButtons
|
||||
end
|
||||
return self.guideButtons
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Smart Pickup — fanen Måned
|
||||
-- Hvad skal nås før månedsskiftet? Høst og såning har korte vinduer; fanen viser markerne med arbejde i
|
||||
-- gang, fremdrift, næste trin og om vinduet lukker. Kun visning — reglerne ligger i ADMonthView.
|
||||
|
||||
-- Periodens navn: spillets egen tekst (fx "Sen sommer") hvis den findes, ellers "Periode 6".
|
||||
-- Periode 1 = tidligt forår = marts (nordlig halvkugle) -> "(aug)".
|
||||
function SmartPickupFrame:monthPeriodText(period)
|
||||
local month = (period + 1) % 12 + 1
|
||||
local names = {}
|
||||
for name in string.gmatch(self.i18n:getText("spu_month_months"), "[^,]+") do table.insert(names, name) end
|
||||
local suffix = names[month] ~= nil and (" (" .. names[month] .. ")") or ""
|
||||
local ok, text = pcall(function() return g_i18n:formatPeriod(period, false) end)
|
||||
if ok and type(text) == "string" and text ~= "" then return text .. suffix end
|
||||
return string.format(self.i18n:getText("spu_month_period"), period) .. suffix
|
||||
end
|
||||
|
||||
-- {period, day, days, hour, minute}; nil hvis miljøet ikke kan læses.
|
||||
function SmartPickupFrame.monthClock()
|
||||
local ok, clock = pcall(function()
|
||||
local environment = g_currentMission.environment
|
||||
local dayTime = environment.dayTime or 0
|
||||
return {period = environment.currentPeriod, day = environment.currentDayInPeriod, days = environment.daysPerPeriod,
|
||||
hour = math.floor(dayTime / 3600000), minute = math.floor(dayTime / 60000) % 60}
|
||||
end)
|
||||
if ok and type(clock) == "table" and type(clock.period) == "number" then return clock end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Courseplays fremdrift for markens sæt (gennemsnit), nil hvis ingen melder noget.
|
||||
function SmartPickupFrame.monthCpProgress(fieldId)
|
||||
local sum, count = 0, 0
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||
local vehicle = assignment.vehicle
|
||||
if not assignment.helper and vehicle ~= nil and vehicle.getCpFieldWorkProgress ~= nil then
|
||||
local ok, progress = pcall(vehicle.getCpFieldWorkProgress, vehicle)
|
||||
if ok and type(progress) == "number" then
|
||||
sum, count = sum + progress, count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return count > 0 and sum / count or nil
|
||||
end
|
||||
|
||||
function SmartPickupFrame.monthContext(period)
|
||||
return {
|
||||
period = period,
|
||||
plantableIn = function(name, forPeriod)
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and ADCpBridge.isPlantableIn(fruit, forPeriod)
|
||||
end,
|
||||
withers = function(info, nextPeriod)
|
||||
local fruit = info.fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
|
||||
local ok, result = pcall(ADMonthView.withersIn, fruit, nextPeriod, info.growthState)
|
||||
return ok and result == true
|
||||
end,
|
||||
cpProgress = SmartPickupFrame.monthCpProgress,
|
||||
}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:monthStageText(stage)
|
||||
if stage == nil then return "–" end
|
||||
return self.i18n:getText("spu_field_stage_" .. tostring(stage))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:monthCells(row)
|
||||
local crop = row.sowFruit ~= nil and self:fruitTitle(row.sowFruit) or (row.crop or "")
|
||||
return {"F" .. tostring(row.id), self:monthStageText(row.stage), crop, ADMonthView.percentText(row.progress),
|
||||
self:monthStageText(row.nextStage), self.i18n:getText("spu_month_window_" .. row.window)}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillMonthRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
local clock = SmartPickupFrame.monthClock()
|
||||
if clock == nil then
|
||||
self:addTextRow(i18n:getText("spu_month_noClock"))
|
||||
return
|
||||
end
|
||||
self:addTextRow(string.format(i18n:getText("spu_month_header"), self:monthPeriodText(clock.period),
|
||||
clock.day or 1, clock.days or 1, clock.hour, clock.minute))
|
||||
local infos = ADFields.list()
|
||||
if #infos == 0 then
|
||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||
return
|
||||
end
|
||||
local view = ADMonthView.build(infos, ADFieldJobs.state.fields, SmartPickupFrame.monthContext(clock.period))
|
||||
if view.critical > 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_month_verdictUrgent"), view.critical))
|
||||
else
|
||||
self:addTextRow(i18n:getText("spu_month_verdictCalm"))
|
||||
end
|
||||
if #view.rows == 0 then
|
||||
self:addTextRow(i18n:getText("spu_month_noWork"))
|
||||
else
|
||||
self:addCellRow(self.monthPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_month_col_work"), i18n:getText("spu_col_crop"),
|
||||
i18n:getText("spu_month_col_progress"), i18n:getText("spu_month_col_next"), i18n:getText("spu_month_col_window")})
|
||||
for _, row in ipairs(view.rows) do
|
||||
self:addCellRow(self.monthPrefab, self:monthCells(row))
|
||||
end
|
||||
end
|
||||
if #view.upcoming > 0 then
|
||||
self:addTextRow(i18n:getText("spu_month_upcoming"))
|
||||
for _, entry in ipairs(view.upcoming) do
|
||||
local key = entry.closes and "spu_month_upcomingLast" or "spu_month_upcomingOpen"
|
||||
self:addTextRow(string.format(i18n:getText(key), entry.id, self:fruitTitle(entry.fruit)))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -57,66 +57,9 @@ function SmartPickupFrame.getPolicyIndex(policies, policy)
|
||||
return 1
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillSourceRows(adEnv, run)
|
||||
local chosen = {}
|
||||
for _, id in ipairs(run.sourceWayPointIds or {}) do
|
||||
chosen[id] = true
|
||||
end
|
||||
local entries = ADRunsController.getSourceMarkers(adEnv, g_currentMission:getFarmId())
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "source", wayPointId = entry.marker.id})
|
||||
local kindText = self.i18n:getText("spu_source_kind_" .. tostring(entry.kind))
|
||||
row:getDescendantByName("title"):setText(string.format(" %s — %s (%s)", tostring(entry.marker.name), tostring(entry.name), kindText))
|
||||
row:getDescendantByName("option"):setState(chosen[entry.marker.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
if #entries == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_noSources"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillProductRows(adEnv, run)
|
||||
self.productRows = {}
|
||||
local products = ADRunsController.getRunProducts(adEnv, run)
|
||||
for _, product in ipairs(products) do
|
||||
local policies = SmartPickupFrame.getPoliciesFor(product)
|
||||
local policyTexts = {}
|
||||
for _, policy in ipairs(policies) do
|
||||
table.insert(policyTexts, self.i18n:getText("spu_policy_" .. policy))
|
||||
end
|
||||
local row = self:addRow(self.productPrefab, {kind = "product", name = product.name, policies = policies})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts(policyTexts)
|
||||
option:setState(SmartPickupFrame.getPolicyIndex(policies, product.policy), false)
|
||||
local title = row:getDescendantByName("title")
|
||||
title:setText(self:describeProduct(adEnv, product))
|
||||
table.insert(self.productRows, {name = product.name, title = title, option = option})
|
||||
end
|
||||
if #products == 0 then
|
||||
self:addTextRow(#(run.sourceWayPointIds or {}) == 0 and self.i18n:getText("spu_noSources") or self.i18n:getText("spu_noProducts"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillOutboundStatusRows(adEnv, run)
|
||||
self.outboundStatusRows = {}
|
||||
local status = ADRunsController.getOutboundStatus(adEnv, run)
|
||||
for _, reason in ipairs(status.reasons or {}) do
|
||||
table.insert(self.outboundStatusRows, self:addTextRow(string.format(self.i18n:getText("spu_outbound_waiting"), reason)))
|
||||
end
|
||||
if status.warning ~= nil then
|
||||
table.insert(self.outboundStatusRows, self:addTextRow(string.format(self.i18n:getText("spu_outbound_warning"), status.warning)))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:rebuildOutboundTab(adEnv, run)
|
||||
self.sellAtOption:setState(SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT), false)
|
||||
self.storeAboveOption:setState(SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT), false)
|
||||
self.collectRoundRowOption = self:addToggleRow("collectRound", self.i18n:getText("spu_collectRound"), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, run.collectRound == true)
|
||||
self:addTextRow(self.i18n:getText("spu_section_sources"))
|
||||
self:fillSourceRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_products"))
|
||||
self:fillProductRows(adEnv, run)
|
||||
self:fillOutboundStatusRows(adEnv, run)
|
||||
end
|
||||
|
||||
-- Hvert sekund: produktrækkernes mængde/pris og statuslinjerne (antal linjer ændres kun ved rebuild).
|
||||
function SmartPickupFrame:refreshOutboundRows(adEnv, run)
|
||||
@@ -167,21 +110,21 @@ end
|
||||
|
||||
-- ---- callbacks ----
|
||||
|
||||
function SmartPickupFrame:onClickSource(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, run.id, data.wayPointId)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': sources=%s", tostring(run.name), table.concat(ADRuns.getRun(ADRunsController.store, run.id).sourceWayPointIds, " "))
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickProduct(state, element)
|
||||
local clicked = self.rowData[element]
|
||||
if clicked ~= nil and clicked.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "storageView" then
|
||||
return self:safe(function(frame) frame:onClickStorageView(state) end)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
||||
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
||||
end
|
||||
if clicked ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[clicked.kind] then
|
||||
return self:onClickFieldSiloRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
||||
return self:onClickFieldRow(state, clicked)
|
||||
end
|
||||
@@ -210,10 +153,3 @@ function SmartPickupFrame:onClickStoreAbove(state)
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickCollectRound(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {collectRound = state == SmartPickupFrame.STATE_ON})
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -128,6 +128,7 @@ end
|
||||
|
||||
function SmartPickupFrame:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||
self.storageDetailRows = {}
|
||||
self.storageDetailPlaceable = nil
|
||||
if #inventory == 0 then
|
||||
return
|
||||
end
|
||||
@@ -177,6 +178,12 @@ function SmartPickupFrame:fillStorageRows(adEnv)
|
||||
self.storageRows = {}
|
||||
local fullInventory = self:getInventory(adEnv)
|
||||
local inventory = ADStorageStats.filterKind(fullInventory, SmartPickupFrame.getTabKind(self.currentTab))
|
||||
self:addStorageViewRow()
|
||||
if self:getStorageView() == SmartPickupFrame.STORAGE_VIEW_ITEMS then
|
||||
self.storageDetailPlaceable = nil
|
||||
self:fillItemOverview(adEnv, inventory, fullInventory)
|
||||
return
|
||||
end
|
||||
self:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||
self:addTextRow(self.i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))))
|
||||
for _, entry in ipairs(inventory) do
|
||||
@@ -193,6 +200,11 @@ end
|
||||
|
||||
function SmartPickupFrame:refreshStorageRows(adEnv)
|
||||
local kind = SmartPickupFrame.getTabKind(self.currentTab)
|
||||
if kind ~= nil and self.itemView ~= nil then
|
||||
local fullInventory = self:getInventory(adEnv)
|
||||
self:refreshItemOverview(adEnv, ADStorageStats.filterKind(fullInventory, kind), fullInventory)
|
||||
return
|
||||
end
|
||||
if kind == nil or #(self.storageRows or {}) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
-- Smart Pickup — fanerne Lagre, vareoversigten (v1.52)
|
||||
-- Én række pr. vare med samlet beholdning, fyldningsbjælke, værdi og gårdens netto; klik folder varen ud
|
||||
-- til bygningerne den ligger i. Øverst nøgletal og "kræver handling". Tallene regnes i ADStorageStats;
|
||||
-- her tegnes kun. Indlæses efter SmartPickupStorageTab.lua (funktionerne lægges på SmartPickupFrame).
|
||||
|
||||
SmartPickupFrame.STORAGE_VIEW_ITEMS, SmartPickupFrame.STORAGE_VIEW_BUILDINGS = 1, 2
|
||||
SmartPickupFrame.MAX_ATTENTION = 4
|
||||
-- nøgletalsrækken er højere end de andre rækker (tal over label)
|
||||
SmartPickupFrame.KPI_ROW_SCALE = 1.6
|
||||
-- ændret struktur (ny vare, flere alarmer) genopbygges højst hvert N. sekund, så fokus ikke hopper
|
||||
SmartPickupFrame.ITEM_REBUILD_TICKS = 10
|
||||
|
||||
SmartPickupFrame.LEVEL_COLORS = {
|
||||
red = {0.90, 0.25, 0.20, 1},
|
||||
yellow = {0.95, 0.72, 0.15, 1},
|
||||
green = {0.30, 0.75, 0.35, 1},
|
||||
}
|
||||
SmartPickupFrame.NEUTRAL_BAR_COLOR = {0.35, 0.60, 0.85, 1}
|
||||
SmartPickupFrame.TEXT_COLOR = {1, 1, 1, 1}
|
||||
|
||||
local unpackFn = table.unpack or unpack
|
||||
|
||||
-- ---- små tegne-hjælpere ----
|
||||
|
||||
local function setTextColor(element, color)
|
||||
if element ~= nil and element.setTextColor ~= nil then element:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
|
||||
-- Fyld bjælken til brøken af baggrundens bredde; tom bjælke skjules (bredde 0 tegnes som en prik).
|
||||
function SmartPickupFrame.setBar(bar, background, fraction, color)
|
||||
if bar == nil or background == nil or background.size == nil then return end
|
||||
if fraction <= 0 then
|
||||
bar:setVisible(false)
|
||||
return
|
||||
end
|
||||
bar:setVisible(true)
|
||||
bar:setSize(background.size[1] * fraction, background.size[2])
|
||||
if bar.setImageColor ~= nil then bar:setImageColor(nil, unpackFn(color)) end
|
||||
end
|
||||
|
||||
-- Bjælkens farve: rød/gul ved problem, ellers grøn for varer i omløb og blå for rent lager.
|
||||
function SmartPickupFrame.barColor(level, isActive)
|
||||
if level ~= nil then return SmartPickupFrame.LEVEL_COLORS[level] end
|
||||
return isActive and SmartPickupFrame.LEVEL_COLORS.green or SmartPickupFrame.NEUTRAL_BAR_COLOR
|
||||
end
|
||||
|
||||
function SmartPickupFrame:formatDays(days)
|
||||
return string.format(self.i18n:getText("spu_stat_days"), days)
|
||||
end
|
||||
|
||||
-- "−14.4k/døgn · 3.2 døgn", "+3.6k/døgn · fuld om 2.0 døgn", "+1.2k/døgn · stiger" eller "-"
|
||||
function SmartPickupFrame:itemRateText(stat)
|
||||
local fc = SmartPickupFrame.formatCompact
|
||||
if (stat.net or 0) == 0 then
|
||||
return (stat.consumption or 0) > 0 and self.i18n:getText("spu_item_balanced") or "-"
|
||||
end
|
||||
local rate = string.format(self.i18n:getText("spu_item_perDay"), (stat.net < 0 and "-" or "+") .. fc(math.abs(stat.net)))
|
||||
if stat.days ~= nil then
|
||||
return rate .. " · " .. self:formatDays(stat.days)
|
||||
elseif stat.daysToFull ~= nil then
|
||||
return rate .. " · " .. string.format(self.i18n:getText("spu_item_fullIn"), self:formatDays(stat.daysToFull))
|
||||
end
|
||||
return rate .. " · " .. self.i18n:getText("spu_stat_rising")
|
||||
end
|
||||
|
||||
-- "1.2M/1.5M · 80 %" eller "24 paller · 1 %"
|
||||
function SmartPickupFrame:itemAmountText(entry)
|
||||
local fc = SmartPickupFrame.formatCompact
|
||||
local amount = entry.count ~= nil and string.format(self.i18n:getText("spu_storage_pallets"), entry.count)
|
||||
or string.format("%s/%s", fc(entry.level or 0), fc(entry.capacity or 0))
|
||||
if entry.percent ~= nil then amount = amount .. string.format(" · %d %%", entry.percent) end
|
||||
return amount
|
||||
end
|
||||
|
||||
-- ---- data ----
|
||||
|
||||
-- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården.
|
||||
function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory)
|
||||
self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local totals = ADStorageStats.farmTotals(fullInventory)
|
||||
local capacities = ADStorageStats.farmCapacities(fullInventory)
|
||||
local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {}
|
||||
local stats = {}
|
||||
local titleOf = function(fillType) return self:fillTypeTitle(fillType) end
|
||||
for _, group in ipairs(ADStorageStats.groupByFillType(inventory, titleOf)) do
|
||||
local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(group.fillType) or nil
|
||||
local farm = {level = totals[group.fillType], capacity = capacities[group.fillType]}
|
||||
table.insert(stats, ADStorageStats.groupStats(group, farm, rates[group.fillType], quote))
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Nøgle der ændrer sig når rækkerne skal være anderledes (varer, udfoldede bygninger, alarmer).
|
||||
function SmartPickupFrame:itemStructureKey(stats, attention)
|
||||
local parts = {}
|
||||
for _, stat in ipairs(stats) do
|
||||
table.insert(parts, tostring(stat.fillType))
|
||||
if self:isItemExpanded(stat.fillType) then
|
||||
-- sorteret: at to bygninger bytter plads i mængde må ikke udløse en genopbygning
|
||||
local placeables = {}
|
||||
for _, building in ipairs(stat.buildings) do table.insert(placeables, tostring(building.placeable)) end
|
||||
table.sort(placeables)
|
||||
table.insert(parts, table.concat(placeables, ";"))
|
||||
end
|
||||
end
|
||||
table.insert(parts, "a" .. #attention)
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:isItemExpanded(fillType)
|
||||
local expanded = self.expandedItems ~= nil and self.expandedItems[self.currentTab] or nil
|
||||
return expanded ~= nil and expanded[fillType] == true
|
||||
end
|
||||
|
||||
function SmartPickupFrame:toggleItem(fillType)
|
||||
self.expandedItems = self.expandedItems or {}
|
||||
self.expandedItems[self.currentTab] = self.expandedItems[self.currentTab] or {}
|
||||
local expanded = self.expandedItems[self.currentTab]
|
||||
expanded[fillType] = not expanded[fillType] or nil
|
||||
-- listen hopper ellers til toppen ved genopbygningen
|
||||
local layout = self.boxLayout
|
||||
local scrollY = layout ~= nil and layout.firstVisibleY or nil
|
||||
self:rebuild()
|
||||
if scrollY ~= nil and layout.scrollTo ~= nil then pcall(layout.scrollTo, layout, scrollY, true) end
|
||||
end
|
||||
|
||||
-- ---- visningsvælger (Varer | Bygninger) ----
|
||||
|
||||
function SmartPickupFrame:getStorageView()
|
||||
local views = self.storageViewByTab or {}
|
||||
return views[self.currentTab] or SmartPickupFrame.STORAGE_VIEW_ITEMS
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addStorageViewRow()
|
||||
local row = self:addRow(self.productPrefab, {kind = "storageView"})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts({self.i18n:getText("spu_view_items"), self.i18n:getText("spu_view_buildings")})
|
||||
option:setState(self:getStorageView(), false)
|
||||
row:getDescendantByName("title"):setText(self.i18n:getText("spu_view_title"))
|
||||
local tooltip = option:getDescendantByName("ignore")
|
||||
if tooltip ~= nil then tooltip:setText(self.i18n:getText("spu_view_tooltip")) end
|
||||
option.toolTipText = self.i18n:getText("spu_view_tooltip")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickStorageView(state)
|
||||
self.storageViewByTab = self.storageViewByTab or {}
|
||||
self.storageViewByTab[self.currentTab] = state
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- ---- opbygning ----
|
||||
|
||||
function SmartPickupFrame:kpiTexts(stats)
|
||||
local kpi = ADStorageStats.kpis(stats)
|
||||
local i18n = self.i18n
|
||||
return {
|
||||
{SmartPickupFrame.formatMoney(kpi.valueNow), i18n:getText("spu_kpi_value")},
|
||||
{"+" .. SmartPickupFrame.formatMoney(kpi.gain), i18n:getText("spu_kpi_gain")},
|
||||
{tostring(kpi.emptyCount), i18n:getText("spu_kpi_empty")},
|
||||
{tostring(kpi.fullCount), i18n:getText("spu_kpi_full")},
|
||||
}, kpi
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setKpiRow(row, stats)
|
||||
local texts, kpi = self:kpiTexts(stats)
|
||||
for index, pair in ipairs(texts) do
|
||||
local value = row:getDescendantByName("v" .. index)
|
||||
if value ~= nil then value:setText(pair[1]) end
|
||||
local label = row:getDescendantByName("l" .. index)
|
||||
if label ~= nil then label:setText(pair[2]) end
|
||||
end
|
||||
local colors = SmartPickupFrame.LEVEL_COLORS
|
||||
setTextColor(row:getDescendantByName("v2"), kpi.gain > 0 and colors.green or SmartPickupFrame.TEXT_COLOR)
|
||||
setTextColor(row:getDescendantByName("v3"), kpi.emptyCount > 0 and colors.red or SmartPickupFrame.TEXT_COLOR)
|
||||
setTextColor(row:getDescendantByName("v4"), kpi.fullCount > 0 and colors.yellow or SmartPickupFrame.TEXT_COLOR)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addKpiRow(stats)
|
||||
local row = self:addRow(self.kpiPrefab, {kind = "text"})
|
||||
if row.size ~= nil then row:setSize(row.size[1], row.size[2] * SmartPickupFrame.KPI_ROW_SCALE) end
|
||||
self:setKpiRow(row, stats)
|
||||
return row
|
||||
end
|
||||
|
||||
function SmartPickupFrame:alertText(entry)
|
||||
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
||||
if entry.kind == "empty" then
|
||||
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
|
||||
elseif entry.kind == "full" then
|
||||
if stat.daysToFull ~= nil then
|
||||
return string.format(i18n:getText("spu_alert_fullIn"), stat.title, stat.percent or 0, self:formatDays(stat.daysToFull))
|
||||
end
|
||||
return string.format(i18n:getText("spu_alert_full"), stat.title, stat.percent or 0)
|
||||
end
|
||||
return string.format(i18n:getText("spu_alert_sell"), stat.title, math.floor((stat.priceRatio or 0) * 100 + 0.5), SmartPickupFrame.formatMoney(stat.valueNow))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setAlertRow(row, entry)
|
||||
row.entry = entry
|
||||
local hint = ""
|
||||
if SmartPickupFrame.attentionAdvice ~= nil then
|
||||
local ok, advice, info = pcall(self.attentionAdvice, self, entry)
|
||||
if ok then hint = self:attentionHint(advice, info) end
|
||||
end
|
||||
row.c1:setText(self:alertText(entry) .. hint)
|
||||
if row.dot ~= nil and row.dot.setImageColor ~= nil then
|
||||
row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:itemCells(stat)
|
||||
local marker = self:isItemExpanded(stat.fillType) and "– " or "+ "
|
||||
local value = stat.valueNow ~= nil and SmartPickupFrame.formatMoney(stat.valueNow) or self.i18n:getText("spu_stat_noSell")
|
||||
return {marker .. stat.title, self:itemAmountText(stat), value, self:itemRateText(stat)}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setItemRow(row, stat)
|
||||
SmartPickupFrame.setCells(row.cells, self:itemCells(stat))
|
||||
local isActive = (stat.consumption or 0) > 0 or (stat.production or 0) > 0
|
||||
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(stat.percent), SmartPickupFrame.barColor(stat.severity, isActive))
|
||||
local rateColor = SmartPickupFrame.TEXT_COLOR
|
||||
if stat.emptyLevel ~= nil then
|
||||
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.emptyLevel]
|
||||
elseif stat.fullLevel ~= nil then
|
||||
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.fullLevel]
|
||||
end
|
||||
setTextColor(row.cells[4], rateColor)
|
||||
end
|
||||
|
||||
-- Bygningens andel af varen: værdi efter samme pris som varen, bjælke efter bygningens egen fyldning.
|
||||
function SmartPickupFrame:setSubRow(row, stat, building)
|
||||
local kind = SmartPickupFrame.getTabKind(self.currentTab)
|
||||
local isStableOutput = kind == "husbandry" and (stat.production or 0) > 0 and (stat.consumption or 0) == 0
|
||||
local value = "-"
|
||||
if stat.valueNow ~= nil and stat.level > 0 then
|
||||
value = SmartPickupFrame.formatMoney(stat.valueNow * building.level / stat.level)
|
||||
end
|
||||
local share = stat.level > 0 and math.floor(100 * building.level / stat.level + 0.5) or 0
|
||||
SmartPickupFrame.setCells(row.cells, {tostring(building.name), self:itemAmountText(building), value,
|
||||
string.format(self.i18n:getText("spu_item_share"), share)})
|
||||
local level = ADStorageStats.buildingLevel(building, kind, isStableOutput)
|
||||
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(building.percent), SmartPickupFrame.barColor(level, false))
|
||||
end
|
||||
|
||||
-- Klon af item-/sub-prefab med referencer til cellerne og bjælken.
|
||||
function SmartPickupFrame:addBarRow(prefab)
|
||||
local row = self:addRow(prefab, {kind = "text"})
|
||||
local cells = {}
|
||||
for index = 1, 4 do cells[index] = row:getDescendantByName("c" .. index) end
|
||||
return {element = row, cells = cells, bar = row:getDescendantByName("bar"), barBg = row:getDescendantByName("barBg")}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addItemHeader()
|
||||
local i18n = self.i18n
|
||||
local header = self:addBarRow(self.itemPrefab)
|
||||
SmartPickupFrame.setCells(header.cells, {i18n:getText("spu_col_item"), i18n:getText("spu_icol_amount"),
|
||||
i18n:getText("spu_icol_value"), i18n:getText("spu_icol_rate")})
|
||||
if header.bar ~= nil then header.bar:setVisible(false) end
|
||||
if header.barBg ~= nil then header.barBg:setVisible(false) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
|
||||
local i18n = self.i18n
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
|
||||
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
|
||||
self:addTextRow(i18n:getText("spu_attention"))
|
||||
if #attention == 0 then
|
||||
self:addTextRow(i18n:getText("spu_attention_none"))
|
||||
end
|
||||
for _, entry in ipairs(attention) do
|
||||
local element = self:addRow(self.alertPrefab, {kind = "text"})
|
||||
local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")}
|
||||
self:setAlertRow(row, entry)
|
||||
-- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua)
|
||||
self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end)
|
||||
table.insert(self.itemView.alertRows, row)
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint"))
|
||||
if #stats == 0 then
|
||||
self:addTextRow(i18n:getText("spu_storage_empty"))
|
||||
return
|
||||
end
|
||||
self:addItemHeader()
|
||||
for _, stat in ipairs(stats) do
|
||||
local row = self:addBarRow(self.itemPrefab)
|
||||
local fillType = stat.fillType
|
||||
self:makeRowClickable(row.element, function(frame) frame:toggleItem(fillType) end)
|
||||
self:setItemRow(row, stat)
|
||||
row.subs = {}
|
||||
if self:isItemExpanded(fillType) then
|
||||
for _, building in ipairs(stat.buildings) do
|
||||
local sub = self:addBarRow(self.subPrefab)
|
||||
sub.placeable = building.placeable
|
||||
self:setSubRow(sub, stat, building)
|
||||
table.insert(row.subs, sub)
|
||||
end
|
||||
end
|
||||
table.insert(self.itemView.itemRows, row)
|
||||
end
|
||||
end
|
||||
|
||||
-- Hvert sekund: tal og bjælker opdateres på stedet; ny struktur -> genopbygning (højst hvert 10. s).
|
||||
function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
|
||||
local view = self.itemView
|
||||
if view == nil then return end
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
if self:itemStructureKey(stats, attention) ~= view.key then
|
||||
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
|
||||
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
|
||||
return
|
||||
end
|
||||
self:setKpiRow(view.kpiRow, stats)
|
||||
for index, entry in ipairs(attention) do self:setAlertRow(view.alertRows[index], entry) end
|
||||
for index, stat in ipairs(stats) do
|
||||
local row = view.itemRows[index]
|
||||
self:setItemRow(row, stat)
|
||||
-- underrækkerne beholder rækkefølgen fra opbygningen; tallene findes via bygningen
|
||||
local byPlaceable = {}
|
||||
for _, building in ipairs(stat.buildings) do byPlaceable[building.placeable] = building end
|
||||
for _, sub in ipairs(row.subs) do
|
||||
local building = byPlaceable[sub.placeable]
|
||||
if building ~= nil then self:setSubRow(sub, stat, building) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,42 +2,8 @@
|
||||
-- Mål, varer (Auto/Vælg), behovsrække, ture og "kør hjem"-tærskel for et forsynings-run.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
function SmartPickupFrame:rebuildSupplyTab(adEnv, run)
|
||||
self.loopsOption:setState(run.loops + 1, false)
|
||||
self.returnBelowOption:setState(self:getReturnBelowIndex(run), false)
|
||||
self:addTextRow(self.i18n:getText("spu_section_targets"))
|
||||
self.ingredientModeRowOption = self:addToggleRow("ingredientMode", self.i18n:getText("spu_ingredientMode"), {self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")}, run.ingredientMode == "manual")
|
||||
self:addTextRow(self.i18n:getText("spu_targets"))
|
||||
self:fillTargetRows(adEnv, run)
|
||||
self:fillIngredientRows(adEnv, run)
|
||||
self:fillNeedsRows(adEnv, run)
|
||||
end
|
||||
|
||||
-- Mål = afkrydsning (flere stalde/fabrikker pr. run). Ved hver tur vælges den der mangler mest.
|
||||
function SmartPickupFrame:fillTargetRows(adEnv, run)
|
||||
local chosen = {}
|
||||
for _, id in ipairs(ADRunsController.getRunTargets(run)) do
|
||||
chosen[id] = true
|
||||
end
|
||||
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, g_currentMission:getFarmId())) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "target", wayPointId = marker.id})
|
||||
row:getDescendantByName("title"):setText(" " .. tostring(marker.name))
|
||||
row:getDescendantByName("option"):setState(chosen[marker.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Behovsrække pr. mål ("Cow 1 Food: Forage 0/33.750 l · …"), opdateres hvert sekund.
|
||||
function SmartPickupFrame:fillNeedsRows(adEnv, run)
|
||||
self.needsRows = {}
|
||||
local targets = ADRunsController.getTargetsLevels(adEnv, run)
|
||||
if #targets == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_needs_noTarget"))
|
||||
return
|
||||
end
|
||||
for _, target in ipairs(targets) do
|
||||
table.insert(self.needsRows, {wayPointId = target.wayPointId, title = self:addTextRow(self:describeTargetNeeds(target))})
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:describeTargetNeeds(target)
|
||||
if #target.levels == 0 then
|
||||
@@ -58,39 +24,8 @@ function SmartPickupFrame:refreshNeedsRows(adEnv, run)
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillIngredientRows(adEnv, run)
|
||||
if run.ingredientMode ~= "manual" then
|
||||
return
|
||||
end
|
||||
local chosen = {}
|
||||
for _, name in ipairs(run.ingredients) do
|
||||
chosen[name] = true
|
||||
end
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, run)) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "ingredient", name = fillType.name})
|
||||
row:getDescendantByName("title"):setText(tostring(fillType.title))
|
||||
row:getDescendantByName("option"):setState(chosen[fillType.name] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickTarget(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
ADRunsController.store = ADRuns.toggleTarget(ADRunsController.store, run.id, data.wayPointId)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': targets=%s", tostring(run.name), table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, run.id)), " "))
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickIngredientMode(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {ingredientMode = state == SmartPickupFrame.STATE_ON and "manual" or "auto"})
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickLoops(state)
|
||||
self:safe(function(frame)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<Profile name="spuInfoText" extends="fs25_settingsMultiTextOptionTitle">
|
||||
<textBold value="false"/>
|
||||
<size value="1400px 40px"/>
|
||||
<textMaxWidth value="800px"/>
|
||||
</Profile>
|
||||
<!-- Tabel-celler i fanen Lagre: position og bredde sættes pr. celle i SmartPickupFrame.xml. -->
|
||||
<Profile name="spuCell" extends="fs25_settingsMultiTextOptionTitle">
|
||||
@@ -19,4 +20,53 @@
|
||||
<Profile name="spuCellBold" extends="spuCell">
|
||||
<textBold value="true"/>
|
||||
</Profile>
|
||||
<!-- Lagre (v1.52): bjælker = hvid pixel i spillets UI-atlas farvet med imageColor
|
||||
(samme teknik som Precision Farming). Bredden på fyldet sættes i Lua. -->
|
||||
<Profile name="spuBar" extends="baseReference" with="anchorMiddleLeft">
|
||||
<imageUVs value="10px 1010px 4px 4px"/>
|
||||
<positionOrigin value="middleLeft"/>
|
||||
<imageColor value="0.30 0.75 0.35 1"/>
|
||||
<size value="200px 16px"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarBg" extends="spuBar">
|
||||
<imageColor value="0 0 0 0.45"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarThin" extends="spuBar">
|
||||
<size value="200px 10px"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarThinBg" extends="spuBarThin">
|
||||
<imageColor value="0 0 0 0.45"/>
|
||||
</Profile>
|
||||
<!-- markør foran en "kræver handling"-linje -->
|
||||
<Profile name="spuDot" extends="spuBar">
|
||||
<size value="8px 26px"/>
|
||||
</Profile>
|
||||
<!-- nøgletal: stort tal over lille label -->
|
||||
<Profile name="spuKpiValue" extends="spuCellBold">
|
||||
<textSize value="24px"/>
|
||||
<textOffset value="0px 8px"/>
|
||||
</Profile>
|
||||
<Profile name="spuKpiLabel" extends="spuCell">
|
||||
<textSize value="14px"/>
|
||||
<textOffset value="0px -18px"/>
|
||||
<textColor value="0.75 0.75 0.75 1"/>
|
||||
</Profile>
|
||||
<Profile name="spuCellDim" extends="spuCell">
|
||||
<textColor value="0.75 0.75 0.75 1"/>
|
||||
</Profile>
|
||||
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser/placering sættes i Lua ud fra boxLayout -->
|
||||
<Profile name="spuOverlayBackdrop" extends="baseReference" with="anchorTopLeft">
|
||||
<imageUVs value="10px 1010px 4px 4px"/>
|
||||
<positionOrigin value="bottomLeft"/>
|
||||
<imageColor value="0 0 0 0.65"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<!-- farver er lineære: 0.016 ≈ menuens egen mørke baggrund (0.09 blev lysegrå, in-game 29/9) -->
|
||||
<Profile name="spuOverlayPanel" extends="spuOverlayBackdrop">
|
||||
<imageColor value="0.016 0.016 0.016 1"/>
|
||||
</Profile>
|
||||
<Profile name="spuPanelTitle" extends="spuCellBold">
|
||||
<textSize value="20px"/>
|
||||
<size value="700px 30px"/>
|
||||
</Profile>
|
||||
</GUIProfiles>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<text name="spu_title" text="Smart Pickup"/>
|
||||
<text name="spu_run" text="Run"/>
|
||||
<text name="spu_run_tooltip" text="Vælg det run du vil se eller ændre."/>
|
||||
<text name="spu_noRuns" text="Ingen runs — tryk 'Nyt run'"/>
|
||||
<text name="spu_noRuns" text="Ingen flows — tryk 'Nyt flow'"/>
|
||||
<text name="spu_target" text="Mål"/>
|
||||
<text name="spu_target_tooltip" text="AutoDrive-markør ved stalden eller fabrikken der skal fyldes."/>
|
||||
<text name="spu_noTarget" text="(vælg mål)"/>
|
||||
@@ -27,7 +27,10 @@
|
||||
<text name="spu_status_started" text="Startet %d, sprunget over %d"/>
|
||||
<text name="spu_status_noAd" text="AutoDrive-API ikke fundet"/>
|
||||
<text name="spu_status_error" text="Fejl: %s"/>
|
||||
<text name="spu_reason_noTrailer" text="ingen vogn"/>
|
||||
<text name="spu_guide_flow_wagon" text=" ↳ Vogn"/>
|
||||
<text name="spu_guide_flow_noFreeWagon" text=" ↳ ingen ledig vogn (frakoblet, med Connect-markør, kan bære varen)"/>
|
||||
<text name="spu_guide_flow_soloText" text="%s — ingen vogn bagpå: henter en ledig vogn"/>
|
||||
<text name="spu_reason_noTrailer" text="ingen vogn (kobl en på, eller giv vognens parkering flowets navn)"/>
|
||||
<text name="spu_reason_cannotCarry" text="vognen kan ikke bære run'ets varer"/>
|
||||
<text name="spu_reason_farFromNetwork" text="for langt fra vejnettet"/>
|
||||
<text name="spu_reason_noTarget" text="intet mål"/>
|
||||
@@ -40,7 +43,28 @@
|
||||
<text name="spu_status_stoppedAll" text="Stoppet %d traktorer"/>
|
||||
<text name="spu_overview_hint" text="Klik på et flow for at redigere det"/>
|
||||
<text name="spu_tab_fields" text="Marker"/>
|
||||
<text name="spu_tab_month" text="Måned"/>
|
||||
<text name="spu_month_months" text="jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec"/>
|
||||
<text name="spu_month_period" text="Periode %d"/>
|
||||
<text name="spu_month_header" text="%s · dag %d af %d · kl. %02d:%02d"/>
|
||||
<text name="spu_month_noClock" text="Kan ikke læse spillets kalender"/>
|
||||
<text name="spu_month_verdictCalm" text="Intet haster — du kan roligt sove til næste måned"/>
|
||||
<text name="spu_month_verdictUrgent" text="OBS: %d marker skal nå det før månedsskiftet"/>
|
||||
<text name="spu_month_noWork" text="Ingen marker har arbejde i gang"/>
|
||||
<text name="spu_month_col_work" text="Arbejde"/>
|
||||
<text name="spu_month_col_progress" text="Færdig"/>
|
||||
<text name="spu_month_col_next" text="Næste"/>
|
||||
<text name="spu_month_col_window" text="Vindue"/>
|
||||
<text name="spu_month_window_ok" text="ok"/>
|
||||
<text name="spu_month_window_harvestCloses" text="OBS: visner snart"/>
|
||||
<text name="spu_month_window_lastSowMonth" text="OBS: sidste såmåned"/>
|
||||
<text name="spu_month_upcoming" text="Kan sås nu, ikke gået i gang:"/>
|
||||
<text name="spu_month_upcomingOpen" text="F%d: %s — kan også sås næste måned"/>
|
||||
<text name="spu_month_upcomingLast" text="F%d: %s — OBS: sidste såmåned"/>
|
||||
<text name="spu_field_button_start" text="Start nu"/>
|
||||
<text name="spu_field_manualTransport" text=" %s kører selv på vejen (Nej = for bred, du kører den til marken)"/>
|
||||
<text name="spu_field_button_takeover" text="Start nu"/>
|
||||
<text name="spu_field_takeoverRequested" text="F%d: overtager høsteren nu, hvis skærebordet passer"/>
|
||||
<text name="spu_field_button_stop" text="Stop mark"/>
|
||||
<text name="spu_field_noMarker" text="! mangler markør F%d"/>
|
||||
<text name="spu_field_stage_idle" text="Venter"/>
|
||||
@@ -51,8 +75,45 @@
|
||||
<text name="spu_field_stage_collecting" text="Samles op"/>
|
||||
<text name="spu_field_stage_done" text="Færdig"/>
|
||||
<text name="spu_field_reason_noRig" text="ingen %s med flueben i markflåden"/>
|
||||
<text name="spu_field_reasonShort_noRig" text="ingen maskine"/>
|
||||
<text name="spu_field_reasonShort_allBusy" text="venter på maskine"/>
|
||||
<text name="spu_field_reasonShort_tooWide" text="for bred"/>
|
||||
<text name="spu_field_reasonShort_widthUnknown" text="bredde ukendt"/>
|
||||
<text name="spu_field_reasonShort_tooNarrow" text="for smal"/>
|
||||
<text name="spu_field_reasonShort_cannotDo" text="ingen kan"/>
|
||||
<text name="spu_field_reasonShort_noSowFruit" text="vælg afgrøde"/>
|
||||
<text name="spu_field_reasonShort_notEnough" text="for få ledige"/>
|
||||
<text name="spu_field_reasonShort_notPlantable" text="ikke såtid nu"/>
|
||||
<text name="spu_field_reasonShort_regrowing" text="venter på genvækst"/>
|
||||
<text name="spu_field_reasonShort_rain" text="venter på tørvejr"/>
|
||||
<text name="spu_field_reasonShort_text" text="se detaljer"/>
|
||||
<text name="spu_field_reason_allBusy" text="venter på ledig %s"/>
|
||||
<text name="spu_pool_section_tractors" text="Maskinpulje — traktorer (henter redskaber når ingen færdig kombination er ledig)"/>
|
||||
<text name="spu_pool_section_implements" text="Maskinpulje — redskaber (parkering "X" + markør "Connect X")"/>
|
||||
<text name="spu_pool_noTractors" text="ingen traktorer uden redskab"/>
|
||||
<text name="spu_pool_noImplements" text="ingen redskaber med en markrolle"/>
|
||||
<text name="spu_pool_tractor_free" text="ledig%.0s"/>
|
||||
<text name="spu_pool_tractor_noPark" text="mangler egen AD-parkering%.0s"/>
|
||||
<text name="spu_pool_tractor_player" text="spilleren kører den%.0s"/>
|
||||
<text name="spu_pool_tractor_returning" text="sætter redskab af og kører hjem%.0s"/>
|
||||
<text name="spu_pool_tractor_job" text="opgave F%s"/>
|
||||
<text name="spu_pool_tractor_busy" text="AutoDrive/Courseplay kører%.0s"/>
|
||||
<text name="spu_pool_tractor_notInFleet" text="ikke i markflåden (flueben)%.0s"/>
|
||||
<text name="spu_pool_tractor_offNetwork" text="står uden for AutoDrive-vejnettet%.0s"/>
|
||||
<text name="spu_pool_tractor_hitched" text="har et redskab på (kobl det af for at bruge den i puljen)%.0s"/>
|
||||
<text name="spu_pool_implement_flow" text="hører til flowet '%s'"/>
|
||||
<text name="spu_pool_implement_parked" text="parkeret ved %s"/>
|
||||
<text name="spu_pool_implement_noPark" text="ikke i puljen — mangler AD-parkering%.0s"/>
|
||||
<text name="spu_pool_implement_noConnect" text="ikke i puljen — mangler markøren "Connect %s""/>
|
||||
<text name="spu_pool_implement_attached" text="på %s"/>
|
||||
<text name="spu_pool_implement_quarantine" text="kunne ikke kobles på — prøves igen om %s min"/>
|
||||
<text name="spu_pool_implement_reserved" text="reserveret — en traktor er på vej%.0s"/>
|
||||
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
|
||||
<text name="spu_field_reason_fetching" text="henter '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s for bred: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_widthUnknown" text="%s: arbejdsbredde %.1f m kan ikke være rigtig — sæt bredden i Courseplay (sendes ikke ud)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
|
||||
<text name="spu_field_role_mower" text="slåmaskine"/>
|
||||
<text name="spu_field_role_tedder" text="vender"/>
|
||||
@@ -65,25 +126,33 @@
|
||||
<text name="spu_field_mode_manual" text="Manuel"/>
|
||||
<text name="spu_field_notMowable" text="ikke græs/kløver"/>
|
||||
<text name="spu_field_scanning" text="Marker indlæses ... (én mark pr. sekund)"/>
|
||||
<text name="spu_field_hint" text="Klik på en mark for at vælge den. Sæt flueben ved maskinerne i markflåden nederst."/>
|
||||
<text name="spu_field_hint" text="Klik på en mark for at se og rette dens opsætning."/>
|
||||
<text name="spu_field_fleetEmpty" text="! Markflåden står på "kun med flueben", men ingen maskiner har flueben — intet kan sendes ud. Åbn Markflåde og vælg "Brug alle ledige maskiner"."/>
|
||||
<text name="spu_field_showAll" text="Vis alle marker (%d skjult)"/>
|
||||
<text name="spu_col_field" text="Mark"/>
|
||||
<text name="spu_col_crop" text="Afgrøde"/>
|
||||
<text name="spu_col_plan" text="Plan"/>
|
||||
<text name="spu_col_mode" text="Kørsel"/>
|
||||
<text name="spu_field_section" text="— Mark F%d: %s —"/>
|
||||
<text name="spu_field_statusFull" text="Status: %s"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Kørsel"/>
|
||||
<text name="spu_field_rake" text="Riv skårene sammen før opsamling (gør smalle læssevogne brugbare)"/>
|
||||
<text name="spu_field_marker" text=" Markør %s, %.0f m fra marken"/>
|
||||
<text name="spu_field_mowWidth" text=" Slået med %.1f m — opsamling skal være mindst lige så bred"/>
|
||||
<text name="spu_field_section_store" text="— Tving lager (ellers nærmeste silo med plads) —"/>
|
||||
<text name="spu_field_section_dedicated" text="— Faste maskiner til denne mark (bruges kun her; ingen valgt = de frie) —"/>
|
||||
<text name="spu_field_dedicatedElsewhere" text="fast på F%s"/>
|
||||
<text name="spu_field_hiddenDedicated" text="%d maskiner er faste på andre marker (%s) og vises ikke her"/>
|
||||
<text name="spu_field_fieldSilo" text="marksilo"/>
|
||||
<text name="spu_field_storeHidden" text="+ %d siloer længere væk vises ikke"/>
|
||||
<text name="spu_field_noStore" text="ingen silo tager markens vare"/>
|
||||
<text name="spu_field_storeAuto" text="intet valgt: nærmeste silo med plads bruges"/>
|
||||
<text name="spu_field_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
|
||||
<text name="spu_field_section_fleet" text="— Markflåde (flueben = må sendes ud) —"/>
|
||||
<text name="spu_field_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
||||
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
||||
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d startes, når en maskine er ledig"/>
|
||||
@@ -94,6 +163,7 @@
|
||||
<text name="spu_field_fermenter" text="gæringssilo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="intet valgt: nærmeste gæringssilo med plads, ellers nærmeste silo"/>
|
||||
<text name="spu_field_bare" text="bar jord"/>
|
||||
<text name="spu_field_stubble" text="stub (%s)"/>
|
||||
<text name="spu_field_careNeed" text="trænger til %s"/>
|
||||
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
||||
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
||||
@@ -116,7 +186,7 @@
|
||||
<text name="spu_field_rig_seeds" text="frø ok"/>
|
||||
<text name="spu_field_rigs" text="Maskiner pr. trin (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="kultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="gødningsspreder"/>
|
||||
<text name="spu_field_role_fertilizer" text="gødskning"/>
|
||||
<text name="spu_field_role_harvester" text="høster"/>
|
||||
<text name="spu_field_role_limer" text="kalkspreder"/>
|
||||
<text name="spu_field_role_mulcher" text="stubhakker"/>
|
||||
@@ -152,14 +222,53 @@
|
||||
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
||||
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
||||
<text name="spu_field_notice_done" text="er færdig"/>
|
||||
<text name="spu_field_reason_regrowing" text="venter på at græsset vokser helt ud igen"/>
|
||||
<text name="spu_field_reason_rain" text="venter på tørvejr"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="Nyt run"/>
|
||||
<text name="spu_field_target" text="Afgrøde på marken (moden passer resten)"/>
|
||||
<text name="spu_field_target_none" text="Ingen — planlæg selv"/>
|
||||
<text name="spu_field_target_bare" text="Klargøres og tilsås med %s, så snart det er såtid"/>
|
||||
<text name="spu_field_target_keep" text="%s står allerede: slås og samles op, hver gang den er klar"/>
|
||||
<text name="spu_field_target_switch" text="%s får lov at vokse færdig og høstes/slås; derefter klargøres og tilsås med %s"/>
|
||||
<text name="spu_field_waitSowing" text="venter på såtid for %s"/>
|
||||
<text name="spu_field_fleetAll" text="Brug alle ledige maskiner (Nej = kun med flueben)"/>
|
||||
<text name="spu_field_role_baler" text="ballepresser"/>
|
||||
<text name="spu_field_role_baleCollector" text="ballesamler"/>
|
||||
<text name="spu_field_stage_strawCollecting" text="Halm samles"/>
|
||||
<text name="spu_field_stage_baling" text="Presses"/>
|
||||
<text name="spu_field_stage_baleCollecting" text="Baller samles"/>
|
||||
<text name="spu_field_straw" text="Halm efter høst"/>
|
||||
<text name="spu_field_straw_chop" text="Hakkes (spredes på marken)"/>
|
||||
<text name="spu_field_straw_collect" text="Samles op til silo"/>
|
||||
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
|
||||
<text name="spu_button_start" text="Start flow"/>
|
||||
<text name="spu_button_stop" text="Stop flow"/>
|
||||
<text name="spu_button_new" text="Nyt flow"/>
|
||||
<text name="spu_button_renameBuilding" text="Omdøb bygning"/>
|
||||
<text name="spu_renameBuildingTitle" text="Nyt navn på bygningen (tomt = butiksnavnet)"/>
|
||||
<text name="spu_button_stopPark" text="Stop alt og parkér"/>
|
||||
<text name="spu_status_queuedAll" text="Start alle: %d traktorer startes én ad gangen (hvert %d. sekund), %d flows kørte allerede, %d sprunget over"/>
|
||||
<text name="spu_status_stopParked" text="Stop alt: %d stoppet, %d på vej til parkering — pause til Start alle"/>
|
||||
<text name="spu_status_noPark" text="uden parkeringspunkt: %s"/>
|
||||
<text name="spu_status_fetchManual" text="hent selv (for brede): %s"/>
|
||||
<text name="spu_status_starting" text="Starter traktorer: %d af %d …"/>
|
||||
<text name="spu_status_paused" text="Pause — tryk Start alle for at fortsætte"/>
|
||||
<text name="spu_flow_pickOther" text="Vælg et andet flow (tavlen med alle flows står i Oversigt)"/>
|
||||
<text name="spu_button_rename" text="Omdøb"/>
|
||||
<text name="spu_button_delete" text="Slet run"/>
|
||||
<text name="spu_newRunName" text="Run %d"/>
|
||||
<text name="spu_renameTitle" text="Navn på run"/>
|
||||
<text name="spu_button_delete" text="Slet flow"/>
|
||||
<text name="spu_panel_close" text="Luk"/>
|
||||
<text name="spu_panel_gone" text="Posten findes ikke længere — vinduet er lukket"/>
|
||||
<text name="spu_flowList_hint" text="Klik på et flow for at rette det. [Start]/[Stop] yderst til højre styrer det direkte. Farver: grøn kører · gul venter · rød problem · grå stoppet. "Efter behov" = hver tur henter det målet har mindst af."/>
|
||||
<text name="spu_flowList_start" text="[Start]"/>
|
||||
<text name="spu_flowList_stop" text="[Stop]"/>
|
||||
<text name="spu_flowList_status" text="Flows i gang: %d af %d"/>
|
||||
<text name="spu_panel_flowTitle" text="Rediger flow: %s"/>
|
||||
<text name="spu_panel_fieldTitle" text="Mark F%d"/>
|
||||
<text name="spu_field_pausedSuffix" text="(pause)"/>
|
||||
<text name="spu_field_pausedBanner" text="! Markarbejdet står på pause siden "Stop alt" (gemmes med savegamet) — tryk Start alle i Oversigt, eller [Start] på en enkelt mark."/>
|
||||
<text name="spu_prune_removed" text="Flow '%s': %d traktor(er) findes ikke længere og er fjernet (%d tilbage)"/>
|
||||
<text name="spu_prune_empty" text="Flow '%s' har ingen traktorer længere og virker ikke — tildel en i Flows"/>
|
||||
<text name="spu_newRunName" text="Flow %d"/>
|
||||
<text name="spu_renameTitle" text="Navn på flow"/>
|
||||
<text name="spu_deleteConfirm" text="Slet run '%s'? Dets kørende traktorer stoppes. Kan ikke fortrydes."/>
|
||||
<text name="spu_capacity" text="%s l"/>
|
||||
<text name="spu_capacity_pallets" text="autoload (paller)"/>
|
||||
@@ -203,6 +312,8 @@
|
||||
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
|
||||
<text name="spu_storeAbove" text="Kør til lager over"/>
|
||||
<text name="spu_sellAbove" text="Sælg altid når kilden er over"/>
|
||||
<text name="spu_sellAbove_off" text="Fra"/>
|
||||
<text name="spu_storeAbove_tooltip" text="Varer med Lager-politik køres til silo når kildens lager er over denne fyldning (eller der er et fuldt læs)."/>
|
||||
<text name="spu_collectRound" text="Saml op på én tur"/>
|
||||
<text name="spu_collectRound_tooltip" text="Tipvogn: samme vare fra flere kilder til vognen er fuld. Autoload-vogn: flere pallevarer med samme mål på én tur."/>
|
||||
@@ -212,6 +323,7 @@
|
||||
<text name="spu_reason_noProducts" text="ingen vare valgt (sæt Lager/Salg på en vare ovenfor)"/>
|
||||
<text name="spu_reason_noWaitPoint" text="intet Wait-punkt i puljen (markør i en mappe med 'Wait' i navnet)"/>
|
||||
<text name="spu_reason_cargoNoTarget" text="lasten på vognen har intet mål (pris for lav / ingen silo) — tøm vognen"/>
|
||||
<text name="spu_reason_autoloadOn" text="AutoLoad er slået til på vognen — AutoDrive ser den kun som palle-/ballevogn (slå AutoLoad fra på vognen)"/>
|
||||
<text name="spu_reason_needsAutoload" text="pallevarer kræver Universal Autoload på vognen"/>
|
||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||
<text name="spu_price_pct" text="%d %% af top (%s)"/>
|
||||
@@ -237,14 +349,16 @@
|
||||
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
||||
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
||||
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
||||
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
||||
<text name="spu_flow_round" text="runde"/>
|
||||
<text name="spu_flow_ruleIn" text="behov"/>
|
||||
<text name="spu_flow_ruleInLoops" text="behov, %d ture"/>
|
||||
<text name="spu_flow_ruleIn" text="efter behov — laveste først"/>
|
||||
<text name="spu_flow_ruleInLoops" text="efter behov, %d ture"/>
|
||||
<text name="spu_markerAt" text="%s %d m"/>
|
||||
<text name="spu_noMarker" text="INGEN MARKØR inden for 40 m — sæt en AD-markør ved triggeren"/>
|
||||
<text name="spu_noBuildings" text="Ingen bygninger fundet"/>
|
||||
<text name="spu_buildingGone" text="bygningen findes ikke længere"/>
|
||||
<text name="spu_buildingGone" text="Fjernet bygning (findes ikke mere) — sæt NEJ"/>
|
||||
<text name="spu_reason_noMarker" text="'%s' mangler en AD-markør ved triggeren"/>
|
||||
<text name="spu_storage_select" text="Vis bygning"/>
|
||||
<text name="spu_stat_now" text="nu %s"/>
|
||||
@@ -284,5 +398,181 @@
|
||||
<text name="spu_col_lasts" text="Rækker til"/>
|
||||
<text name="spu_col_rate" text="Forbrug/prod. døgn"/>
|
||||
<text name="spu_storage_select_tooltip" text="Vælg bygningen tabellen viser."/>
|
||||
<text name="spu_guide_next" text="Næste"/>
|
||||
<text name="spu_guide_save" text="Gem"/>
|
||||
<text name="spu_guide_ok" text="OK"/>
|
||||
<text name="spu_guide_back" text="Tilbage"/>
|
||||
<text name="spu_guide_cancel" text="Annullér"/>
|
||||
<text name="spu_guide_saveOnly" text="Gem uden at starte"/>
|
||||
<text name="spu_guide_runAuto" text="Kør automatisk"/>
|
||||
<text name="spu_guide_stepOf" text="Trin %d af %d — %s"/>
|
||||
<text name="spu_guide_stepOne" text="Trin %d — %s"/>
|
||||
<text name="spu_guide_editStep" text="Ret: %s"/>
|
||||
<text name="spu_guide_editSuffix" text=" [Ret]"/>
|
||||
<text name="spu_guide_sum_hint" text="Tryk på en linje for at rette netop det."/>
|
||||
<text name="spu_guide_auto" text="Automatisk (anbefalet)"/>
|
||||
<text name="spu_guide_manual" text="Vælg selv"/>
|
||||
<text name="spu_guide_closed" text="Guiden er lukket — marken/flowet findes ikke længere"/>
|
||||
<text name="spu_guide_cannotAdvance" text="Vælg først"/>
|
||||
<text name="spu_guide_pickField" text="Vælg en mark først"/>
|
||||
<text name="spu_guide_saved" text="F%d gemt"/>
|
||||
<text name="spu_guide_noMarker" text="! Sæt en AD-markør ved F%d — arbejdet venter på markøren"/>
|
||||
<text name="spu_guide_field_open" text="Opsæt mark"/>
|
||||
<text name="spu_guide_fleet_open" text="Markflåde"/>
|
||||
<text name="spu_guide_fleet_close" text="Til marker"/>
|
||||
<text name="spu_guide_field_title" text="— Opsæt mark F%d —"/>
|
||||
<text name="spu_guide_field_titleNone" text="— Opsæt mark —"/>
|
||||
<text name="spu_guide_field_step_field" text="Mark"/>
|
||||
<text name="spu_guide_field_step_crop" text="Afgrøde"/>
|
||||
<text name="spu_guide_field_step_cropChoice" text="Afgrødens valg"/>
|
||||
<text name="spu_guide_field_step_prep" text="Klargøring"/>
|
||||
<text name="spu_guide_field_step_care" text="Pleje"/>
|
||||
<text name="spu_guide_field_step_machines" text="Maskiner"/>
|
||||
<text name="spu_guide_field_step_store" text="Lager"/>
|
||||
<text name="spu_guide_field_step_summary" text="Oversigt"/>
|
||||
<text name="spu_guide_field_hint_field" text="Vælg den mark du vil sætte op."/>
|
||||
<text name="spu_guide_field_hint_crop" text="Hvad skal gro på marken? 'Behold' kører videre med det der står nu."/>
|
||||
<text name="spu_guide_field_hint_cropChoice" text="Hvad skal der ske med afgrøden ved slåning eller høst?"/>
|
||||
<text name="spu_guide_field_hint_prep" text="Efter høst / på bar mark. Automatisk pløjer eller kultiverer og kalker efter behov."/>
|
||||
<text name="spu_guide_field_hint_care" text="Mens afgrøden gror. Der gødes/sprøjtes kun når marken trænger."/>
|
||||
<text name="spu_guide_field_hint_machines" text="Hvor mange ad gangen, og skal marken have sine egne faste maskiner?"/>
|
||||
<text name="spu_guide_field_hint_store" text="Hvor skal det høstede/slåede hen?"/>
|
||||
<text name="spu_guide_field_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||
<text name="spu_guide_crop" text="Afgrøde"/>
|
||||
<text name="spu_guide_crop_keep" text="Behold det der gror"/>
|
||||
<text name="spu_guide_crop_sowAgain" text="Så samme afgrøde igen efter høst"/>
|
||||
<text name="spu_guide_prep" text="Klargøring"/>
|
||||
<text name="spu_guide_prep_autoText" text=" Pløjer når jorden trænger, ellers kultiverer; kalker ved lav pH."/>
|
||||
<text name="spu_guide_care" text="Pleje"/>
|
||||
<text name="spu_guide_care_autoText" text=" Gøder op til max når afgrøden vokser, sprøjter/harver ukrudt væk og kalker ved lav pH."/>
|
||||
<text name="spu_guide_rigs" text="Maskiner ad gangen (multi-tool)"/>
|
||||
<text name="spu_guide_dedicated" text="Faste maskiner til marken?"/>
|
||||
<text name="spu_guide_useFieldSilo" text="Brug marksiloen?"/>
|
||||
<text name="spu_guide_store" text="Lager"/>
|
||||
<text name="spu_guide_store_autoText" text=" Nærmeste silo med plads (ensilage: gæringssilo)."/>
|
||||
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||
<text name="spu_guide_sum_sowAgain" text="så igen"/>
|
||||
<text name="spu_guide_sum_rake" text="river"/>
|
||||
<text name="spu_guide_sum_noRake" text="uden river"/>
|
||||
<text name="spu_guide_sum_straw_chop" text="halm hakkes"/>
|
||||
<text name="spu_guide_sum_straw_collect" text="halm samles"/>
|
||||
<text name="spu_guide_sum_straw_bale" text="halm presses"/>
|
||||
<text name="spu_guide_sum_auto" text="Automatisk"/>
|
||||
<text name="spu_guide_sum_lime" text="kalk"/>
|
||||
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||
<text name="spu_guide_sum_roll" text="tromle"/>
|
||||
<text name="spu_guide_sum_fert" text="gødning"/>
|
||||
<text name="spu_guide_sum_weed" text="ukrudt"/>
|
||||
<text name="spu_guide_sum_noCare" text="ingen pleje"/>
|
||||
<text name="spu_guide_sum_dedicated" text="%d faste"/>
|
||||
<text name="spu_guide_sum_freeRigs" text="ledige maskiner"/>
|
||||
<text name="spu_guide_sum_rigs" text="%d ad gangen"/>
|
||||
<text name="spu_guide_sum_unloaders" text="%d aflæsningsvogne"/>
|
||||
<text name="spu_guide_sum_fieldSilo" text="Marksilo"/>
|
||||
<text name="spu_guide_sum_stores" text="%d valgte siloer"/>
|
||||
<text name="spu_guide_flow_open" text="Opsæt flow"/>
|
||||
<text name="spu_guide_flow_title" text="— Opsæt flow: %s —"/>
|
||||
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||
<text name="spu_guide_flow_saved" text="Flow '%s' gemt"/>
|
||||
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||
<text name="spu_guide_flow_rename" text="Skift navn …"/>
|
||||
<text name="spu_guide_flow_step_name" text="Navn"/>
|
||||
<text name="spu_guide_flow_step_direction" text="Retning"/>
|
||||
<text name="spu_guide_flow_step_from" text="Fra"/>
|
||||
<text name="spu_guide_flow_step_to" text="Til"/>
|
||||
<text name="spu_guide_flow_step_goods" text="Varer"/>
|
||||
<text name="spu_guide_flow_step_store" text="Lager"/>
|
||||
<text name="spu_guide_flow_step_rules" text="Regler"/>
|
||||
<text name="spu_guide_flow_step_wait" text="Vent ved"/>
|
||||
<text name="spu_guide_flow_step_tractors" text="Traktorer"/>
|
||||
<text name="spu_guide_flow_step_summary" text="Oversigt"/>
|
||||
<text name="spu_guide_flow_hint_name" text="Flowets navn (traktorerne får navn efter det)."/>
|
||||
<text name="spu_guide_flow_hint_direction" text="Ind til bygning: fyld stalde/fabrikker. Ud fra bygning: tøm, lagr og sælg."/>
|
||||
<text name="spu_guide_flow_hint_buildings_in" text="Hvilke stalde/fabrikker skal fyldes? Markøren findes automatisk."/>
|
||||
<text name="spu_guide_flow_hint_buildings_out" text="Hvor skal der hentes fra? Markøren findes automatisk."/>
|
||||
<text name="spu_guide_flow_hint_goods_in" text="Automatisk henter hver tur den vare målet mangler mest."/>
|
||||
<text name="spu_guide_flow_hint_goods_out" text="For hver vare: Lager, Salg, eller Salg ellers lager."/>
|
||||
<text name="spu_guide_flow_hint_store" text="Hvor skal lagervarerne hen?"/>
|
||||
<text name="spu_guide_flow_hint_rules_in" text="Antal ture, og hvornår traktoren kører hjem fra Wait."/>
|
||||
<text name="spu_guide_flow_hint_rules_out" text="Hvornår der sælges, og hvornår der køres til lager."/>
|
||||
<text name="spu_guide_flow_hint_wait" text="Hvor venter traktoren med et rest-læs?"/>
|
||||
<text name="spu_guide_flow_hint_tractors" text="Hvilke traktorer kører flowet? Traktorer i andre flows vises ikke."/>
|
||||
<text name="spu_guide_flow_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||
<text name="spu_guide_flow_goods" text="Varer"/>
|
||||
<text name="spu_guide_flow_goodsAutoChoice" text="Automatisk (det bygningen mangler)"/>
|
||||
<text name="spu_guide_flow_goodsAutoText" text=" Hver tur hentes den vare målet mangler mest."/>
|
||||
<text name="spu_guide_flow_noGoodsTo" text=" Vælg først hvor der skal køres til (trinnet Til)"/>
|
||||
<text name="spu_guide_flow_noProducts" text=" Vælg først hvor der hentes fra (trinnet Fra)"/>
|
||||
<text name="spu_guide_flow_storeAutoChoice" text="Automatisk (nærmeste med plads)"/>
|
||||
<text name="spu_guide_flow_storeAutoText" text=" Lagerturene kører til nærmeste lager med plads."/>
|
||||
<text name="spu_guide_flow_noTractors" text=" Ingen ledige traktorer"/>
|
||||
<text name="spu_guide_flow_otherFlows" text=" %d traktorer kører i andre flows og vises ikke"/>
|
||||
<text name="spu_guide_flow_blockedShow" text=" %d traktorer kan ikke bruges her (vognen passer ikke) — klik for at se hvorfor"/>
|
||||
<text name="spu_guide_flow_blockedHide" text=" %d traktorer kan ikke bruges her — klik for at skjule"/>
|
||||
<text name="spu_guide_flow_blocked" text=" %d traktorer kan ikke bruges her (vognen passer ikke)"/>
|
||||
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
||||
<text name="spu_guide_flow_sumRound" text="samlerunde"/>
|
||||
<text name="spu_guide_flow_sumLoops" text="%d ture"/>
|
||||
<text name="spu_guide_flow_sumLoopsInf" text="uendelige ture"/>
|
||||
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
|
||||
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
|
||||
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
|
||||
<text name="spu_tab_fieldsilos" text="Marksiloer"/>
|
||||
<text name="spu_fs_enabled" text="Hent marksiloer hjem automatisk"/>
|
||||
<text name="spu_fs_maxVehicles" text="Maks. vogne ude"/>
|
||||
<text name="spu_fs_minLiters" text="Mindste mængde pr. vare"/>
|
||||
<text name="spu_fs_status" text="%d/%d vogne ude · %d siloer venter"/>
|
||||
<text name="spu_fs_noStores" text="Vælg mindst ét gårdlager nedenfor — ellers køres intet hjem"/>
|
||||
<text name="spu_fs_silosTitle" text="Marksiloer med varer"/>
|
||||
<text name="spu_fs_noVehicle" text="Ingen ledig tømmevogn: %d uden AD-parkeringspunkt, %d optaget, %d med spiller i, %d ikke i flåden (vogne i flows lånes ikke)"/>
|
||||
<text name="spu_fs_nothing" text="Ingen marksiloer har varer over mindste mængde"/>
|
||||
<text name="spu_fs_col_silo" text="Silo"/>
|
||||
<text name="spu_fs_col_liters" text="Liter"/>
|
||||
<text name="spu_fs_col_onWay" text="På vej"/>
|
||||
<text name="spu_fs_col_state" text="Status"/>
|
||||
<text name="spu_fs_state_ok" text="ok"/>
|
||||
<text name="spu_fs_state_noMarker" text="mangler markør"/>
|
||||
<text name="spu_fs_state_noFarmStore" text="ingen gård tager %s"/>
|
||||
<text name="spu_fs_state_paused" text="pause (kom ikke i gang)"/>
|
||||
<text name="spu_fs_storesTitle" text="Gårdlagre (hvor varerne køres hjem til)"/>
|
||||
<text name="spu_fs_toProduction" text="Kør overskud til produktion (foder til staldene først)"/>
|
||||
<text name="spu_fs_reserveDays" text="Foderreserve: staldenes forbrug i"/>
|
||||
<text name="spu_fs_days" text="%d døgn"/>
|
||||
<text name="spu_fs_toFactory" text="→ fabrik"/>
|
||||
<text name="spu_fs_harvestHeld" text="%d tømmevogn(e) holdes tilbage til høstere der arbejder"/>
|
||||
<text name="spu_view_title" text="Vis"/>
|
||||
<text name="spu_view_items" text="Varer"/>
|
||||
<text name="spu_view_buildings" text="Bygninger"/>
|
||||
<text name="spu_view_tooltip" text="Varer: samlet beholdning pr. vare (klik for at se hvor den ligger). Bygninger: tabel for én bygning."/>
|
||||
<text name="spu_kpi_value" text="Lagerværdi nu"/>
|
||||
<text name="spu_kpi_gain" text="Ekstra ved toppris"/>
|
||||
<text name="spu_kpi_empty" text="Løber tør snart"/>
|
||||
<text name="spu_kpi_full" text="Næsten fuldt"/>
|
||||
<text name="spu_attention" text="— Kræver handling —"/>
|
||||
<text name="spu_attention_none" text="Intet kræver handling lige nu"/>
|
||||
<text name="spu_attention_hintFlow" text=" → klik: opret flow"/>
|
||||
<text name="spu_attention_hintOpen" text=" → klik: åbn flow '%s'"/>
|
||||
<text name="spu_attention_hintProduce" text=" → producér/køb"/>
|
||||
<text name="spu_attention_produce" text="Et flow hjælper ikke — gården har kun %2$s %1$s i alt. Producér eller køb mere."/>
|
||||
<text name="spu_attention_flowName" text="%s til %s"/>
|
||||
<text name="spu_attention_hintSellFlow" text=" → klik: opret salgsflow"/>
|
||||
<text name="spu_attention_sellFlowName" text="Sælg %s"/>
|
||||
<text name="spu_alert_empty" text="%s løber tør om %s — gården har %s, bruger %s/døgn"/>
|
||||
<text name="spu_alert_fullIn" text="%s er %d %% fuld — fuld om %s"/>
|
||||
<text name="spu_alert_full" text="%s er %d %% fuld"/>
|
||||
<text name="spu_alert_sell" text="Sælg %s nu: prisen er %d %% af årets top — %s"/>
|
||||
<text name="spu_items_hint" text="(klik på en vare)"/>
|
||||
<text name="spu_icol_amount" text="Beholdning"/>
|
||||
<text name="spu_icol_value" text="Værdi nu"/>
|
||||
<text name="spu_icol_rate" text="Gården netto · rækker"/>
|
||||
<text name="spu_item_perDay" text="%s/døgn"/>
|
||||
<text name="spu_item_fullIn" text="fuld om %s"/>
|
||||
<text name="spu_item_balanced" text="i balance"/>
|
||||
<text name="spu_item_share" text="%d %% af varen"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<text name="spu_title" text="Smart Pickup"/>
|
||||
<text name="spu_run" text="Run"/>
|
||||
<text name="spu_run_tooltip" text="Select the run to view or edit."/>
|
||||
<text name="spu_noRuns" text="No runs — press 'New run'"/>
|
||||
<text name="spu_noRuns" text="No flows — press 'New flow'"/>
|
||||
<text name="spu_target" text="Target"/>
|
||||
<text name="spu_target_tooltip" text="AutoDrive marker at the barn or factory to keep filled."/>
|
||||
<text name="spu_noTarget" text="(choose target)"/>
|
||||
@@ -27,7 +27,10 @@
|
||||
<text name="spu_status_started" text="Started %d, skipped %d"/>
|
||||
<text name="spu_status_noAd" text="AutoDrive API not found"/>
|
||||
<text name="spu_status_error" text="Error: %s"/>
|
||||
<text name="spu_reason_noTrailer" text="no trailer"/>
|
||||
<text name="spu_guide_flow_wagon" text=" ↳ Trailer"/>
|
||||
<text name="spu_guide_flow_noFreeWagon" text=" ↳ no free trailer (detached, with a Connect marker, able to carry the goods)"/>
|
||||
<text name="spu_guide_flow_soloText" text="%s — no trailer attached: fetches a free trailer"/>
|
||||
<text name="spu_reason_noTrailer" text="no trailer (attach one, or name the trailer's parking after the flow)"/>
|
||||
<text name="spu_reason_cannotCarry" text="trailer cannot carry the run's goods"/>
|
||||
<text name="spu_reason_farFromNetwork" text="too far from the road network"/>
|
||||
<text name="spu_reason_noTarget" text="no target"/>
|
||||
@@ -40,7 +43,28 @@
|
||||
<text name="spu_status_stoppedAll" text="Stopped %d tractors"/>
|
||||
<text name="spu_overview_hint" text="Click a flow to edit it"/>
|
||||
<text name="spu_tab_fields" text="Fields"/>
|
||||
<text name="spu_tab_month" text="Month"/>
|
||||
<text name="spu_month_months" text="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"/>
|
||||
<text name="spu_month_period" text="Period %d"/>
|
||||
<text name="spu_month_header" text="%s · day %d of %d · %02d:%02d"/>
|
||||
<text name="spu_month_noClock" text="Cannot read the game calendar"/>
|
||||
<text name="spu_month_verdictCalm" text="Nothing urgent — safe to sleep to next month"/>
|
||||
<text name="spu_month_verdictUrgent" text="NOTE: %d fields must finish before the month ends"/>
|
||||
<text name="spu_month_noWork" text="No fields have work in progress"/>
|
||||
<text name="spu_month_col_work" text="Work"/>
|
||||
<text name="spu_month_col_progress" text="Done"/>
|
||||
<text name="spu_month_col_next" text="Next"/>
|
||||
<text name="spu_month_col_window" text="Window"/>
|
||||
<text name="spu_month_window_ok" text="ok"/>
|
||||
<text name="spu_month_window_harvestCloses" text="NOTE: withers soon"/>
|
||||
<text name="spu_month_window_lastSowMonth" text="NOTE: last sow month"/>
|
||||
<text name="spu_month_upcoming" text="Can be sown now, not started:"/>
|
||||
<text name="spu_month_upcomingOpen" text="F%d: %s — can also be sown next month"/>
|
||||
<text name="spu_month_upcomingLast" text="F%d: %s — NOTE: last sowing month"/>
|
||||
<text name="spu_field_button_start" text="Start now"/>
|
||||
<text name="spu_field_manualTransport" text=" %s drives itself on the road (No = too wide, you drive it to the field)"/>
|
||||
<text name="spu_field_button_takeover" text="Start now"/>
|
||||
<text name="spu_field_takeoverRequested" text="F%d: taking over the harvester now if the header fits"/>
|
||||
<text name="spu_field_button_stop" text="Stop field"/>
|
||||
<text name="spu_field_noMarker" text="! missing marker F%d"/>
|
||||
<text name="spu_field_stage_idle" text="Waiting"/>
|
||||
@@ -51,8 +75,45 @@
|
||||
<text name="spu_field_stage_collecting" text="Collecting"/>
|
||||
<text name="spu_field_stage_done" text="Done"/>
|
||||
<text name="spu_field_reason_noRig" text="no %s ticked in the field fleet"/>
|
||||
<text name="spu_field_reasonShort_noRig" text="no machine"/>
|
||||
<text name="spu_field_reasonShort_allBusy" text="waiting for machine"/>
|
||||
<text name="spu_field_reasonShort_tooWide" text="too wide"/>
|
||||
<text name="spu_field_reasonShort_widthUnknown" text="width unknown"/>
|
||||
<text name="spu_field_reasonShort_tooNarrow" text="too narrow"/>
|
||||
<text name="spu_field_reasonShort_cannotDo" text="none can do it"/>
|
||||
<text name="spu_field_reasonShort_noSowFruit" text="choose crop"/>
|
||||
<text name="spu_field_reasonShort_notEnough" text="too few free"/>
|
||||
<text name="spu_field_reasonShort_notPlantable" text="not sowing time"/>
|
||||
<text name="spu_field_reasonShort_regrowing" text="waiting for regrowth"/>
|
||||
<text name="spu_field_reasonShort_rain" text="waiting for dry weather"/>
|
||||
<text name="spu_field_reasonShort_text" text="see details"/>
|
||||
<text name="spu_field_reason_allBusy" text="waiting for a free %s"/>
|
||||
<text name="spu_pool_section_tractors" text="Machine pool — tractors (fetch implements when no ready combination is free)"/>
|
||||
<text name="spu_pool_section_implements" text="Machine pool — implements (park "X" + marker "Connect X")"/>
|
||||
<text name="spu_pool_noTractors" text="no tractors without implement"/>
|
||||
<text name="spu_pool_noImplements" text="no implements with a field role"/>
|
||||
<text name="spu_pool_tractor_free" text="free%.0s"/>
|
||||
<text name="spu_pool_tractor_noPark" text="needs its own AD park destination%.0s"/>
|
||||
<text name="spu_pool_tractor_player" text="player is driving%.0s"/>
|
||||
<text name="spu_pool_tractor_returning" text="dropping implement and driving home%.0s"/>
|
||||
<text name="spu_pool_tractor_job" text="job F%s"/>
|
||||
<text name="spu_pool_tractor_busy" text="AutoDrive/Courseplay active%.0s"/>
|
||||
<text name="spu_pool_tractor_notInFleet" text="not in field fleet (tick)%.0s"/>
|
||||
<text name="spu_pool_tractor_offNetwork" text="off the AutoDrive network%.0s"/>
|
||||
<text name="spu_pool_tractor_hitched" text="has an implement attached (detach it to use the pool)%.0s"/>
|
||||
<text name="spu_pool_implement_flow" text="belongs to flow '%s'"/>
|
||||
<text name="spu_pool_implement_parked" text="parked at %s"/>
|
||||
<text name="spu_pool_implement_noPark" text="not in pool — needs an AD park destination%.0s"/>
|
||||
<text name="spu_pool_implement_noConnect" text="not in pool — needs marker "Connect %s""/>
|
||||
<text name="spu_pool_implement_attached" text="on %s"/>
|
||||
<text name="spu_pool_implement_quarantine" text="could not attach — retry in %s min"/>
|
||||
<text name="spu_pool_implement_reserved" text="reserved — a tractor is on its way%.0s"/>
|
||||
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
|
||||
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s too wide: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_widthUnknown" text="%s: work width %.1f m cannot be right — set the width in Courseplay (not sent out)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m — turn on Rake or use a wider one"/>
|
||||
<text name="spu_field_role_mower" text="mower"/>
|
||||
<text name="spu_field_role_tedder" text="tedder"/>
|
||||
@@ -65,25 +126,33 @@
|
||||
<text name="spu_field_mode_manual" text="Manual"/>
|
||||
<text name="spu_field_notMowable" text="not grass/clover"/>
|
||||
<text name="spu_field_scanning" text="Loading fields ... (one field per second)"/>
|
||||
<text name="spu_field_hint" text="Click a field to select it. Tick the machines in the field fleet at the bottom."/>
|
||||
<text name="spu_field_hint" text="Click a field to see and change its setup."/>
|
||||
<text name="spu_field_fleetEmpty" text="! The field fleet is set to "only ticked", but no machine is ticked — nothing can be sent out. Open Field fleet and choose "Use all free machines"."/>
|
||||
<text name="spu_field_showAll" text="Show all fields (%d hidden)"/>
|
||||
<text name="spu_col_field" text="Field"/>
|
||||
<text name="spu_col_crop" text="Crop"/>
|
||||
<text name="spu_col_plan" text="Plan"/>
|
||||
<text name="spu_col_mode" text="Mode"/>
|
||||
<text name="spu_field_section" text="— Field F%d: %s —"/>
|
||||
<text name="spu_field_statusFull" text="Status: %s"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Mode"/>
|
||||
<text name="spu_field_rake" text="Rake the swaths before collecting (lets narrow loading wagons collect)"/>
|
||||
<text name="spu_field_marker" text=" Marker %s, %.0f m from the field"/>
|
||||
<text name="spu_field_mowWidth" text=" Mowed at %.1f m — collecting must be at least as wide"/>
|
||||
<text name="spu_field_section_store" text="— Force storage (else nearest silo with room) —"/>
|
||||
<text name="spu_field_section_dedicated" text="— Dedicated machines for this field (used only here; none = the free ones) —"/>
|
||||
<text name="spu_field_dedicatedElsewhere" text="dedicated to F%s"/>
|
||||
<text name="spu_field_hiddenDedicated" text="%d machines are dedicated to other fields (%s) and not shown here"/>
|
||||
<text name="spu_field_fieldSilo" text="field silo"/>
|
||||
<text name="spu_field_storeHidden" text="+ %d silos further away not shown"/>
|
||||
<text name="spu_field_noStore" text="no silo takes this field's goods"/>
|
||||
<text name="spu_field_storeAuto" text="none chosen: nearest silo with room is used"/>
|
||||
<text name="spu_field_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
|
||||
<text name="spu_field_section_fleet" text="— Field fleet (ticked = may be sent out) —"/>
|
||||
<text name="spu_field_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
||||
<text name="spu_field_rig_working" text="working on F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
||||
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d starts when a machine is free"/>
|
||||
@@ -94,6 +163,7 @@
|
||||
<text name="spu_field_fermenter" text="fermenting silo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="none chosen: nearest fermenting silo with room, else nearest silo"/>
|
||||
<text name="spu_field_bare" text="bare soil"/>
|
||||
<text name="spu_field_stubble" text="stubble (%s)"/>
|
||||
<text name="spu_field_careNeed" text="needs %s"/>
|
||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
||||
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
||||
@@ -116,7 +186,7 @@
|
||||
<text name="spu_field_rig_seeds" text="seeds ok"/>
|
||||
<text name="spu_field_rigs" text="Machines per step (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="cultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="fertilizer spreader"/>
|
||||
<text name="spu_field_role_fertilizer" text="fertilizing"/>
|
||||
<text name="spu_field_role_harvester" text="harvester"/>
|
||||
<text name="spu_field_role_limer" text="lime spreader"/>
|
||||
<text name="spu_field_role_mulcher" text="mulcher"/>
|
||||
@@ -152,14 +222,53 @@
|
||||
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
||||
<text name="spu_field_weed" text="Control weeds"/>
|
||||
<text name="spu_field_notice_done" text="is done"/>
|
||||
<text name="spu_field_reason_regrowing" text="waiting for the regrowth to reach full height"/>
|
||||
<text name="spu_field_reason_rain" text="waiting for dry weather"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="New run"/>
|
||||
<text name="spu_field_target" text="Crop on the field (the mod handles the rest)"/>
|
||||
<text name="spu_field_target_none" text="None — plan it yourself"/>
|
||||
<text name="spu_field_target_bare" text="Prepared and sown with %s as soon as it is sowing time"/>
|
||||
<text name="spu_field_target_keep" text="%s is already there: mowed and collected every time it is ready"/>
|
||||
<text name="spu_field_target_switch" text="%s is left to finish growing and is harvested/mowed; then the field is prepared and sown with %s"/>
|
||||
<text name="spu_field_waitSowing" text="waiting for sowing time for %s"/>
|
||||
<text name="spu_field_fleetAll" text="Use all free machines (No = only ticked ones)"/>
|
||||
<text name="spu_field_role_baler" text="baler"/>
|
||||
<text name="spu_field_role_baleCollector" text="bale collector"/>
|
||||
<text name="spu_field_stage_strawCollecting" text="Collecting straw"/>
|
||||
<text name="spu_field_stage_baling" text="Baling"/>
|
||||
<text name="spu_field_stage_baleCollecting" text="Collecting bales"/>
|
||||
<text name="spu_field_straw" text="Straw after harvest"/>
|
||||
<text name="spu_field_straw_chop" text="Chopped (spread on the field)"/>
|
||||
<text name="spu_field_straw_collect" text="Collected to a silo"/>
|
||||
<text name="spu_field_straw_bale" text="Baled and collected"/>
|
||||
<text name="spu_button_start" text="Start flow"/>
|
||||
<text name="spu_button_stop" text="Stop flow"/>
|
||||
<text name="spu_button_new" text="New flow"/>
|
||||
<text name="spu_button_renameBuilding" text="Rename building"/>
|
||||
<text name="spu_renameBuildingTitle" text="New name for the building (empty = shop name)"/>
|
||||
<text name="spu_button_stopPark" text="Stop all and park"/>
|
||||
<text name="spu_status_queuedAll" text="Start all: %d tractors start one at a time (every %d seconds), %d flows already running, %d skipped"/>
|
||||
<text name="spu_status_stopParked" text="Stop all: %d stopped, %d driving to parking — paused until Start all"/>
|
||||
<text name="spu_status_noPark" text="no park destination: %s"/>
|
||||
<text name="spu_status_fetchManual" text="fetch yourself (too wide): %s"/>
|
||||
<text name="spu_status_starting" text="Starting tractors: %d of %d …"/>
|
||||
<text name="spu_status_paused" text="Paused — press Start all to continue"/>
|
||||
<text name="spu_flow_pickOther" text="Pick another flow (the board with all flows is in Overview)"/>
|
||||
<text name="spu_button_rename" text="Rename"/>
|
||||
<text name="spu_button_delete" text="Delete run"/>
|
||||
<text name="spu_newRunName" text="Run %d"/>
|
||||
<text name="spu_renameTitle" text="Run name"/>
|
||||
<text name="spu_button_delete" text="Delete flow"/>
|
||||
<text name="spu_panel_close" text="Close"/>
|
||||
<text name="spu_panel_gone" text="That item no longer exists — window closed"/>
|
||||
<text name="spu_flowList_hint" text="Click a flow to edit it. [Start]/[Stop] on the far right controls it directly. Colours: green runs · yellow waits · red problem · grey stopped. "By need" = each trip fetches what the target is lowest on."/>
|
||||
<text name="spu_flowList_start" text="[Start]"/>
|
||||
<text name="spu_flowList_stop" text="[Stop]"/>
|
||||
<text name="spu_flowList_status" text="Flows running: %d of %d"/>
|
||||
<text name="spu_panel_flowTitle" text="Edit flow: %s"/>
|
||||
<text name="spu_panel_fieldTitle" text="Field F%d"/>
|
||||
<text name="spu_field_pausedSuffix" text="(paused)"/>
|
||||
<text name="spu_field_pausedBanner" text="! Field work is paused since "Stop all" (saved with the savegame) — press Start all in Overview, or [Start] on a single field."/>
|
||||
<text name="spu_prune_removed" text="Flow '%s': %d tractor(s) no longer exist and were removed (%d left)"/>
|
||||
<text name="spu_prune_empty" text="Flow '%s' has no tractors left and won't run — assign one in Flows"/>
|
||||
<text name="spu_newRunName" text="Flow %d"/>
|
||||
<text name="spu_renameTitle" text="Flow name"/>
|
||||
<text name="spu_deleteConfirm" text="Delete run '%s'? Its running tractors will be stopped. This cannot be undone."/>
|
||||
<text name="spu_capacity" text="%s l"/>
|
||||
<text name="spu_capacity_pallets" text="autoload (pallets)"/>
|
||||
@@ -203,6 +312,8 @@
|
||||
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
|
||||
<text name="spu_storeAbove" text="Drive to storage above"/>
|
||||
<text name="spu_sellAbove" text="Always sell when source is above"/>
|
||||
<text name="spu_sellAbove_off" text="Off"/>
|
||||
<text name="spu_storeAbove_tooltip" text="Goods with the Store policy are driven to a silo when the source storage is above this fill level (or a full load is available)."/>
|
||||
<text name="spu_collectRound" text="Collect in one trip"/>
|
||||
<text name="spu_collectRound_tooltip" text="Tipper: same good from several sources until full. Autoload trailer: several pallet goods with the same destination in one trip."/>
|
||||
@@ -212,6 +323,7 @@
|
||||
<text name="spu_reason_noProducts" text="no good selected (set Store/Sell on a good above)"/>
|
||||
<text name="spu_reason_noWaitPoint" text="no Wait point in the pool (marker in a folder with 'Wait' in its name)"/>
|
||||
<text name="spu_reason_cargoNoTarget" text="the load on the trailer has no destination (price too low / no silo) — empty the trailer"/>
|
||||
<text name="spu_reason_autoloadOn" text="AutoLoad is on for this trailer — AutoDrive treats it as a pallet/bale trailer only (turn AutoLoad off on the trailer)"/>
|
||||
<text name="spu_reason_needsAutoload" text="pallet goods need Universal Autoload on the trailer"/>
|
||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||
<text name="spu_price_pct" text="%d %% of top (%s)"/>
|
||||
@@ -237,14 +349,16 @@
|
||||
<text name="spu_flow_noSource" text="(no source)"/>
|
||||
<text name="spu_flow_noTarget" text="(no target)"/>
|
||||
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
||||
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
||||
<text name="spu_flow_round" text="round"/>
|
||||
<text name="spu_flow_ruleIn" text="need"/>
|
||||
<text name="spu_flow_ruleInLoops" text="need, %d loops"/>
|
||||
<text name="spu_flow_ruleIn" text="by need — lowest first"/>
|
||||
<text name="spu_flow_ruleInLoops" text="by need, %d trips"/>
|
||||
<text name="spu_markerAt" text="%s %d m"/>
|
||||
<text name="spu_noMarker" text="NO MARKER within 40 m — place an AD marker at the trigger"/>
|
||||
<text name="spu_noBuildings" text="No buildings found"/>
|
||||
<text name="spu_buildingGone" text="building no longer exists"/>
|
||||
<text name="spu_buildingGone" text="Removed building (no longer exists) — set NO"/>
|
||||
<text name="spu_reason_noMarker" text="'%s' has no AD marker at its trigger"/>
|
||||
<text name="spu_storage_select" text="Show building"/>
|
||||
<text name="spu_stat_now" text="now %s"/>
|
||||
@@ -284,5 +398,181 @@
|
||||
<text name="spu_col_lasts" text="Lasts"/>
|
||||
<text name="spu_col_rate" text="Use/prod. per day"/>
|
||||
<text name="spu_storage_select_tooltip" text="Choose the building shown in the table."/>
|
||||
<text name="spu_guide_next" text="Next"/>
|
||||
<text name="spu_guide_save" text="Save"/>
|
||||
<text name="spu_guide_ok" text="OK"/>
|
||||
<text name="spu_guide_back" text="Back"/>
|
||||
<text name="spu_guide_cancel" text="Cancel"/>
|
||||
<text name="spu_guide_saveOnly" text="Save without starting"/>
|
||||
<text name="spu_guide_runAuto" text="Run automatically"/>
|
||||
<text name="spu_guide_stepOf" text="Step %d of %d — %s"/>
|
||||
<text name="spu_guide_stepOne" text="Step %d — %s"/>
|
||||
<text name="spu_guide_editStep" text="Edit: %s"/>
|
||||
<text name="spu_guide_editSuffix" text=" [Edit]"/>
|
||||
<text name="spu_guide_sum_hint" text="Click a line to change just that."/>
|
||||
<text name="spu_guide_auto" text="Automatic (recommended)"/>
|
||||
<text name="spu_guide_manual" text="Choose myself"/>
|
||||
<text name="spu_guide_closed" text="Guide closed — the field/flow no longer exists"/>
|
||||
<text name="spu_guide_cannotAdvance" text="Choose first"/>
|
||||
<text name="spu_guide_pickField" text="Pick a field first"/>
|
||||
<text name="spu_guide_saved" text="F%d saved"/>
|
||||
<text name="spu_guide_noMarker" text="! Place an AD marker at F%d — work waits for the marker"/>
|
||||
<text name="spu_guide_field_open" text="Set up field"/>
|
||||
<text name="spu_guide_fleet_open" text="Field fleet"/>
|
||||
<text name="spu_guide_fleet_close" text="Back to fields"/>
|
||||
<text name="spu_guide_field_title" text="— Set up field F%d —"/>
|
||||
<text name="spu_guide_field_titleNone" text="— Set up field —"/>
|
||||
<text name="spu_guide_field_step_field" text="Field"/>
|
||||
<text name="spu_guide_field_step_crop" text="Crop"/>
|
||||
<text name="spu_guide_field_step_cropChoice" text="Crop options"/>
|
||||
<text name="spu_guide_field_step_prep" text="Preparation"/>
|
||||
<text name="spu_guide_field_step_care" text="Crop care"/>
|
||||
<text name="spu_guide_field_step_machines" text="Machines"/>
|
||||
<text name="spu_guide_field_step_store" text="Storage"/>
|
||||
<text name="spu_guide_field_step_summary" text="Summary"/>
|
||||
<text name="spu_guide_field_hint_field" text="Pick the field you want to set up."/>
|
||||
<text name="spu_guide_field_hint_crop" text="What should grow here? 'Keep' carries on with the current crop."/>
|
||||
<text name="spu_guide_field_hint_cropChoice" text="What happens to the crop when mowing or harvesting?"/>
|
||||
<text name="spu_guide_field_hint_prep" text="After harvest / on bare land. Automatic plows or cultivates and limes as needed."/>
|
||||
<text name="spu_guide_field_hint_care" text="While the crop grows. Fertilizing/spraying only happens when needed."/>
|
||||
<text name="spu_guide_field_hint_machines" text="How many at a time, and should the field have its own dedicated machines?"/>
|
||||
<text name="spu_guide_field_hint_store" text="Where should the harvest go?"/>
|
||||
<text name="spu_guide_field_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||
<text name="spu_guide_crop" text="Crop"/>
|
||||
<text name="spu_guide_crop_keep" text="Keep what grows"/>
|
||||
<text name="spu_guide_crop_sowAgain" text="Sow the same crop again after harvest"/>
|
||||
<text name="spu_guide_prep" text="Preparation"/>
|
||||
<text name="spu_guide_prep_autoText" text=" Plows when the soil needs it, otherwise cultivates; limes at low pH."/>
|
||||
<text name="spu_guide_care" text="Crop care"/>
|
||||
<text name="spu_guide_care_autoText" text=" Fertilizes up to max while the crop grows, removes weeds and limes at low pH."/>
|
||||
<text name="spu_guide_rigs" text="Machines at a time (multi-tool)"/>
|
||||
<text name="spu_guide_dedicated" text="Dedicated machines for this field?"/>
|
||||
<text name="spu_guide_useFieldSilo" text="Use the field silo?"/>
|
||||
<text name="spu_guide_store" text="Storage"/>
|
||||
<text name="spu_guide_store_autoText" text=" Nearest silo with room (silage: fermenting silo)."/>
|
||||
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||
<text name="spu_guide_sum_sowAgain" text="sow again"/>
|
||||
<text name="spu_guide_sum_rake" text="rake"/>
|
||||
<text name="spu_guide_sum_noRake" text="no rake"/>
|
||||
<text name="spu_guide_sum_straw_chop" text="straw chopped"/>
|
||||
<text name="spu_guide_sum_straw_collect" text="straw collected"/>
|
||||
<text name="spu_guide_sum_straw_bale" text="straw baled"/>
|
||||
<text name="spu_guide_sum_auto" text="Automatic"/>
|
||||
<text name="spu_guide_sum_lime" text="lime"/>
|
||||
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||
<text name="spu_guide_sum_roll" text="roll"/>
|
||||
<text name="spu_guide_sum_fert" text="fertilizer"/>
|
||||
<text name="spu_guide_sum_weed" text="weeds"/>
|
||||
<text name="spu_guide_sum_noCare" text="no care"/>
|
||||
<text name="spu_guide_sum_dedicated" text="%d dedicated"/>
|
||||
<text name="spu_guide_sum_freeRigs" text="free machines"/>
|
||||
<text name="spu_guide_sum_rigs" text="%d at a time"/>
|
||||
<text name="spu_guide_sum_unloaders" text="%d unloaders"/>
|
||||
<text name="spu_guide_sum_fieldSilo" text="Field silo"/>
|
||||
<text name="spu_guide_sum_stores" text="%d chosen silos"/>
|
||||
<text name="spu_guide_flow_open" text="Set up flow"/>
|
||||
<text name="spu_guide_flow_title" text="— Set up flow: %s —"/>
|
||||
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||
<text name="spu_guide_flow_saved" text="Flow '%s' saved"/>
|
||||
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||
<text name="spu_guide_flow_rename" text="Change name …"/>
|
||||
<text name="spu_guide_flow_step_name" text="Name"/>
|
||||
<text name="spu_guide_flow_step_direction" text="Direction"/>
|
||||
<text name="spu_guide_flow_step_from" text="From"/>
|
||||
<text name="spu_guide_flow_step_to" text="To"/>
|
||||
<text name="spu_guide_flow_step_goods" text="Goods"/>
|
||||
<text name="spu_guide_flow_step_store" text="Storage"/>
|
||||
<text name="spu_guide_flow_step_rules" text="Rules"/>
|
||||
<text name="spu_guide_flow_step_wait" text="Wait at"/>
|
||||
<text name="spu_guide_flow_step_tractors" text="Tractors"/>
|
||||
<text name="spu_guide_flow_step_summary" text="Summary"/>
|
||||
<text name="spu_guide_flow_hint_name" text="The flow's name (tractors are named after it)."/>
|
||||
<text name="spu_guide_flow_hint_direction" text="Into building: keep barns/factories filled. Out of building: empty, store and sell."/>
|
||||
<text name="spu_guide_flow_hint_buildings_in" text="Which barns/factories should be filled? The marker is found automatically."/>
|
||||
<text name="spu_guide_flow_hint_buildings_out" text="Where should goods be picked up? The marker is found automatically."/>
|
||||
<text name="spu_guide_flow_hint_goods_in" text="Automatic fetches whatever the target lacks most on each trip."/>
|
||||
<text name="spu_guide_flow_hint_goods_out" text="For each good: Store, Sell, or Sell else store."/>
|
||||
<text name="spu_guide_flow_hint_store" text="Where should stored goods go?"/>
|
||||
<text name="spu_guide_flow_hint_rules_in" text="Number of trips, and when the tractor leaves Wait."/>
|
||||
<text name="spu_guide_flow_hint_rules_out" text="When to sell, and when to drive to storage."/>
|
||||
<text name="spu_guide_flow_hint_wait" text="Where does the tractor wait with a leftover load?"/>
|
||||
<text name="spu_guide_flow_hint_tractors" text="Which tractors run this flow? Tractors in other flows are hidden."/>
|
||||
<text name="spu_guide_flow_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||
<text name="spu_guide_flow_goods" text="Goods"/>
|
||||
<text name="spu_guide_flow_goodsAutoChoice" text="Automatic (what the building needs)"/>
|
||||
<text name="spu_guide_flow_goodsAutoText" text=" Each trip fetches the good the target lacks most."/>
|
||||
<text name="spu_guide_flow_noGoodsTo" text=" First choose where to deliver (step To)"/>
|
||||
<text name="spu_guide_flow_noProducts" text=" First choose where to pick up (step From)"/>
|
||||
<text name="spu_guide_flow_storeAutoChoice" text="Automatic (nearest with room)"/>
|
||||
<text name="spu_guide_flow_storeAutoText" text=" Storage trips go to the nearest store with room."/>
|
||||
<text name="spu_guide_flow_noTractors" text=" No free tractors"/>
|
||||
<text name="spu_guide_flow_otherFlows" text=" %d tractors run in other flows and are hidden"/>
|
||||
<text name="spu_guide_flow_blockedShow" text=" %d tractors cannot be used here (trailer does not fit) — click to see why"/>
|
||||
<text name="spu_guide_flow_blockedHide" text=" %d tractors cannot be used here — click to hide"/>
|
||||
<text name="spu_guide_flow_blocked" text=" %d tractors cannot be used here (trailer does not fit)"/>
|
||||
<text name="spu_guide_flow_none" text="none chosen"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
||||
<text name="spu_guide_flow_sumRound" text="collection round"/>
|
||||
<text name="spu_guide_flow_sumLoops" text="%d trips"/>
|
||||
<text name="spu_guide_flow_sumLoopsInf" text="endless trips"/>
|
||||
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
|
||||
<text name="spu_guide_flow_sumNoTractors" text="none"/>
|
||||
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
|
||||
<text name="spu_tab_fieldsilos" text="Field silos"/>
|
||||
<text name="spu_fs_enabled" text="Bring field silos home automatically"/>
|
||||
<text name="spu_fs_maxVehicles" text="Max. vehicles out"/>
|
||||
<text name="spu_fs_minLiters" text="Minimum amount per product"/>
|
||||
<text name="spu_fs_status" text="%d/%d vehicles out · %d silos waiting"/>
|
||||
<text name="spu_fs_noStores" text="Select at least one farm storage below — otherwise nothing is brought home"/>
|
||||
<text name="spu_fs_silosTitle" text="Field silos with products"/>
|
||||
<text name="spu_fs_noVehicle" text="No free unloading trailer: %d without AD park destination, %d busy, %d with a player in it, %d not in the fleet (flow vehicles are never borrowed)"/>
|
||||
<text name="spu_fs_nothing" text="No field silo has products above the minimum amount"/>
|
||||
<text name="spu_fs_col_silo" text="Silo"/>
|
||||
<text name="spu_fs_col_liters" text="Liters"/>
|
||||
<text name="spu_fs_col_onWay" text="En route"/>
|
||||
<text name="spu_fs_col_state" text="Status"/>
|
||||
<text name="spu_fs_state_ok" text="ok"/>
|
||||
<text name="spu_fs_state_noMarker" text="missing marker"/>
|
||||
<text name="spu_fs_state_noFarmStore" text="no farm storage takes %s"/>
|
||||
<text name="spu_fs_state_paused" text="paused (did not get going)"/>
|
||||
<text name="spu_fs_storesTitle" text="Farm storages (where products are brought home to)"/>
|
||||
<text name="spu_fs_toProduction" text="Send surplus to production (barn feed first)"/>
|
||||
<text name="spu_fs_reserveDays" text="Feed reserve: barn consumption for"/>
|
||||
<text name="spu_fs_days" text="%d days"/>
|
||||
<text name="spu_fs_toFactory" text="→ factory"/>
|
||||
<text name="spu_fs_harvestHeld" text="%d unloading trailer(s) held back for working harvesters"/>
|
||||
<text name="spu_view_title" text="Show"/>
|
||||
<text name="spu_view_items" text="Goods"/>
|
||||
<text name="spu_view_buildings" text="Buildings"/>
|
||||
<text name="spu_view_tooltip" text="Goods: total stock per good (click to see where it is stored). Buildings: table for one building."/>
|
||||
<text name="spu_kpi_value" text="Stock value now"/>
|
||||
<text name="spu_kpi_gain" text="Extra at top price"/>
|
||||
<text name="spu_kpi_empty" text="Running out soon"/>
|
||||
<text name="spu_kpi_full" text="Almost full"/>
|
||||
<text name="spu_attention" text="— Needs attention —"/>
|
||||
<text name="spu_attention_none" text="Nothing needs attention right now"/>
|
||||
<text name="spu_attention_hintFlow" text=" → click: create flow"/>
|
||||
<text name="spu_attention_hintOpen" text=" → click: open flow '%s'"/>
|
||||
<text name="spu_attention_hintProduce" text=" → produce/buy"/>
|
||||
<text name="spu_attention_produce" text="A flow won't help — the farm only has %2$s %1$s in total. Produce or buy more."/>
|
||||
<text name="spu_attention_flowName" text="%s to %s"/>
|
||||
<text name="spu_attention_hintSellFlow" text=" → click: create sell flow"/>
|
||||
<text name="spu_attention_sellFlowName" text="Sell %s"/>
|
||||
<text name="spu_alert_empty" text="%s runs out in %s — farm has %s, uses %s/day"/>
|
||||
<text name="spu_alert_fullIn" text="%s is %d %% full — full in %s"/>
|
||||
<text name="spu_alert_full" text="%s is %d %% full"/>
|
||||
<text name="spu_alert_sell" text="Sell %s now: price is %d %% of the yearly top — %s"/>
|
||||
<text name="spu_items_hint" text="(click a good)"/>
|
||||
<text name="spu_icol_amount" text="Stock"/>
|
||||
<text name="spu_icol_value" text="Value now"/>
|
||||
<text name="spu_icol_rate" text="Farm net · lasts"/>
|
||||
<text name="spu_item_perDay" text="%s/day"/>
|
||||
<text name="spu_item_fullIn" text="full in %s"/>
|
||||
<text name="spu_item_balanced" text="balanced"/>
|
||||
<text name="spu_item_share" text="%d %% of the good"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.26.0.0</version>
|
||||
<version>1.60.11.0</version>
|
||||
<title>
|
||||
<en>AD Smart Pickup</en>
|
||||
</title>
|
||||
@@ -12,6 +12,10 @@
|
||||
<multiplayer supported="true"/>
|
||||
<l10n filenamePrefix="l10n/l10n"/>
|
||||
<extraSourceFiles>
|
||||
<sourceFile filename="adCache.lua"/>
|
||||
<sourceFile filename="adNaming.lua"/>
|
||||
<sourceFile filename="adNames.lua"/>
|
||||
<sourceFile filename="adStuckDiag.lua"/>
|
||||
<sourceFile filename="adSupplyPlanner.lua"/>
|
||||
<sourceFile filename="adRuns.lua"/>
|
||||
<sourceFile filename="adRunsStorage.lua"/>
|
||||
@@ -22,6 +26,8 @@
|
||||
<sourceFile filename="adOutboundPlanner.lua"/>
|
||||
<sourceFile filename="adStorageStats.lua"/>
|
||||
<sourceFile filename="adFlowView.lua"/>
|
||||
<sourceFile filename="adGuide.lua"/>
|
||||
<sourceFile filename="adFlowGuide.lua"/>
|
||||
<sourceFile filename="adSources.lua"/>
|
||||
<sourceFile filename="adBuildings.lua"/>
|
||||
<sourceFile filename="adOutbound.lua"/>
|
||||
@@ -29,11 +35,25 @@
|
||||
<sourceFile filename="adFieldModel.lua"/>
|
||||
<sourceFile filename="adFieldFleet.lua"/>
|
||||
<sourceFile filename="adFieldView.lua"/>
|
||||
<sourceFile filename="adFieldGuide.lua"/>
|
||||
<sourceFile filename="adFields.lua"/>
|
||||
<sourceFile filename="adCpBridge.lua"/>
|
||||
<sourceFile filename="adCourseCache.lua"/>
|
||||
<sourceFile filename="adFieldStorage.lua"/>
|
||||
<sourceFile filename="adManualTransport.lua"/>
|
||||
<sourceFile filename="adFieldWork.lua"/>
|
||||
<sourceFile filename="adFieldSiloPlanner.lua"/>
|
||||
<sourceFile filename="adFieldSiloConfig.lua"/>
|
||||
<sourceFile filename="adFieldSilos.lua"/>
|
||||
<sourceFile filename="adCpResume.lua"/>
|
||||
<sourceFile filename="adHitch.lua"/>
|
||||
<sourceFile filename="adHitchCreep.lua"/>
|
||||
<sourceFile filename="adPool.lua"/>
|
||||
<sourceFile filename="adFlowWagons.lua"/>
|
||||
<sourceFile filename="adFleetNames.lua"/>
|
||||
<sourceFile filename="adFieldJobs.lua"/>
|
||||
<sourceFile filename="adMonthView.lua"/>
|
||||
<sourceFile filename="adProfiler.lua"/>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
</modDesc>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
**Brugermanual for spillere:** [dansk](docs/BRUGERMANUAL.md) · [English](docs/USER_MANUAL.md). Resten af denne fil er udviklernoter pr. version.
|
||||
|
||||
**Download / installation:** hent `FS25_ADSmartPickup.zip` under [Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases) og læg zip-filen (ikke udpakket) i `Documents/My Games/FarmingSimulator2025/mods/`. Kræver **AutoDrive** (og gerne Courseplay til markarbejde). Fanen "Smart Pickup" ligger i ESC-menuen. Fejl og idéer: opret et issue her i repoet.
|
||||
|
||||
AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Deliver:
|
||||
|
||||
## 1. Vare-bevidst mappe-rotation (én vare valgt)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Simuleringer
|
||||
|
||||
`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner
|
||||
(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode.
|
||||
|
||||
luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode
|
||||
git worktree add /tmp/old <commit>; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign
|
||||
|
||||
Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning.
|
||||
luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms.
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua <mappe med modulerne>)
|
||||
-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3
|
||||
-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode
|
||||
-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame.
|
||||
-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller.
|
||||
local dir = arg[1] or "FS25_ADSmartPickup"
|
||||
math.randomseed(42)
|
||||
local function noop() end
|
||||
Logging = {info = noop, warning = noop}
|
||||
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
|
||||
function entityExists() return true end
|
||||
local nodes = {}
|
||||
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
||||
local simTime = 0
|
||||
g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}}
|
||||
function netGetTime() return os.clock() * 1000 end
|
||||
addModEventListener = noop
|
||||
Utils = {prependedFunction = function(a) return a end}
|
||||
|
||||
-- vejnet og markører
|
||||
local wayPoints, markers = {}, {}
|
||||
for id = 1, 13550 do
|
||||
local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600
|
||||
wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}}
|
||||
end
|
||||
for index = 1, 600 do
|
||||
local id = index * 22
|
||||
table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"})
|
||||
end
|
||||
-- stationer ved hver 5. markør, 3 triggere hver
|
||||
local stations = {}
|
||||
for index = 1, 120 do
|
||||
local wp = wayPoints[markers[150 + index].id]
|
||||
local triggers = {}
|
||||
for t = 1, 3 do
|
||||
local node = #nodes + 1
|
||||
nodes[node] = {wp.x + t, wp.z + t}
|
||||
table.insert(triggers, {exactFillRootNode = node})
|
||||
end
|
||||
table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index,
|
||||
isa = function() return false end, getIsFillTypeAllowed = function() return true end})
|
||||
end
|
||||
g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end}
|
||||
g_currentMission.placeableSystem = {placeables = {}}
|
||||
-- køretøjer
|
||||
local vehicles = {}
|
||||
for index = 1, 40 do
|
||||
local node = #nodes + 1
|
||||
nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)}
|
||||
table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end})
|
||||
end
|
||||
g_currentMission.vehicleSystem = {vehicles = vehicles}
|
||||
local adEnv = {ADGraphManager = {
|
||||
getMapMarkers = function() return markers end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
getPathTo = function(_, _, id) return {{id = id}} end,
|
||||
}}
|
||||
|
||||
for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do
|
||||
local chunk = loadfile(dir .. "/" .. file)
|
||||
if chunk ~= nil then pcall(chunk) end
|
||||
end
|
||||
if ADCache ~= nil then ADCache.enabled = true end
|
||||
ADPrices = ADPrices or {}
|
||||
ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end
|
||||
|
||||
-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager)
|
||||
local function replan(vehicle)
|
||||
ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end)
|
||||
for _, marker in ipairs(markers) do
|
||||
ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1)
|
||||
end
|
||||
end
|
||||
|
||||
local t0 = os.clock()
|
||||
replan(vehicles[1])
|
||||
local single = (os.clock() - t0) * 1000
|
||||
|
||||
-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det.
|
||||
local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS)
|
||||
local timers = {}
|
||||
for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end
|
||||
local worst, steady, frames = 0, 0, 0
|
||||
for frame = 1, 60 * 60 do
|
||||
simTime = simTime + 16.7
|
||||
g_currentMission.time = simTime
|
||||
local started = os.clock()
|
||||
for index = 1, 15 do
|
||||
timers[index] = timers[index] + 16.7
|
||||
if timers[index] >= 5000 then
|
||||
timers[index] = 0
|
||||
replan(vehicles[index])
|
||||
end
|
||||
end
|
||||
local spent = (os.clock() - started) * 1000
|
||||
if spent > worst then worst = spent end
|
||||
if simTime > 12000 and spent > steady then steady = spent end
|
||||
if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end
|
||||
frames = frames + 1
|
||||
end
|
||||
print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " "))
|
||||
print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady))
|
||||
+90
-25
@@ -1,6 +1,6 @@
|
||||
# AD Smart Pickup — brugermanual
|
||||
|
||||
Version 1.26.0 · Farming Simulator 25 · tillæg til AutoDrive
|
||||
Version 1.28.0 · Farming Simulator 25 · tillæg til AutoDrive
|
||||
|
||||
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
||||
|
||||
@@ -159,12 +159,17 @@ samme rækkefølge.
|
||||
|
||||
### Knapper
|
||||
|
||||
Fanen **Flows** viser tavlen (én linje pr. flow) og under den det valgte flows **resumé**: Navn, Retning,
|
||||
Fra/Til, Varer, (Lager), Regler, Vent ved, Traktorer og Status. **Klik på et flow** i tavlen for at vælge det,
|
||||
og **klik på en resumé-linje** for at rette netop det trin (**Gem** gemmer og går tilbage). Klik på
|
||||
**Navn** for at omdøbe flowet.
|
||||
|
||||
| Knap | Funktion |
|
||||
|---|---|
|
||||
| **Nyt run** | Opretter et nyt flow. Det starter som *Ind til bygning* |
|
||||
| **Omdøb** | Giver det valgte flow et nyt navn |
|
||||
| **Slet run** | Sletter flowet og stopper dets kørende traktorer. Kan ikke fortrydes |
|
||||
| **Start run / Stop run** | Starter eller stopper alle traktorer i flowet |
|
||||
| **Start flow / Stop flow** | Starter eller stopper alle traktorer i flowet |
|
||||
| **Opsæt flow** | Guiden for det valgte flow med de nuværende valg |
|
||||
| **Nyt flow** | Guiden for et nyt flow. Intet oprettes før du trykker **Start flow** eller **Gem uden at starte** |
|
||||
| **Slet flow** | Sletter flowet og stopper dets kørende traktorer. Kan ikke fortrydes |
|
||||
|
||||
På fanen **Overblik** er knapperne i stedet:
|
||||
|
||||
@@ -172,12 +177,30 @@ På fanen **Overblik** er knapperne i stedet:
|
||||
|---|---|
|
||||
| **Start alle flows** | Starter alle flows med traktorer. Flows, der allerede kører, røres ikke, så ingen tur afbrydes |
|
||||
| **Stop alle flows** | Stopper alle aktive traktorer i alle flows |
|
||||
| **Nyt run** | Opretter et nyt flow og hopper til fanen **Flows** |
|
||||
| **Nyt flow** | Hopper til fanen **Flows** og åbner guiden for et nyt flow |
|
||||
|
||||
Statuslinjen viser bagefter, hvor mange flows og traktorer der blev startet, og op til tre grunde til
|
||||
traktorer, der blev sprunget over. Alle grunde står i `log.txt`.
|
||||
|
||||
Vælg flow i feltet **Run** øverst. Under tavlen står detaljerne for det valgte flow.
|
||||
### Guiden "Opsæt flow"
|
||||
|
||||
Samme trin-skærm og knapper som markguiden (**Næste**, **Tilbage**, **Annullér**; ét spørgsmål ad gangen,
|
||||
trin der ikke passer springes over):
|
||||
|
||||
| Trin | Vises når | Valg |
|
||||
|---|---|---|
|
||||
| **Navn** | nyt flow | *Skift navn …* åbner tekstfeltet |
|
||||
| **Retning** | altid | *Ind til bygning* (forsyning) / *Ud fra bygning* (udkørsel) |
|
||||
| **Til** / **Fra** | altid | Bygningerne (se *Bygningslisten* nedenfor) |
|
||||
| **Varer** | altid | Forsyning: *Automatisk (det bygningen mangler)* eller *Vælg selv*. Udkørsel: pr. vare Fra / Lager / Salg / Salg, ellers lager |
|
||||
| **Lager** | udkørsel med en vare på Lager | *Automatisk (nærmeste med plads)* eller *Vælg selv* blandt lagrene |
|
||||
| **Regler** | altid | Udkørsel: sælg fra %, lager over %, saml op på én tur. Forsyning: ture, kør hjem fra Wait under % |
|
||||
| **Vent ved** | altid | Wait-puljen |
|
||||
| **Traktorer** | altid | Ledige traktorer og flowets egne. Traktorer i andre flows (og dem hvis vogn ikke passer) er skjult med en linje |
|
||||
| **Oversigt** | altid | Resuméet; klik på en linje for at rette. **Start flow** eller **Gem uden at starte** |
|
||||
|
||||
Moden spørger aldrig om markøren ved bygningen, rutetjek, bedste foder først, læs-bytte, kildevalg,
|
||||
fordeling på flere mål, hvile af tomme kilder eller traktornavne.
|
||||
|
||||
### Retning
|
||||
|
||||
@@ -207,7 +230,7 @@ Et forsyningsflow holder én eller flere bygninger fyldt med alt det, de bruger.
|
||||
|
||||
### Opsætning
|
||||
|
||||
1. **Nyt run**, og lad Retning stå på **Ind til bygning**.
|
||||
1. **Nyt flow** (guiden), og lad Retning stå på **Ind til bygning**.
|
||||
2. Under **Til**: sæt Ja ud for de stalde og fabrikker, der skal fyldes. Listen viser også siloer,
|
||||
der kun kan fyldes og ikke tømmes, fx Pumps & Hoses' drive-on-bunkere (DOB 115), der fodrer en
|
||||
biogas-fermentor. For dem er **Vælg** med de varer, fermentoren skal have, bedst.
|
||||
@@ -538,14 +561,30 @@ maskiner. Kræver **Courseplay** ud over AutoDrive.
|
||||
| **Efter høst / bar mark** | (Stub hakkes) → (Kalkes) → Pløjes eller Kultiveres → (Sås) → (Tromles) |
|
||||
| **Pleje mens afgrøden vokser** | Gødes · Ukrudt bekæmpes · Kalkes — når jorden trænger |
|
||||
|
||||
### Vælg og glem: afgrøde på marken
|
||||
|
||||
Det nemmeste: vælg en afgrøde (fx *Hvede*) i guidens trin **Afgrøde** (Opsæt mark). Så passer moden resten:
|
||||
|
||||
- **Vokser der noget nu**, får det lov at vokse færdig, bliver passet (gødning, ukrudt, kalk) og høstet
|
||||
eller slået, når det er klart.
|
||||
- **Bagefter** klargøres marken og tilsås med din afgrøde. Er det ikke såtid, venter marken ("venter på
|
||||
såtid for …").
|
||||
- **Står din afgrøde der allerede** (græs → græs), bliver den bare slået og samlet op, hver gang den er klar.
|
||||
- **Bar mark:** klargøres og tilsås, så snart det er såtid.
|
||||
- Derefter kører cyklussen af sig selv: vækst → pleje → høst → klargøring → såning → …
|
||||
|
||||
Med en afgrøde valgt kører marken automatisk. Du kan stadig vælge ensilage/hø, halm, antal maskiner,
|
||||
jordbehandling og pleje i de næste trin. Vælg *Behold det der gror* for at styre marken som før.
|
||||
|
||||
### Opsætning
|
||||
|
||||
1. **En markør pr. mark.** Lav en AutoDrive-markør ved markkanten med navnet `F` + marknummeret, fx
|
||||
`F109`. Maskinerne kører dertil ad vejen, før Courseplay tager over. Højst 400 m fra marken. Mangler
|
||||
den, står marken med `!` og sendes ikke ud.
|
||||
2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig på.
|
||||
3. **Markflåde.** Nederst på fanen står alle køretøjer med et markredskab, med rolle og bredde. Sæt
|
||||
flueben ved dem, moden må sende ud. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører,
|
||||
3. **Markflåde.** Knappen **Markflåde** viser alle køretøjer med et markredskab, med rolle og bredde. Med
|
||||
**Brug alle ledige maskiner** = Ja (standard) finder moden selv maskinerne; med Nej bruges kun dem med
|
||||
flueben. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører,
|
||||
og køretøjer du selv kører, bruges aldrig. En sprøjte/spreder har først en rolle, når den har fyld på
|
||||
(gødning → gødningsspreder, kalk → kalkspreder, sprøjtemiddel → ukrudtsmaskine).
|
||||
4. **Parkering.** Sæt en AutoDrive-parkering på hvert sæt (på redskabet). Færdige sæt kører dertil.
|
||||
@@ -562,24 +601,50 @@ maskiner. Kræver **Courseplay** ud over AutoDrive.
|
||||
|
||||
### En mark
|
||||
|
||||
Klik på en mark i tabellen. Under tabellen står:
|
||||
Klik på en mark i tabellen. Under tabellen står markens **resumé** — én linje pr. emne:
|
||||
|
||||
| Valg | Betydning |
|
||||
| Linje | Eksempel |
|
||||
|---|---|
|
||||
| **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: den starter selv, når den er klar eller trænger til pleje |
|
||||
| **Maskiner pr. trin** | 1–5 sæt samtidig (Courseplays multi-tool). Et trin starter først når alle sæt er ledige |
|
||||
| **Plan** (slåafgrøde) | Ensilage eller Hø |
|
||||
| **Riv** (slåafgrøde) | Riv skårene sammen før opsamling |
|
||||
| **Aflæsningsvogne** (høst) | 1–3 vogne der følger høsteren og kører til lager |
|
||||
| **Stubbehakning** | Hak stubben efter høst |
|
||||
| **Jordbehandling** | Auto (pløj når jorden trænger, ellers kultivér), Kultivér, Pløj eller Ingen |
|
||||
| **Så bagefter** + **Afgrøde at så** | Så efter jordbehandlingen. Standard: samme afgrøde som blev høstet |
|
||||
| **Tromle** | Tromle efter såning |
|
||||
| **Gød / Kalk / Bekæmp ukrudt** | Pleje mens afgrøden vokser, når jorden trænger (højst to gange pr. pleje pr. vækst) |
|
||||
| **Tving lager** | Vælg lagre. Uden valg: ensilage → nærmeste gæringssilo, ellers silo; hø og høst → nærmeste silo |
|
||||
| **Afgrøde** | *Behold det der gror* / *-> Hvede* |
|
||||
| **Afgrødens valg** | *Ensilage, river* / *halm hakkes* |
|
||||
| **Klargøring** | *Automatisk* / *Pløj, kalk* |
|
||||
| **Pleje** | *gødning, ukrudt* |
|
||||
| **Maskiner** | *3 faste* / *ledige maskiner*, *1 ad gangen*, *1 aflæsningsvogne* |
|
||||
| **Lager** | *Marksilo* / *Automatisk* / en valgt silo |
|
||||
|
||||
Under valgene står **Jorden trænger til** (kalk, pløjning, gødning, ukrudt), markøren og evt. slåbredde og
|
||||
skår-forskydning.
|
||||
**Klik på en linje** for at rette netop det (fx lageret): kun det ene trin åbnes, og **Gem** gemmer og går
|
||||
tilbage. Under resuméet står hele statusteksten og markøren.
|
||||
|
||||
Bundknapperne: **Start nu / Stop mark**, **Opsæt mark** og **Markflåde**.
|
||||
|
||||
### Guiden "Opsæt mark"
|
||||
|
||||
**Opsæt mark** sætter marken op med ét spørgsmål ad gangen ("Trin 3 af 7 — …"). Trin, der ikke passer
|
||||
til marken, springes over; en opsat mark åbner med de nuværende valg. Uden valgt mark vælger du først marken.
|
||||
|
||||
| Trin | Vises når | Valg |
|
||||
|---|---|---|
|
||||
| **Afgrøde** | altid | *Behold det der gror* eller en afgrøde at så (mål-afgrøde, se ovenfor). Høst-/bar mark: *Så samme afgrøde igen efter høst* |
|
||||
| **Afgrødens valg** | slåafgrøde (nu eller som mål): Ensilage/Hø + Riv. Høstafgrøde med halm: Halm hakkes/samles/presses | |
|
||||
| **Klargøring** | efter høst / bar mark / skift af afgrøde | *Automatisk (anbefalet)* eller *Vælg selv*: jordbehandling, kalk, stubhakning, tromle |
|
||||
| **Pleje** | afgrøde der gror | Gødning, ukrudt (ikke på slåafgrøder); kalk her, når der ingen klargøring er |
|
||||
| **Maskiner** | altid | Antal ad gangen (1–5), aflæsningsvogne ved høst (1–3), **Faste maskiner til marken?** + listen |
|
||||
| **Lager** | høst/slåning | *Brug marksiloen?* (når der er en), ellers *Automatisk* eller *Vælg selv* blandt de 8 nærmeste |
|
||||
| **Oversigt** | altid | Resuméet; klik på en linje for at rette. **Kør automatisk** (Auto) eller **Gem uden at starte** (Manuel) |
|
||||
|
||||
Knapperne: **Næste**, **Tilbage**, **Annullér**. Intet gemmes før **Kør automatisk** / **Gem uden at starte**
|
||||
(eller **Gem** ved ret af én linje). Lukkes menuen, eller skifter du fane, kasseres guiden. Med en
|
||||
mål-afgrøde kører marken altid automatisk. Mangler markøren, kan marken sættes op; arbejdet venter på markøren.
|
||||
|
||||
**Faste maskiner:** en fast maskine bruges kun på denne mark, og marken venter på sin egen hellere end at
|
||||
låne en anden. Listen viser kun ledige maskiner og dem der allerede er faste her; maskiner der er faste på
|
||||
andre marker står som "N maskiner er faste på andre marker (F22)".
|
||||
|
||||
**Markflåde** (knap): den fælles markflåde — *Brug alle ledige maskiner* og listen med flueben, "fast på
|
||||
F22", "i gang på F108" og bredde-advarsler. **Til marker** går tilbage.
|
||||
|
||||
Moden spørger aldrig om: scanning, hvornår der høstes/slås/gødes/sprøjtes, påfyldning, tømning ved forkert
|
||||
vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter og gæringssilo ved ensilage.
|
||||
|
||||
En mark er **klar**, når mindst 90 % står i høstvinduet. **Start nu** starter hele kæden (eller pleje, hvis
|
||||
marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Testplan — Markarbejde (fanen Marker), version 1.26.0.0
|
||||
# Testplan — Markarbejde (fanen Marker), version 1.28.0.0 (+ guider v1.31/v1.32)
|
||||
|
||||
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
||||
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
||||
@@ -11,7 +11,7 @@ samme rækkefølge.
|
||||
|
||||
## 0. Forberedelse
|
||||
|
||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.26.0.0) FS25_ADSmartPickup`.
|
||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.28.0.0) FS25_ADSmartPickup`.
|
||||
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
||||
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
||||
@@ -73,6 +73,13 @@ Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set
|
||||
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
|
||||
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
|
||||
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
|
||||
- [ ] **3.6 Halm samles op.** Halm efter høst = *Samles op til silo*. Mejetærskeren lægger skår
|
||||
(Courseplay-indstilling halmskår = til), derefter `harvesting -> strawCollecting`, en læssevogn kører
|
||||
**høsterens kursus** (`harvester-kursus kopieret`) og kører halmen til en silo der tager STRAW.
|
||||
- [ ] **3.7 Halm til baller.** Halm = *Presses til baller*: `-> baling` (ballepresser på høsterens
|
||||
kursus) → `-> baleCollecting` (ballesamler/Universal Autoload, `Courseplay startet (ballesamler)`),
|
||||
fulde læs til ballelager (`ballesamler … kører fulde læs til …`).
|
||||
- [ ] **3.8 Halm hakkes.** Halm = *Hakkes*: mejetærskeren lægger IKKE skår (Courseplay halmskår = fra).
|
||||
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
|
||||
Kørende maskiner fortsætter.
|
||||
|
||||
@@ -112,6 +119,12 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
|
||||
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
|
||||
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
|
||||
- [ ] **6.6 Tom sprøjte/spreder (v1.29.2).** Tom sættes i gang → log `er tom — fyldes med X ved <markør> og
|
||||
kører så til F<nr>` → AutoDrive (LOAD) fylder op og kører til marken → Courseplay starter med fyld.
|
||||
Ingen påfyldning med AD-markør → sendes IKKE ud; marken viser "er tom for X … fyld den op".
|
||||
Sikkerhedsnet: Courseplay startes aldrig med tom tank ("er stadig tom … ved marken").
|
||||
- [ ] **6.7 Forkert vare (v1.28.8).** Spreder med kalk sendes til gødskning kun hvis ingen tom/gødet spreder
|
||||
findes → log `tømt for N l LIME (skal bruges til fertilizer)` → fyldes med gødning.
|
||||
|
||||
## 7. Multi-maskine (Courseplay multi-tool)
|
||||
|
||||
@@ -122,6 +135,24 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
|
||||
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
|
||||
|
||||
## 9. Vælg og glem (afgrøde på marken)
|
||||
|
||||
- [ ] **9.1 Kornmark → hvede.** En mark med byg der vokser: vælg *Afgrøde på marken* = Hvede. Linjen siger
|
||||
"Byg får lov at vokse færdig og høstes; derefter klargøres og tilsås med Hvede". Kørsel skjules (auto).
|
||||
Når byggen er moden: høst → (halm) → klargøring → såning af **hvede** (ikke byg). Log:
|
||||
`mål-afgrøde: WHEAT`, `starter (harvest: harvesting)`, `sowing`.
|
||||
- [ ] **9.2 Bar mark.** Bar mark med mål = Hvede: starter selv klargøring + såning, når det er hvede-såtid.
|
||||
Uden for såtid: status "venter på såtid for Hvede".
|
||||
- [ ] **9.3 Græs → græs.** Græsmark med mål = Græs: slås og samles op hver gang, ingen pløjning/såning.
|
||||
- [ ] **9.4 Græs → hvede.** Græsmark med mål = Hvede: slås og samles op én gang, derefter klargøring
|
||||
(jordbehandling-valget vises) og såning af hvede.
|
||||
- [ ] **9.5 Hele året.** Lad en mark med mål køre en hel sæson uden at røre den: vækst → pleje → høst →
|
||||
klargøring → såning → ... uden at hænge og uden dobbelt-såning.
|
||||
- [ ] **9.6 Alle ledige maskiner.** Markflåde: *Brug alle ledige maskiner* = Ja (standard): maskiner uden
|
||||
flueben sendes også ud. = Nej: kun maskiner med flueben. Maskiner du selv kører, maskiner i flows og
|
||||
maskiner AutoDrive/Courseplay allerede kører, bruges aldrig.
|
||||
- [ ] **9.7 Fjern målet.** Vælg *Ingen — planlæg selv*: marken går tilbage til Manuel.
|
||||
|
||||
## 8. Robusthed
|
||||
|
||||
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
|
||||
@@ -133,6 +164,73 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
|
||||
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
|
||||
|
||||
## 10. Guiden "Opsæt mark" (v1.31)
|
||||
|
||||
- [ ] **10.1 Oversigt.** Klik F22 i tabellen. Forvent: resumé-linjer (Afgrøde, …, Lager) med "[Ret]", hele
|
||||
status, markør. Ingen lang detaljeliste og ingen markflåde nederst. Knapper: Start nu, Opsæt mark, Markflåde.
|
||||
- [ ] **10.2 Guiden fra bunden.** Opsæt mark → "Trin 1 af N — Afgrøde". Klik Næste gennem alle trin uden at
|
||||
scrolle. Tællingen passer med de viste trin (græs: ingen Klargøring; bar mark: ingen Pleje/Lager).
|
||||
- [ ] **10.3 Valg gemmes.** Vælg fx Hø + Riv, 2 ad gangen, Lager: Vælg selv + en silo → Oversigt viser valgene
|
||||
→ **Kør automatisk**. Forvent: tabellens Plan "Hø + riv (x2)", Kørsel "Auto". Log: `F22 guide gemt: mode=auto …`.
|
||||
- [ ] **10.4 Annullér.** Åbn guiden, skift afgrøde, Annullér → intet ændret. Samme med ESC (menuen lukkes).
|
||||
- [ ] **10.5 Ret én linje.** Klik "Lager: …" i oversigten → "Ret: Lager"; skift → **Gem** → tilbage i
|
||||
oversigten med ændringen; Kørsel (Auto/Manuel) uændret. Tilbage i et Ret-trin fortryder.
|
||||
- [ ] **10.6 Ret fra Oversigt-trinnet.** I guidens sidste trin: klik en linje → ret → **OK** → tilbage på Oversigt.
|
||||
- [ ] **10.7 Mål-afgrøde skifter trin.** Kornmark: vælg Græs som afgrøde → trinnet "Afgrødens valg" viser
|
||||
Ensilage/Hø; tællingen ændres. På Oversigt kun **Kør automatisk** (ingen "Gem uden at starte").
|
||||
- [ ] **10.8 Faste maskiner.** Maskiner → Faste = Ja → sæt flueben ved 2 maskiner → gem. Åbn en anden mark:
|
||||
de 2 vises ikke; linje "2 maskiner er faste på andre marker (F22)". Log: `guide gemt … faste=…`.
|
||||
- [ ] **10.9 Marksilo.** Mark med silo inden for 150 m: Lager-trinnet spørger "Brug marksiloen?". Ja → resumé "Marksilo".
|
||||
- [ ] **10.10 Uden valgt mark.** Ingen mark valgt → Opsæt mark → trin "Mark" med listen; klik en mark → Afgrøde.
|
||||
Næste uden valgt mark giver "Vælg en mark først".
|
||||
- [ ] **10.11 Markflåde.** Knappen Markflåde viser flåden (flueben, "fast på F22"); klik et flueben gemmes;
|
||||
**Til marker** går tilbage.
|
||||
- [ ] **10.12 Klik routes.** Alle Ja/Nej, valg og flueben i guiden reagerer (ingen "GUI-fejl" i statuslinjen/log).
|
||||
|
||||
## 11. Guiden "Opsæt flow" (v1.32)
|
||||
|
||||
- [ ] **11.1 Flows-fanen.** Tavlen øverst; valgt flow (">") har resumé-linjer med "[Ret]" og Status. Ingen
|
||||
lang detaljeliste og ingen faste felter (Run, Wait-pulje …). Knapper: Start flow, Opsæt flow, Nyt flow, Slet flow.
|
||||
- [ ] **11.2 Nyt flow.** Nyt flow → "Trin 1 af 8 — Navn" → *Skift navn …* → skriv navn → Næste gennem Retning
|
||||
(Ud fra bygning), Fra (en fabrik), Varer (sæt en vare på Lager → trinnet Lager dukker op, tællingen bliver 9),
|
||||
Regler, Vent ved, Traktorer (flueben) → Oversigt → **Start flow**. Forvent: flowet findes, kører, log
|
||||
`run '…': …` med kind/products/storeBuildingIds.
|
||||
- [ ] **11.3 Annullér nyt flow.** Nyt flow → Annullér (eller ESC) → intet nyt flow i tavlen.
|
||||
- [ ] **11.4 Ret én linje.** Klik "Traktorer: …" → "Ret: Traktorer" → sæt flueben → **Gem** → tilbage; traktoren er i flowet.
|
||||
- [ ] **11.5 Omdøb.** Klik "Navn: …" → tekstfeltet åbner → nyt navn står i tavlen.
|
||||
- [ ] **11.6 Andre flows skjult.** Traktor i et andet flow vises ikke i trinnet Traktorer; linje "N traktorer kører i andre flows".
|
||||
- [ ] **11.7 Forsyning.** Opsæt et forsyningsflow: Til = stald, Varer = Vælg selv → varerne stalden tager vises; Regler viser Ture og "Kør hjem fra Wait under".
|
||||
- [ ] **11.8 Nyt flow fra Oversigt-fanen** hopper til Flows med guiden åben.
|
||||
- [ ] **11.9 Klik routes.** Alle valg (også pr. vare-politik) reagerer; ingen "GUI-fejl".
|
||||
|
||||
---
|
||||
|
||||
## 12. Genbrug af Courseplay-kurser (v1.34)
|
||||
|
||||
Log-linjer at kigge efter (`ADSmartPickup: F<nr> …`): `nyt kursus beregnes til '<maskine>' (<grund>)`,
|
||||
`kursus genbrugt ('<maskine>', <bredde> m) — ingen ny beregning`, `kursus kopieret fra '<A>' til '<B>'`,
|
||||
`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under `<courses>`.
|
||||
|
||||
- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)`
|
||||
→ CP starter; `<courses><course vehicle=… field=… width=… sig=… settings=… waypoints=…/>` står i filen efter gem.
|
||||
- [ ] **12.2 Genbrug.** Samme maskine til samme mark igen (næste pleje/klargøring, eller efter "Stop" og start) → log
|
||||
`kursus genbrugt` → CP kører UDEN beregnings-hak og kører hele marken fra start (ikke kun sidste stykke).
|
||||
⚠ Usikkert: accepterer CP 8.1 at jobbet startes på det eksisterende kursus uden `onClickGenerateFieldWorkCourse`?
|
||||
(validate kræver kun `hasCpCourse`). Tegn på fejl: `Courseplay: …no course…`, eller maskinen kører ingenting.
|
||||
- [ ] **12.3 Efter genstart.** Gem, genindlæs, vent de 2 min → samme maskine til samme mark → `kursus genbrugt`
|
||||
(CP har selv gemt kurset i CpAssignedCourses.xml; antal punkter matcher registret).
|
||||
- [ ] **12.4 Kopi fra anden maskine.** En anden maskine med samme CP-bredde til samme mark → `kursus kopieret fra 'A'`
|
||||
→ den kører kurset (ikke A's rest). ⚠ Usikkert: kopieret kursus + lane/vin som i dag med `cpCopyCourse`.
|
||||
- [ ] **12.5 Ny bredde.** Skift CP-bredden i HUD'en (eller en anden maskine med anden bredde) → `nyt kursus beregnes … (ny bredde)`.
|
||||
- [ ] **12.6 Spilleren laver selv kursus i CP's HUD** på maskinen → næste udsendelse genbruger det IKKE
|
||||
(antal punkter passer ikke) → kopi fra en anden maskine eller `… (kurset på maskinen er ændret)`.
|
||||
- [ ] **12.7 Ændret mark.** Lav marken om (kort-redigering/mark-tilkøb der ændrer omridset) → efter scanning, eller når
|
||||
marken gemmes i "Opsæt mark", log `markens form er ændret — N … glemt` → næste start beregner nyt og gemmer det.
|
||||
- [ ] **12.8 Multi-tool.** 2 sæt på samme mark: lederen genbruger kun et kursus lavet til 2 sæt/samme bane; følgeren kopierer
|
||||
stadig lederens kursus (log `…, multi-kursus kopieret, bane N`), og begge kører hver sin bane.
|
||||
- [ ] **12.9 Vinmark.** Sprøjte (skip 1) og harve (skip 0) på samme vinmark må ikke genbruge hinandens kursus (vin-indstillinger
|
||||
indgår i nøglen); høsteren (offset 0) heller ikke.
|
||||
|
||||
---
|
||||
|
||||
## Kendte usikkerheder (første gang i spillet)
|
||||
@@ -146,3 +244,12 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
|
||||
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
|
||||
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
|
||||
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
|
||||
| Halm-valg i guiden | `fruitType.hasWindrow/windrowName` = afgrøder med halm | Halm-valget mangler ved hvede eller vises ved majs |
|
||||
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
|
||||
| Kursus-genbrug (v1.34) | CP-job startes på maskinens eksisterende kursus uden ny beregning | `Courseplay: no course`, eller maskinen starter midt/sidst i kurset |
|
||||
|
||||
- [ ] **Høsterens rest (v1.32.2).** Modet høster en mark med aflæsningsvogne → ved høstens slutning stoppes
|
||||
vognene; høsteren med korn i tanken → log `'Mejetærsker' kører rest-læs (N l) til lager` → AutoDrive
|
||||
tømmer den ved lageret → parkering. Kunne AD ikke tømme den: besked "har stadig korn i tanken … tøm den".
|
||||
⚠ Usikkert: kan AutoDrive aflæsse en mejetærskers tank (røret) ved en silo?
|
||||
|
||||
+92
-25
@@ -1,6 +1,6 @@
|
||||
# AD Smart Pickup — user manual
|
||||
|
||||
Version 1.26.0 · Farming Simulator 25 · add-on for AutoDrive
|
||||
Version 1.28.0 · Farming Simulator 25 · add-on for AutoDrive
|
||||
|
||||
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
||||
|
||||
@@ -161,12 +161,17 @@ the same order.
|
||||
|
||||
### Buttons
|
||||
|
||||
The **Flows** tab shows the board (one line per flow) and below it the selected flow's **summary**: Name,
|
||||
Direction, From/To, Goods, (Storage), Rules, Wait at, Tractors and Status. **Click a flow** on the board to
|
||||
select it, and **click a summary line** to change just that step (**Save** saves and returns). Click
|
||||
**Name** to rename the flow.
|
||||
|
||||
| Button | What it does |
|
||||
|---|---|
|
||||
| **New run** | Creates a new flow. It starts as *Into building* |
|
||||
| **Rename** | Renames the selected flow |
|
||||
| **Delete run** | Deletes the flow and stops its running tractors. Cannot be undone |
|
||||
| **Start run / Stop run** | Starts or stops all tractors in the flow |
|
||||
| **Start flow / Stop flow** | Starts or stops all tractors in the flow |
|
||||
| **Set up flow** | The guide for the selected flow with its current choices |
|
||||
| **New flow** | The guide for a new flow. Nothing is created before you press **Start flow** or **Save without starting** |
|
||||
| **Delete flow** | Deletes the flow and stops its running tractors. Cannot be undone |
|
||||
|
||||
On the **Overview** tab the buttons are instead:
|
||||
|
||||
@@ -174,12 +179,30 @@ On the **Overview** tab the buttons are instead:
|
||||
|---|---|
|
||||
| **Start all flows** | Starts every flow that has tractors. Flows already running are left alone, so no trip is interrupted |
|
||||
| **Stop all flows** | Stops all active tractors in all flows |
|
||||
| **New run** | Creates a new flow and jumps to the **Flows** tab |
|
||||
| **New flow** | Jumps to the **Flows** tab and opens the guide for a new flow |
|
||||
|
||||
Afterwards the status line shows how many flows and tractors were started, plus up to three reasons for
|
||||
tractors that were skipped. All reasons are in `log.txt`.
|
||||
|
||||
Pick a flow in the **Run** field at the top. The details of the selected flow are shown below the board.
|
||||
### The "Set up flow" guide
|
||||
|
||||
Same step screen and buttons as the field guide (**Next**, **Back**, **Cancel**; one question at a time,
|
||||
steps that do not apply are skipped):
|
||||
|
||||
| Step | Shown when | Choices |
|
||||
|---|---|---|
|
||||
| **Name** | new flow | *Change name …* opens the text field |
|
||||
| **Direction** | always | *Into building* (supply) / *Out of building* (delivery) |
|
||||
| **To** / **From** | always | The buildings (see *The building list* below) |
|
||||
| **Goods** | always | Supply: *Automatic (what the building needs)* or *Choose myself*. Delivery: per good Off / Store / Sell / Sell, else store |
|
||||
| **Storage** | delivery with a good on Store | *Automatic (nearest with room)* or *Choose myself* among the stores |
|
||||
| **Rules** | always | Delivery: sell from %, store above %, collect in one trip. Supply: trips, leave Wait below % |
|
||||
| **Wait at** | always | The Wait pool |
|
||||
| **Tractors** | always | Free tractors and the flow's own. Tractors in other flows (and those whose trailer does not fit) are hidden with a line |
|
||||
| **Summary** | always | The summary; click a line to change it. **Start flow** or **Save without starting** |
|
||||
|
||||
The mod never asks about the marker at the building, route checks, best feed first, load swap, source
|
||||
choice, spreading over several targets, resting empty sources or tractor names.
|
||||
|
||||
### Direction
|
||||
|
||||
@@ -209,7 +232,7 @@ A supply flow keeps one or more buildings filled with everything they use.
|
||||
|
||||
### Setup
|
||||
|
||||
1. **New run**, and leave Direction on **Into building**.
|
||||
1. **New flow** (the guide), and leave Direction on **Into building**.
|
||||
2. Under **To**: set Yes on the barns and factories to fill. The list also shows silos that can only
|
||||
be filled and not emptied, such as the Pumps & Hoses drive-on bunkers (DOB 115) that feed a biogas
|
||||
fermenter. For those, **Select** with the goods the fermenter needs works best.
|
||||
@@ -544,14 +567,30 @@ and which machines. Requires **Courseplay** in addition to AutoDrive.
|
||||
| **After harvest / bare field** | (Mulch) → (Lime) → Plow or Cultivate → (Sow) → (Roll) |
|
||||
| **Care while the crop grows** | Fertilize · Control weeds · Lime — when the soil needs it |
|
||||
|
||||
### Select and forget: crop on the field
|
||||
|
||||
The easiest way: pick a crop (such as *Wheat*) in the guide step **Crop** (Set up field). The mod does the rest:
|
||||
|
||||
- **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when
|
||||
ready.
|
||||
- **Afterwards** the field is prepared and sown with your crop. If it is not sowing time, the field waits
|
||||
("waiting for sowing time for …").
|
||||
- **If your crop is already there** (grass → grass), it is simply mowed and collected every time it is ready.
|
||||
- **Bare field:** prepared and sown as soon as it is sowing time.
|
||||
- After that the cycle runs by itself: growth → care → harvest → preparation → sowing → …
|
||||
|
||||
With a crop chosen, the field runs automatically. You can still choose silage/hay, straw, number of
|
||||
machines, soil work and care in the following steps. Choose *Keep what grows* to run the field as before.
|
||||
|
||||
### Setup
|
||||
|
||||
1. **One marker per field.** Create an AutoDrive marker at the field edge named `F` + the field number,
|
||||
such as `F109`. Machines drive there on the road before Courseplay takes over. At most 400 m from the
|
||||
field. Without it, the field shows `!` and nothing is sent.
|
||||
2. **Fixed rigs.** The implement must be attached (or self-propelled). The mod never attaches.
|
||||
3. **Field fleet.** At the bottom are all vehicles with a field implement, with role and width. Tick the
|
||||
ones the mod may send out. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and
|
||||
3. **Field fleet.** The **Field fleet** button shows all vehicles with a field implement, with role and width. With
|
||||
**Use all free machines** = Yes (default) the mod finds the machines itself; with No only ticked ones
|
||||
are used. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and
|
||||
vehicles you drive are never used. A sprayer/spreader only has a role once it holds product
|
||||
(fertilizer → fertilizer spreader, lime → lime spreader, herbicide → weeding machine).
|
||||
4. **Parking.** Set an AutoDrive parking spot on each rig (on the implement). Finished rigs drive there.
|
||||
@@ -568,24 +607,52 @@ and which machines. Requires **Courseplay** in addition to AutoDrive.
|
||||
|
||||
### A field
|
||||
|
||||
Click a field in the table. Below the table:
|
||||
Click a field in the table. Below the table is the field's **summary** — one line per topic:
|
||||
|
||||
| Option | Meaning |
|
||||
| Line | Example |
|
||||
|---|---|
|
||||
| **Mode** | *Manual*: the field waits for **Start now**. *Auto*: it starts by itself when ready or when it needs care |
|
||||
| **Machines per step** | 1–5 rigs at once (Courseplay multi-tool). A step starts only when all rigs are free |
|
||||
| **Plan** (mowable) | Silage or Hay |
|
||||
| **Rake** (mowable) | Rake the swaths before collecting |
|
||||
| **Unloading trailers** (harvest) | 1–3 trailers that follow the harvester and drive to storage |
|
||||
| **Mulch stubble** | Mulch the stubble after harvest |
|
||||
| **Soil work** | Auto (plow when the soil needs it, else cultivate), Cultivate, Plow or None |
|
||||
| **Sow afterwards** + **Crop to sow** | Sow after soil work. Default: the crop that was harvested |
|
||||
| **Roll** | Roll after sowing |
|
||||
| **Fertilize / Lime / Control weeds** | Care while the crop grows, when the soil needs it (at most twice per care per season) |
|
||||
| **Force storage** | Pick stores. Without a choice: silage → nearest fermenting silo, else silo; hay and harvest → nearest silo |
|
||||
| **Crop** | *Keep what grows* / *-> Wheat* |
|
||||
| **Crop options** | *Silage, rake* / *straw chopped* |
|
||||
| **Preparation** | *Automatic* / *Plow, lime* |
|
||||
| **Crop care** | *fertilizer, weeds* |
|
||||
| **Machines** | *3 dedicated* / *free machines*, *1 at a time*, *1 unloaders* |
|
||||
| **Storage** | *Field silo* / *Automatic* / a chosen silo |
|
||||
|
||||
Below the options: **The soil needs** (lime, plowing, fertilizer, weeds), the marker, and mowing width and
|
||||
swath offset when relevant.
|
||||
**Click a line** to change just that (e.g. storage): only that step opens, and **Save** saves and returns.
|
||||
Below the summary are the full status and the marker.
|
||||
|
||||
Buttons: **Start now / Stop field**, **Set up field** and **Field fleet**.
|
||||
|
||||
### The "Set up field" guide
|
||||
|
||||
**Set up field** sets the field up one question at a time ("Step 3 of 7 — …"). Steps that do not fit the
|
||||
field are skipped; a field already set up opens with its current choices. Without a selected field you pick
|
||||
the field first.
|
||||
|
||||
| Step | Shown when | Choices |
|
||||
|---|---|---|
|
||||
| **Crop** | always | *Keep what grows* or a crop to sow (target crop, see above). Harvest/bare field: *Sow the same crop again after harvest* |
|
||||
| **Crop options** | mowable (now or as target): Silage/Hay + Rake. Harvest crop with straw: straw chopped/collected/baled | |
|
||||
| **Preparation** | after harvest / bare field / crop change | *Automatic (recommended)* or *Choose myself*: soil work, lime, mulch, roll |
|
||||
| **Crop care** | a growing crop | Fertilizer, weeds (not on mowables); lime here when there is no preparation |
|
||||
| **Machines** | always | At a time (1–5), unloaders when harvesting (1–3), **Dedicated machines for this field?** + the list |
|
||||
| **Storage** | harvest/mowing | *Use the field silo?* (when there is one), else *Automatic* or *Choose myself* among the 8 nearest |
|
||||
| **Summary** | always | The summary; click a line to change it. **Run automatically** (Auto) or **Save without starting** (Manual) |
|
||||
|
||||
Buttons: **Next**, **Back**, **Cancel**. Nothing is saved before **Run automatically** / **Save without
|
||||
starting** (or **Save** when editing one line). Closing the menu or switching tab discards the guide. With a
|
||||
target crop the field always runs automatically. Without a marker the field can be set up; work waits for
|
||||
the marker.
|
||||
|
||||
**Dedicated machines:** a dedicated machine only works this field, and the field waits for its own rather
|
||||
than borrowing another. The list only shows free machines and those already dedicated here; machines
|
||||
dedicated to other fields show as "N machines are dedicated to other fields (F22)".
|
||||
|
||||
**Field fleet** (button): the shared fleet — *Use all free machines* and the ticked list, "dedicated to
|
||||
F22", "working on F108" and width warnings. **Back to fields** returns.
|
||||
|
||||
The mod never asks about: scanning, when to harvest/mow/fertilize/spray, refilling, emptying wrong cargo,
|
||||
waiting for rain, width checks, choosing the nearest machine, routes, and fermenting silo for silage.
|
||||
|
||||
A field is **ready** when at least 90 % is in the harvest window. **Start now** starts the whole chain (or
|
||||
care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Guider til opsætning (marker + flows) — implementeringsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
|
||||
|
||||
**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
|
||||
|
||||
**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
|
||||
|
||||
**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
|
||||
- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
|
||||
- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
|
||||
- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
|
||||
- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
|
||||
- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
|
||||
- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
|
||||
- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
|
||||
- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
|
||||
- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
|
||||
2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
|
||||
3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
|
||||
4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
|
||||
5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
|
||||
|
||||
---
|
||||
|
||||
## Beslutninger (uklarheder i spec'en)
|
||||
|
||||
- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
|
||||
- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
|
||||
- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
|
||||
- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
|
||||
- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
|
||||
- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
|
||||
- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
|
||||
- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
|
||||
- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
|
||||
- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
|
||||
|
||||
---
|
||||
|
||||
# Etape A — fælles ramme + Marker (v1.31.0.0)
|
||||
|
||||
### Task A1: Guide-motoren `adGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADGuide.new(def, draft, context, options) -> guide
|
||||
-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
|
||||
-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
|
||||
-- guide = {def, draft, context, step, single, returnTo, snapshot}
|
||||
ADGuide.visibleSteps(guide) -> {stepId...}
|
||||
ADGuide.position(guide) -> index|nil, total
|
||||
ADGuide.isLast(guide) -> bool
|
||||
ADGuide.next(guide) -> guide, event -- event nil | "done"
|
||||
ADGuide.back(guide) -> guide, event -- event nil | "cancel"
|
||||
ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
|
||||
ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
|
||||
ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
|
||||
ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
|
||||
ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
|
||||
```
|
||||
|
||||
- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
|
||||
- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
|
||||
- [ ] Implementér; kør → ALLE TESTS OK.
|
||||
|
||||
### Task A2: Markguidens trin `adFieldGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
|
||||
- Produces:
|
||||
```lua
|
||||
ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
|
||||
ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
|
||||
ADFieldGuide.openPicker(context) -> guide -- trin "field"
|
||||
ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
|
||||
ADFieldGuide.isVisible(stepId, guide) -> bool
|
||||
ADFieldGuide.rows(guide) -> {row...}
|
||||
-- row = {type="choice", id, title, options={{label, value}}, value}
|
||||
-- | {type="toggle", id, title, off, on, value}
|
||||
-- | {type="check", id, item, title, value}
|
||||
-- | {type="text", label} | {type="link", id, value, label}
|
||||
ADFieldGuide.choose(guide, id, value) -> guide
|
||||
ADFieldGuide.summary(guide) -> {line...}
|
||||
ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
|
||||
ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
|
||||
ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
|
||||
```
|
||||
- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
|
||||
- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
|
||||
|
||||
- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
|
||||
- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
|
||||
|
||||
### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
|
||||
|
||||
**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
|
||||
SmartPickupFrame:initGuideButtons(i18n)
|
||||
SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
|
||||
SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
|
||||
SmartPickupFrame:guideText(label) -> string
|
||||
SmartPickupFrame:addLinkRow(text, action)
|
||||
SmartPickupFrame:onClickGuideRow(state, data)
|
||||
SmartPickupFrame:getGuideButtons() -> knapliste
|
||||
SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
|
||||
```
|
||||
- [ ] Skriv filen; `luajit -bl` syntakstjek.
|
||||
|
||||
### Task A4: Marker-fanen: oversigt, markflåde, guide
|
||||
|
||||
**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
|
||||
|
||||
- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
|
||||
- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
|
||||
- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
|
||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
||||
|
||||
### Task A5: Docs + ship v1.31.0.0
|
||||
|
||||
- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
|
||||
- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
|
||||
|
||||
# Etape B — Flows på samme ramme (v1.32.0.0)
|
||||
|
||||
### Task B1: Flowguidens trin `adFlowGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
|
||||
ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
|
||||
ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
|
||||
ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
|
||||
ADFlowGuide.summary(guide) -> {line...}
|
||||
ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
|
||||
ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
|
||||
ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
|
||||
```
|
||||
- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
|
||||
- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
|
||||
- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
|
||||
- [ ] FAIL → implementér → OK.
|
||||
|
||||
### Task B2: Flows-fanen i GUI'et
|
||||
|
||||
**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
|
||||
|
||||
- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
|
||||
- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
|
||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
||||
|
||||
### Task B3: Docs + ship v1.32.0.0; selv-review
|
||||
|
||||
- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
|
||||
- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,919 @@
|
||||
# Maskinpulje etape 1 (v1.48.0.0) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Markarbejdet kan sende en ledig solo-traktor, der henter et frakoblet puljeredskab (plov, kultivator,
|
||||
såmaskine …), udfører arbejdet med Courseplay, sætter redskabet af på dets AD-parkering og kører hjem.
|
||||
|
||||
**Architecture:** Nyt modul `adPool.lua` (ren planner + tynd spil-adapter) finder puljeredskaber (frakoblet +
|
||||
AD-parkering "X" + markør "Connect X") og puljetraktorer og parrer dem. `ADFieldWork.dispatch` bruger puljen som
|
||||
fallback, når ingen færdig kombination er ledig; en ny fase `fetching` kører `ADHitch.fetch` og fortsætter så i
|
||||
den eksisterende `driving`-fase. Når en opgave slutter, kører `ADHitch.returnHome` redskabet til dets parkering
|
||||
(afkobling) og traktoren hjem. Marker, der venter, betjenes længst-ventende først.
|
||||
|
||||
**Tech Stack:** Lua 5.1 (FS25-scripting), luajit til tests, AutoDrive 3.0.1.2 og Courseplay 8.1.0.3 API'er.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-28-maskinpulje-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Kun etape 1 (markarbejde). Tømmevogne ved høst (`decision.helper == true`) og marksilo-flowet bruger IKKE puljen i denne version.
|
||||
- Puljeredskab = frakoblet + AD-parkering "X" (`advd.parkDestination >= 1`) + markør "Connect X" (store/små bogstaver ligegyldigt).
|
||||
- Puljetraktor = motoriseret, intet koblet bag på, ikke i et flow (`ADRunsController.getRunForVehicle == nil`), ikke optaget, ikke styret af spilleren, egen AD-parkering, flueben i markflåden (`ADFieldFleet.isEnabled`).
|
||||
- Færdige kombinationer foretrækkes altid; puljen kun når `pick`/`pickMany` intet giver.
|
||||
- Redskab nærmest marken, derefter nærmeste traktor til redskabet med nok hk; manglende hk-data = tilladt.
|
||||
- Karantæne for et redskab, der ikke kunne kobles på: 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
||||
- En traktor køres kun hjem, når et af mod'ens egne jobs slutter — aldrig af sig selv.
|
||||
- Flows (runs) rører aldrig puljen.
|
||||
- Kommentarer og logtekster på dansk, som resten af koden. Ingen Co-Authored-By i commits. Push kun til remote `gitea`.
|
||||
- Tests køres fra repo-roden: `luajit tests/test_<navn>.lua` — output slutter med `ALLE TESTS OK`.
|
||||
- Build: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy mens spillet kører: `cp` til `~/FS25-data/mods/.FS25_ADSmartPickup.new` og `mv` på plads.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- **To marker vil have samme plov samtidig** → kun den første får den; den anden venter (reservation) — testet i Task 1 (`pairFor` springer reserverede over).
|
||||
- **Redskab uden hk-data eller traktor uden motor-data** → tillades (intet går i stå) — testet i Task 1 (`powerOk` nil-tilfælde, `numberOf` af tabel-specs).
|
||||
- **Traktor på hjemtur bliver valgt til et nyt job** → må ikke ske; den er optaget til den er hjemme — testet i Task 3 (`ADPool.isReturning`) og brugt i `getRigs`/`poolTractors`.
|
||||
- **Spilleren kobler selv et puljeredskab på og kører** → traktoren køres ikke hjem af mod'en — dækket af, at `returnHome` kun kaldes fra `finishAssignment`/fejlveje (Task 5); ingen tick-regel.
|
||||
- **Kobling fejler (trækøje står skævt)** → traktor hjem, redskab i karantæne, marken prøver igen — testet i Task 1 (karantæne i `pairFor`) og Task 4 (fejlvej).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `adPool.lua` — ren planlægning
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adPool.lua`
|
||||
- Test: `tests/test_adPool.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFieldFleet.roleFromSpecs(specs)` (adFieldFleet.lua:27), `ADFieldModel.checkWidth(role, width, requiredWidth)` (bruges via `accept`), `ADHitch.parseMarker(name)` (adHitch.lua).
|
||||
- Produces:
|
||||
- `ADPool.roleOf(specs) -> role|nil` (specs = `ADCpBridge.getSpecs`-tabel; `unloader` når `specs.spec_trailer and specs.spec_dischargeable` og ingen anden rolle).
|
||||
- `ADPool.numberOf(value) -> number|nil`
|
||||
- `ADPool.powerOk(tractorHp, neededHp) -> bool`
|
||||
- `ADPool.connectFor(parkName, markers) -> marker|nil` (markers = `{{name, id}}`)
|
||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) -> pair | nil, why, args`
|
||||
- implement = `{id, name, role, roles?, x, z, width, neededHp, connectId, parkId}`
|
||||
- tractor = `{id, name, x, z, hp, parkId}`
|
||||
- blocked = `{[id] = true}` (reserverede + karantæne, både redskab- og traktor-id)
|
||||
- pair = `{implement = implement, tractor = tractor}`
|
||||
- why ∈ `noPoolImplement` / `noPoolTractor` / `tooWeak` (args = `{neededHp}`)
|
||||
- `ADPool.queueOrder(infos, waitingSince) -> infos (ny tabel)`
|
||||
- `ADPool.reserve(owner, ids)`, `ADPool.release(owner)`, `ADPool.quarantine(id, nowMs)`, `ADPool.blocked(nowMs) -> {[id]=true}`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/test_adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
||||
dofile("FS25_ADSmartPickup/adHitch.lua")
|
||||
dofile("FS25_ADSmartPickup/adPool.lua")
|
||||
local P = ADPool
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
-- rolle ud fra redskabets egne specs
|
||||
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
|
||||
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
|
||||
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
|
||||
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
|
||||
check("rolle: intet", P.roleOf({}), nil)
|
||||
-- hk fra butiksdata: tal eller tabel (konfigurationer)
|
||||
check("tal", P.numberOf(180), 180)
|
||||
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
|
||||
check("tom tabel", P.numberOf({}), nil)
|
||||
check("nil", P.numberOf(nil), nil)
|
||||
check("hk: nok", P.powerOk(200, 180), true)
|
||||
check("hk: for lidt", P.powerOk(150, 180), false)
|
||||
check("hk: redskab uden data", P.powerOk(150, nil), true)
|
||||
check("hk: traktor uden data", P.powerOk(nil, 180), true)
|
||||
-- Connect-markør til en parkering
|
||||
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
|
||||
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
|
||||
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
|
||||
check("connect: ingen", P.connectFor("Start", markers), nil)
|
||||
check("connect: uden parkering", P.connectFor(nil, markers), nil)
|
||||
-- parring: redskab nærmest marken, så traktor nærmest redskabet
|
||||
local implements = {
|
||||
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
|
||||
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
|
||||
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
|
||||
}
|
||||
local tractors = {
|
||||
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
|
||||
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
|
||||
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
|
||||
}
|
||||
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
|
||||
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
|
||||
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
|
||||
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
|
||||
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
|
||||
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
|
||||
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
|
||||
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
|
||||
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
|
||||
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
|
||||
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
|
||||
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
|
||||
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
|
||||
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
|
||||
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
|
||||
check("par: redskab med flere roller", multi.implement.id, "spreder")
|
||||
-- reservationer og karantæne
|
||||
P.reserve("F12", {"plovNear", "mid"})
|
||||
check("reserveret: redskab", P.blocked(0).plovNear, true)
|
||||
check("reserveret: traktor", P.blocked(0).mid, true)
|
||||
P.release("F12")
|
||||
check("frigivet", P.blocked(0).plovNear, nil)
|
||||
P.quarantine("plovNear", 1000)
|
||||
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
|
||||
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
|
||||
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
|
||||
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
|
||||
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
|
||||
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
|
||||
check("kø: rører ikke input", infos[1].id, 5)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `luajit tests/test_adPool.lua`
|
||||
Expected: fejl `cannot open FS25_ADSmartPickup/adPool.lua`
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Create `FS25_ADSmartPickup/adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- AD Pool
|
||||
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
||||
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
||||
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
||||
|
||||
ADPool = {}
|
||||
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
||||
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
||||
-- tip/snegl er tømmevogn.
|
||||
function ADPool.roleOf(specs)
|
||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||
if role ~= nil and role ~= "unloader" then return role end
|
||||
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
||||
return role
|
||||
end
|
||||
|
||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||
function ADPool.numberOf(value)
|
||||
if type(value) == "number" then return value end
|
||||
if type(value) ~= "table" then return nil end
|
||||
local best = nil
|
||||
for _, entry in pairs(value) do
|
||||
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
||||
function ADPool.powerOk(tractorHp, neededHp)
|
||||
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
||||
return tractorHp >= neededHp
|
||||
end
|
||||
|
||||
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
||||
function ADPool.connectFor(parkName, markers)
|
||||
if type(parkName) ~= "string" then return nil end
|
||||
local wanted = parkName:lower()
|
||||
for _, marker in pairs(markers or {}) do
|
||||
local action, target = ADHitch.parseMarker(marker.name)
|
||||
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function hasRole(item, role)
|
||||
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
||||
end
|
||||
|
||||
local function distanceSq(ax, az, bx, bz)
|
||||
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
||||
end
|
||||
|
||||
local function nearest(items, x, z, keep)
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, item in ipairs(items) do
|
||||
if keep(item) then
|
||||
local distance = distanceSq(item.x, item.z, x, z)
|
||||
if distance < bestDistance then best, bestDistance = item, distance end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet.
|
||||
-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool.
|
||||
function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept)
|
||||
blocked = blocked or {}
|
||||
local free = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if not blocked[tractor.id] then table.insert(free, tractor) end
|
||||
end
|
||||
local candidates = {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
||||
table.insert(candidates, implement)
|
||||
end
|
||||
end
|
||||
if #candidates == 0 then return nil, "noPoolImplement" end
|
||||
if #free == 0 then return nil, "noPoolTractor" end
|
||||
table.sort(candidates, function(a, b)
|
||||
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
||||
end)
|
||||
local weakest = nil
|
||||
for _, implement in ipairs(candidates) do
|
||||
local tractor = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end)
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end
|
||||
end
|
||||
return nil, "tooWeak", {weakest or 0}
|
||||
end
|
||||
|
||||
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
||||
function ADPool.queueOrder(infos, waitingSince)
|
||||
local waiting, rest = {}, {}
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
||||
end
|
||||
table.sort(waiting, function(a, b)
|
||||
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
||||
if wa ~= wb then return wa < wb end
|
||||
return a.id < b.id
|
||||
end)
|
||||
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
||||
return waiting
|
||||
end
|
||||
|
||||
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
||||
local reservations, quarantined = {}, {}
|
||||
|
||||
function ADPool.reserve(owner, ids)
|
||||
reservations[owner] = ids
|
||||
end
|
||||
|
||||
function ADPool.release(owner)
|
||||
reservations[owner] = nil
|
||||
end
|
||||
|
||||
function ADPool.quarantine(id, nowMs)
|
||||
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
||||
end
|
||||
|
||||
function ADPool.blocked(nowMs)
|
||||
local result = {}
|
||||
for _, ids in pairs(reservations) do
|
||||
for _, id in ipairs(ids) do result[id] = true end
|
||||
end
|
||||
for id, untilMs in pairs(quarantined) do
|
||||
if nowMs < untilMs then result[id] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `luajit tests/test_adPool.lua`
|
||||
Expected: sidste linje `ALLE TESTS OK`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adPool.lua tests/test_adPool.lua
|
||||
git commit -m "feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `adPool.lua` — spil-adapter (indsamling af puljen)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (tilføj nederst)
|
||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (registrér `adPool.lua` efter `adHitch.lua`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADCpBridge.getSpecs(vehicle)` (adCpBridge.lua:153), `ADCpBridge.getWorkWidth(vehicle)` (:363), `ADRunsController.getFarmVehicles(adEnv)`, `ADRunsController.getRunForVehicle(vehicle)`, `ADRunsController.getVehicleId(vehicle)`, `ADRuns.vehicleLabel(vehicle)`, `ADFieldFleet.isEnabled(mode, fleet, id)`, `ADFieldJobs.state`, `ADHitch.rearImplementsOf(vehicle)` (Task 3), `ADPool.isReturning(vehicle)` (Task 3), `adEnv.ADGraphManager:getMapMarkers()`, `:getMapMarkerById(index)`, `g_currentMission.vehicleSystem.vehicles`.
|
||||
- Produces: `ADPool.collect(adEnv, isBusy) -> implements, tractors` (formater som Task 1; implement har også `.vehicle`, tractor har `.vehicle`); `isBusy(vehicle) -> bool` gives af kalderen (markarbejdets optaget-regel).
|
||||
|
||||
- [ ] **Step 1: Tilføj adapteren**
|
||||
|
||||
Tilføj nederst i `FS25_ADSmartPickup/adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- spil ----
|
||||
|
||||
local function parkMarker(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
if type(index) ~= "number" or index < 1 then return nil end
|
||||
return adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
end
|
||||
|
||||
local function positionOf(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
||||
local function neededHpOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
if item == nil then return nil end
|
||||
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
||||
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
||||
end
|
||||
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
||||
end
|
||||
|
||||
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata.
|
||||
local function tractorHpOf(vehicle)
|
||||
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
||||
local kw = motor ~= nil and motor.peakMotorPower or nil
|
||||
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
||||
end
|
||||
|
||||
local function isAttached(vehicle)
|
||||
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||
end
|
||||
|
||||
-- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer.
|
||||
-- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur).
|
||||
function ADPool.collect(adEnv, isBusy)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local implements, tractors = {}, {}
|
||||
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
||||
for _, vehicle in ipairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
||||
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
||||
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
||||
if role ~= nil then
|
||||
local x, z = positionOf(vehicle)
|
||||
table.insert(implements, {
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles, x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
||||
and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle)
|
||||
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) then
|
||||
local x, z = positionOf(vehicle)
|
||||
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||
hp = tractorHpOf(vehicle), parkId = park.id})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Registrér modulet**
|
||||
|
||||
I `FS25_ADSmartPickup/modDesc.xml` efter linjen `<sourceFile filename="adHitch.lua"/>`:
|
||||
|
||||
```xml
|
||||
<sourceFile filename="adPool.lua"/>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Syntakstjek og tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adPool.lua >/dev/null && echo syntaks-ok && luajit tests/test_adPool.lua | tail -1`
|
||||
Expected: `syntaks-ok` og `ALLE TESTS OK`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adPool.lua FS25_ADSmartPickup/modDesc.xml
|
||||
git commit -m "feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `adHitch.lua` — hjemtur og "på hjemtur"-status
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adHitch.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (isReturning-register)
|
||||
- Test: `tests/test_adHitch.lua`, `tests/test_adPool.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADHitch.route(adEnv, vehicle, markerIds, onDone)` (findes), `attachedImplements`/`rearImplements` (findes i adHitch.lua).
|
||||
- Produces:
|
||||
- `ADHitch.homeRoute(implementParkIds, tractorParkId) -> {id, ...}` (ren)
|
||||
- `ADHitch.rearImplementsOf(vehicle) -> {{object, z, name}}` (offentlig udgave af den lokale)
|
||||
- `ADHitch.returnHome(adEnv, tractor, onDone) -> bool` (false = intet puljeredskab bag / ingen parkering)
|
||||
- `ADPool.setReturning(vehicle, isOn)`, `ADPool.isReturning(vehicle) -> bool`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Tilføj i `tests/test_adHitch.lua` før `print(failures == 0`:
|
||||
|
||||
```lua
|
||||
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
|
||||
local home = H.homeRoute({4}, 2)
|
||||
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
|
||||
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
|
||||
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
|
||||
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
|
||||
```
|
||||
|
||||
Tilføj i `tests/test_adPool.lua` før `print(failures == 0`:
|
||||
|
||||
```lua
|
||||
-- på hjemtur: optaget indtil hjemme
|
||||
local tractorObject = {}
|
||||
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
|
||||
P.setReturning(tractorObject, true)
|
||||
check("hjemtur: markeret", P.isReturning(tractorObject), true)
|
||||
P.setReturning(tractorObject, false)
|
||||
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `luajit tests/test_adHitch.lua | tail -3; luajit tests/test_adPool.lua | tail -3`
|
||||
Expected: fejl `attempt to call field 'homeRoute' (a nil value)` og `attempt to call field 'isReturning'`
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
I `FS25_ADSmartPickup/adPool.lua`, efter `ADPool.blocked`:
|
||||
|
||||
```lua
|
||||
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
||||
local returning = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADPool.setReturning(vehicle, isOn)
|
||||
returning[vehicle] = isOn and true or nil
|
||||
end
|
||||
|
||||
function ADPool.isReturning(vehicle)
|
||||
return returning[vehicle] == true
|
||||
end
|
||||
```
|
||||
|
||||
I `FS25_ADSmartPickup/adHitch.lua`, i den rene del (efter `ADHitch.waitAfter`):
|
||||
|
||||
```lua
|
||||
-- Hjemtur: hvert puljeredskabs parkering (der sættes det af), så traktorens egen parkering.
|
||||
function ADHitch.homeRoute(implementParkIds, tractorParkId)
|
||||
local route, seen = {}, {}
|
||||
for _, id in ipairs(implementParkIds or {}) do
|
||||
if not seen[id] then seen[id] = true table.insert(route, id) end
|
||||
end
|
||||
if tractorParkId ~= nil then table.insert(route, tractorParkId) end
|
||||
return route
|
||||
end
|
||||
```
|
||||
|
||||
I spil-delen af `adHitch.lua`, lige efter `local function isImplementPark(...) ... end`:
|
||||
|
||||
```lua
|
||||
function ADHitch.rearImplementsOf(vehicle)
|
||||
return ADHitch.rearImplements(attachedImplements(vehicle))
|
||||
end
|
||||
|
||||
local function parkIdOf(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
```
|
||||
|
||||
Efter `function ADHitch.route(...) ... end`:
|
||||
|
||||
```lua
|
||||
-- Puljeredskaber bag traktoren sættes af på deres parkering, og traktoren kører hjem til sin egen.
|
||||
-- Kun redskaber med en Connect-markør til deres parkering er puljeredskaber; andre bliver siddende.
|
||||
function ADHitch.returnHome(adEnv, tractor, onDone)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local parkIds = {}
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do
|
||||
local parkId = parkIdOf(adEnv, implement.object)
|
||||
local park = parkId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(parkId) or nil
|
||||
if park ~= nil and ADPool.connectFor(park.name, markers) ~= nil then table.insert(parkIds, parkId) end
|
||||
end
|
||||
if #parkIds == 0 then return false end
|
||||
local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor))
|
||||
ADPool.setReturning(tractor, true)
|
||||
ADHitch.route(adEnv, tractor, route, function(vehicle, ok, detail)
|
||||
ADPool.setReturning(vehicle, false)
|
||||
if onDone ~= nil then onDone(vehicle, ok, detail) end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `luajit tests/test_adHitch.lua | tail -1; luajit tests/test_adPool.lua | tail -1; luajit -bl FS25_ADSmartPickup/adHitch.lua >/dev/null && echo syntaks-ok`
|
||||
Expected: `ALLE TESTS OK` to gange og `syntaks-ok`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adHitch.lua FS25_ADSmartPickup/adPool.lua tests/test_adHitch.lua tests/test_adPool.lua
|
||||
git commit -m "feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Markarbejde — puljen som fallback og fasen `fetching`
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`getRigs` :132-170, `dispatch` :382-482, `follow` :1083-1143, `release` :278-283)
|
||||
- Modify: `FS25_ADSmartPickup/l10n/l10n_da.xml`, `FS25_ADSmartPickup/l10n/l10n_en.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADCpBridge.getSeedFruits/hasSeeds/isMechanicalWeeder(vehicle)`, `ADPool.collect(adEnv, isBusy)`, `ADPool.pairFor(...)`, `ADPool.blocked(nowMs)`, `ADPool.reserve/release/quarantine`, `ADPool.isReturning`, `ADHitch.fetch(adEnv, vehicle, markerId, onDone)`, `ADHitch.returnHome`.
|
||||
- Produces: assignment-felter `pool = {implementId, implementName, connectId}`, `phase = "fetching"`, `hitch = {done=bool, ok=bool, detail=any}`; field-årsager `spu_field_reason_noPoolTractor`, `spu_field_reason_fetching` (tooWeak som fritekst).
|
||||
|
||||
- [ ] **Step 1: Optaget-regel genbrugelig + hjemtur tæller som optaget**
|
||||
|
||||
I `adFieldWork.lua` før `function ADFieldWork.getRigs`:
|
||||
|
||||
```lua
|
||||
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
||||
-- eller på hjemtur fra puljen (sætter redskab af).
|
||||
local function isBusyVehicle(vehicle, id)
|
||||
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
||||
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||
end
|
||||
```
|
||||
|
||||
I `getRigs` erstat `busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)\n or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),` med:
|
||||
|
||||
```lua
|
||||
busy = isBusyVehicle(vehicle, id),
|
||||
```
|
||||
|
||||
og tilføj som sidste alternativ i `busyWhy`-kæden (før `or nil`):
|
||||
|
||||
```lua
|
||||
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fallback i dispatch**
|
||||
|
||||
I `ADFieldWork.dispatch`, lige efter `if rigs == nil then` og FØR `if #offNetwork > 0 ...`, indsæt:
|
||||
|
||||
```lua
|
||||
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
||||
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
|
||||
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
||||
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||
if pooled then return end
|
||||
if poolWhy == "tooWeak" or (poolWhy == "noPoolTractor" and why ~= "allBusy") then
|
||||
why, args = poolWhy, poolArgs
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Tilføj før `function ADFieldWork.dispatch` en ny funktion:
|
||||
|
||||
```lua
|
||||
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
||||
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
||||
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
||||
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
||||
end)
|
||||
local accept = function(implement)
|
||||
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
||||
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
||||
if role == "seeder" then
|
||||
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
||||
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
||||
end
|
||||
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
||||
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
||||
local required = ADFieldModel.requiredWidth(field, role)
|
||||
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
||||
end
|
||||
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
||||
if pair == nil then return false, why, args end
|
||||
local tractor, implement = pair.tractor, pair.implement
|
||||
local assignment = {
|
||||
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||
helper = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
|
||||
startX = tractor.x, startZ = tractor.z,
|
||||
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
||||
hitch = {done = false},
|
||||
}
|
||||
ADFieldJobs.state.assignments[tractor.id] = assignment
|
||||
ADPool.reserve(assignment, {implement.id, tractor.id})
|
||||
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
||||
assignment.hitch = {done = true, ok = ok, detail = detail}
|
||||
end)
|
||||
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
||||
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
||||
return true
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Frigiv reservation ved release**
|
||||
|
||||
I `ADFieldWork.release`, før `ADFieldJobs.state.assignments[assignment.vehicleId] = nil`:
|
||||
|
||||
```lua
|
||||
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Fasen fetching i follow**
|
||||
|
||||
I `ADFieldWork.follow`, efter linjen `wake(vehicle)` og før `if assignment.phase == "driving" then`, gør `if` til en kæde ved at indsætte:
|
||||
|
||||
```lua
|
||||
if assignment.phase == "fetching" then
|
||||
local hitch = assignment.hitch
|
||||
if not hitch.done then return end
|
||||
ADPool.release(assignment)
|
||||
if not hitch.ok then
|
||||
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
|
||||
ADPool.quarantine(assignment.pool.implementId, now())
|
||||
ADFieldWork.release(assignment)
|
||||
note(assignment.fieldId, {retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS,
|
||||
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
||||
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
return
|
||||
end
|
||||
-- koblet på: bredden med redskabet på (for bred -> hjem igen)
|
||||
local width = ADCpBridge.getWorkWidth(vehicle)
|
||||
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
||||
if not okWidth then
|
||||
ADFieldWork.release(assignment)
|
||||
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
||||
ADHitch.returnHome(adEnv, vehicle)
|
||||
return
|
||||
end
|
||||
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
||||
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
||||
note(assignment.fieldId, {reason = false})
|
||||
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
||||
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
||||
elseif assignment.phase == "driving" then
|
||||
```
|
||||
|
||||
(og fjern det oprindelige `if assignment.phase == "driving" then`, som nu er `elseif`.)
|
||||
|
||||
Tilføj også `fetching` til spiller-reglen øverst i `follow`, så AD's kørsel under hentning ikke tolkes som spillerens: `steeredByAi` er allerede sand, mens AD kører; ingen ændring nødvendig.
|
||||
|
||||
- [ ] **Step 5: Tekster**
|
||||
|
||||
`l10n/l10n_da.xml` (ved de andre `spu_field_reason_`):
|
||||
|
||||
```xml
|
||||
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
|
||||
<text name="spu_field_reason_fetching" text="henter '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
|
||||
```
|
||||
|
||||
`l10n/l10n_en.xml`:
|
||||
|
||||
```xml
|
||||
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
|
||||
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
|
||||
```
|
||||
|
||||
`tooWeak` bruger fritekst (hk står før rollen, og `fieldReasonText` sætter rollen først): i Step 2 erstattes linjen
|
||||
`why, args = poolWhy, poolArgs` med:
|
||||
|
||||
```lua
|
||||
if poolWhy == "tooWeak" then
|
||||
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
|
||||
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
||||
return
|
||||
end
|
||||
why, args = poolWhy, poolArgs
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Syntakstjek og alle tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||
Expected: `syntaks-ok`, ingen `FEJL`-linjer, `færdig`
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/l10n/l10n_da.xml FS25_ADSmartPickup/l10n/l10n_en.xml
|
||||
git commit -m "feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Markarbejde — hjemtur når opgaven slutter
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`finishAssignment` :834-862, fejlvejen i `follow` "nåede ikke" :1115-1120)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADHitch.returnHome(adEnv, tractor, onDone) -> bool`.
|
||||
- Produces: ingen nye navne.
|
||||
|
||||
- [ ] **Step 1: finishAssignment**
|
||||
|
||||
Erstat i `finishAssignment`:
|
||||
|
||||
```lua
|
||||
local park = not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
```
|
||||
|
||||
med:
|
||||
|
||||
```lua
|
||||
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
||||
-- bag, så det virker også efter genindlæsning); ellers som før
|
||||
local pooled = not assignment.adHandlesEnd and not assignment.manual and ADHitch ~= nil
|
||||
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
||||
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
||||
end)
|
||||
local park = not pooled and not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
if pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: "nåede ikke frem" og "tom" — også hjem med redskabet**
|
||||
|
||||
I `follow`, i grenen der slutter med
|
||||
|
||||
```lua
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
```
|
||||
|
||||
erstat de to linjer med:
|
||||
|
||||
```lua
|
||||
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
```
|
||||
|
||||
I `finishAssignment`, i "tom såmaskine/sprøjte"-grenen efter `ADFieldWork.release(assignment)`, indsæt:
|
||||
|
||||
```lua
|
||||
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Syntakstjek og alle tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||
Expected: `syntaks-ok`, ingen `FEJL`, `færdig`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldWork.lua
|
||||
git commit -m "feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Kø — længst ventende mark først
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldJobs.lua` (`stepField` dispatch-gren ~:421-426, `tick` løkke ~:486)
|
||||
- Test: `tests/test_adPool.lua` (dækket af `queueOrder`-tests i Task 1)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADPool.queueOrder(infos, waitingSince)`.
|
||||
- Produces: `ADFieldJobs.waitingSince = {[fieldId] = nowMs}`.
|
||||
|
||||
- [ ] **Step 1: Registrér ventetid**
|
||||
|
||||
I `adFieldJobs.lua` ved siden af `ADFieldJobs.lastDecision = {}`:
|
||||
|
||||
```lua
|
||||
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||
ADFieldJobs.waitingSince = {}
|
||||
```
|
||||
|
||||
I `stepField`, erstat dispatch-grenen:
|
||||
|
||||
```lua
|
||||
local before = ADFieldJobs.assignmentCount()
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
if ADFieldJobs.assignmentCount() > before then ADFieldJobs.lastDispatchAt = ADFieldJobs.now end
|
||||
```
|
||||
|
||||
med:
|
||||
|
||||
```lua
|
||||
local before = ADFieldJobs.assignmentCount()
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
if ADFieldJobs.assignmentCount() > before then
|
||||
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
elseif ADFieldJobs.waitingSince[info.id] == nil then
|
||||
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
|
||||
end
|
||||
```
|
||||
|
||||
I samme funktion, i grenen `decision.action == "advance"` efter `ADFieldJobs.setField(nextField)`:
|
||||
|
||||
```lua
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Kørsel i kø-rækkefølge**
|
||||
|
||||
I `ADFieldJobs.tick` erstat `for index, info in ipairs(ADFields.list()) do` med:
|
||||
|
||||
```lua
|
||||
local infos = ADFields.list()
|
||||
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
||||
for index, info in ipairs(infos) do
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Tests**
|
||||
|
||||
Run: `for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; luajit -bl FS25_ADSmartPickup/adFieldJobs.lua >/dev/null && echo syntaks-ok`
|
||||
Expected: ingen `FEJL`, `syntaks-ok`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldJobs.lua
|
||||
git commit -m "feat(pulje): marker der venter på en maskine betjenes længst-ventende først"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Version, build, deploy og in-game test
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (`<version>1.47.4.0</version>` → `1.48.0.0`)
|
||||
|
||||
- [ ] **Step 1: Bump version, alle tests, build, deploy**
|
||||
|
||||
```bash
|
||||
cd ~/fs25-adsmartpickup
|
||||
sed -i 's#<version>1\.47\.4\.0</version>#<version>1.48.0.0</version>#' FS25_ADSmartPickup/modDesc.xml
|
||||
for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done
|
||||
rm -f FS25_ADSmartPickup.zip; (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .)
|
||||
cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.new && mv ~/FS25-data/mods/.FS25_ADSmartPickup.new ~/FS25-data/mods/FS25_ADSmartPickup.zip
|
||||
unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -m1 '<version>'
|
||||
```
|
||||
|
||||
Expected: ingen `FEJL`, `<version>1.48.0.0</version>`
|
||||
|
||||
- [ ] **Step 2: Commit og push**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/modDesc.xml
|
||||
git commit -m "chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde)"
|
||||
TOKEN=$(grep -A15 -i "^## Gitea" ~/credentials-vault.md | grep -oE "[a-f0-9]{40}" | head -1); git -c http.extraHeader="Authorization: Basic $(printf 'masterdraco:%s' "$TOKEN" | base64 -w0)" push -q gitea main
|
||||
```
|
||||
|
||||
- [ ] **Step 3: In-game test (savegame4, Golden Lands) — spilleren udfører, vi læser loggen**
|
||||
|
||||
Forudsætning: et redskab til markarbejde (fx kultivator/plov) med AD-parkering "X" og markør "Connect X" (bakkespor), Challengeren solo med egen AD-parkering, og en mark med markør der skal kultiveres/pløjes (plan auto eller Start).
|
||||
|
||||
Forventet i `log.txt` (grep `ADSmartPickup`):
|
||||
1. `F<n>: cultivator: 'Challenger MT635' henter '<redskab>' (pulje)`
|
||||
2. `kobling: 'Challenger MT635' koblede '<redskab>' på ved 'Connect X'`
|
||||
3. `F<n>: cultivator 'Challenger MT635' har '<redskab>' på — kører til <markør>`
|
||||
4. CP arbejder; `F<n>: cultivator '…' færdig`
|
||||
5. `F<n>: '…' sætter redskabet af og kører hjem`, `kobling: … koblede '<redskab>' af ved 'X'`, `… er færdig med turen (2 trin)`
|
||||
|
||||
Fejlscenarie: flyt redskabet 3 m væk fra sin plads → `kunne ikke koble … på` efter 8 s, traktoren kører hjem, fanen viser årsagen, redskabet springes over i 10 min.
|
||||
@@ -0,0 +1,690 @@
|
||||
# Flows/Fields: farvet liste + redigeringsvindue — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
|
||||
|
||||
**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
|
||||
|
||||
**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
|
||||
- Rækker er ≈835 px brede (som lagervisningen).
|
||||
- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
|
||||
- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
|
||||
- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
|
||||
- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
|
||||
- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
|
||||
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
|
||||
- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
|
||||
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rene statusfunktioner for flows (+ fælles farver)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
|
||||
- Test: `tests/test_flowStatus.lua` (ny)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
|
||||
`ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
||||
local V = ADFlowView
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
|
||||
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
|
||||
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
|
||||
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
|
||||
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
|
||||
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
|
||||
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
|
||||
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
|
||||
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
|
||||
|
||||
- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- status pr. flow (Flows-listen) ----
|
||||
|
||||
ADFlowView.LEVEL_COLORS = {
|
||||
running = {0.30, 0.75, 0.35, 1},
|
||||
waiting = {0.95, 0.80, 0.25, 1},
|
||||
error = {0.90, 0.35, 0.30, 1},
|
||||
stopped = {0.55, 0.55, 0.55, 1},
|
||||
}
|
||||
|
||||
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
|
||||
function ADFlowView.flowStatus(s)
|
||||
if s.stopped then return "stopped" end
|
||||
if s.warning ~= nil then return "error" end
|
||||
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
|
||||
if (s.running or 0) > 0 and s.reason == nil then return "running" end
|
||||
if (s.running or 0) > 0 or s.canStop then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
|
||||
- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rækker kan tegnes i et andet layout
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
|
||||
|
||||
- [ ] **Step 1: `addRow`** — replace body:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:addRow(prefab, data)
|
||||
local layout = self.rowTarget or self.boxLayout
|
||||
local row = prefab:clone(layout)
|
||||
row.spuLayout = layout
|
||||
FocusManager:loadElementFromCustomValues(row)
|
||||
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
|
||||
local option = row:getDescendantByName("option")
|
||||
if option ~= nil then
|
||||
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
||||
self.rowData[option] = data
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
|
||||
function SmartPickupFrame:withRowTarget(layout, fn)
|
||||
local previous = self.rowTarget
|
||||
self.rowTarget = layout
|
||||
local ok, err = pcall(fn, self)
|
||||
self.rowTarget = previous
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:clearPanelRows()
|
||||
for _, row in ipairs(self.panelRows or {}) do
|
||||
FocusManager:removeElement(row)
|
||||
row:delete()
|
||||
end
|
||||
self.panelRows = {}
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
|
||||
- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:colorLayout(layout)
|
||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
||||
if colors == nil or layout == nil then return end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local isEvenRow = false
|
||||
for _, element in ipairs(layout.elements) do
|
||||
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
||||
end
|
||||
isEvenRow = not isEvenRow
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:colorRows()
|
||||
self:colorLayout(self.boxLayout)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
|
||||
|
||||
```lua
|
||||
local layout = element.spuLayout or frame.boxLayout
|
||||
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
|
||||
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
||||
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
|
||||
```
|
||||
(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
|
||||
|
||||
- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
|
||||
- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Redigeringsvinduet (overlay + mixin)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
|
||||
- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
|
||||
- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
|
||||
- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
|
||||
|
||||
- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before `</Profiles>`):
|
||||
|
||||
```xml
|
||||
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
|
||||
<Profile name="spuOverlayBackdrop" extends="spuBar">
|
||||
<imageColor value="0 0 0 0.65"/>
|
||||
<positionOrigin value="topLeft"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<Profile name="spuOverlayPanel" extends="spuBar">
|
||||
<imageColor value="0.09 0.09 0.09 0.97"/>
|
||||
<positionOrigin value="topLeft"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<Profile name="spuPanelTitle" extends="spuCellBold">
|
||||
<textSize value="20px"/>
|
||||
</Profile>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: XML overlay** — directly after `</ScrollingLayout>` of `boxLayout`:
|
||||
|
||||
```xml
|
||||
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
|
||||
<GuiElement profile="baseReference" id="editOverlay" visible="false">
|
||||
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
|
||||
<Bitmap profile="spuOverlayPanel" id="editPanel">
|
||||
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
|
||||
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
|
||||
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
|
||||
</Bitmap>
|
||||
</GuiElement>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
|
||||
|
||||
```lua
|
||||
-- Smart Pickup — redigeringsvindue (v1.58)
|
||||
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
|
||||
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
|
||||
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
|
||||
|
||||
function SmartPickupFrame:initEditPanelButtons(i18n)
|
||||
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
|
||||
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
|
||||
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
|
||||
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:editPanelKind()
|
||||
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
|
||||
end
|
||||
|
||||
-- Vinduet dækker listens flade; panelet er centreret i den.
|
||||
function SmartPickupFrame:layoutEditPanel()
|
||||
local box = self.boxLayout
|
||||
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
|
||||
self.editBackdrop:setPosition(x, y)
|
||||
self.editBackdrop:setSize(w, h)
|
||||
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
|
||||
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
|
||||
self.editPanel:setSize(pw, ph)
|
||||
local top = 80 * g_pixelSizeScaledY
|
||||
self.editLayout:setSize(pw, ph - top)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openEditPanel(kind, id)
|
||||
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
|
||||
self:closeGuide()
|
||||
self.editOverlay:setVisible(true)
|
||||
self:layoutEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:closeEditPanel()
|
||||
if not self.editPanelOpen then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self:closeGuide()
|
||||
self:clearPanelRows()
|
||||
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
|
||||
function SmartPickupFrame:fillEditPanel(adEnv)
|
||||
self:clearPanelRows()
|
||||
local hooks = self:editPanelKind()
|
||||
if hooks == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then
|
||||
self.statusOverride = self.i18n:getText("spu_panel_gone")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self.editOverlay:setVisible(false)
|
||||
return
|
||||
end
|
||||
self.editTitle:setText(hooks.title(self, self.editPanelId))
|
||||
self:withRowTarget(self.editLayout, function(frame)
|
||||
if frame.guide ~= nil then
|
||||
frame:fillGuideRows(adEnv)
|
||||
else
|
||||
hooks.fill(frame, adEnv, frame.editPanelId)
|
||||
end
|
||||
end)
|
||||
self:colorLayout(self.editLayout)
|
||||
self.editLayout:invalidateLayout()
|
||||
self:refreshEditPanel(adEnv)
|
||||
end
|
||||
|
||||
-- Hvert sekund: status + knaptekster.
|
||||
function SmartPickupFrame:refreshEditPanel(adEnv)
|
||||
local hooks = self:editPanelKind()
|
||||
if hooks == nil or adEnv == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
|
||||
local status = hooks.status(self, adEnv, self.editPanelId)
|
||||
self.editStatus:setText(status.text or "")
|
||||
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
|
||||
if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
|
||||
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
|
||||
self.panelDeleteButtonInfo.disabled = not hooks.canDelete
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelClose()
|
||||
self:safe(function(frame)
|
||||
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
|
||||
if frame.guide ~= nil then
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
return
|
||||
end
|
||||
frame:closeEditPanel()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelStartStop()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelKind()
|
||||
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelDelete()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelKind()
|
||||
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wire into frame** —
|
||||
- `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
|
||||
- `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
|
||||
- `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
|
||||
- `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
|
||||
- `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
|
||||
- `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
|
||||
- `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
|
||||
- Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
|
||||
- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
|
||||
- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
|
||||
- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
|
||||
- Modify: `l10n/*.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
|
||||
- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
|
||||
|
||||
- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
|
||||
|
||||
```xml
|
||||
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
|
||||
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
|
||||
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
|
||||
|
||||
```lua
|
||||
-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
|
||||
function SmartPickupFrame:toggleRun(adEnv, runId)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if adEnv == nil or run == nil then return end
|
||||
self.lastStartFailed = self.lastStartFailed or {}
|
||||
if ADRunsController.getRunStatus(adEnv, run).canStop then
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
self.lastStartFailed[run.id] = nil
|
||||
self.statusOverride = nil
|
||||
else
|
||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||
self.lastStartFailed[run.id] = started == 0 and skipped > 0
|
||||
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
|
||||
self:showOverride(text, reasons)
|
||||
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
|
||||
end
|
||||
end
|
||||
ADRunsController.save()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowLevel(adEnv, run)
|
||||
local row = ADRunsController.getFlowRow(adEnv, run)
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
|
||||
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
|
||||
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
|
||||
end
|
||||
```
|
||||
And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
|
||||
|
||||
- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
|
||||
self.flowListRows = {}
|
||||
local i18n = self.i18n
|
||||
if #ADRunsController.store.runs == 0 then
|
||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||
return
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
for _, run in ipairs(group.runs) do
|
||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
|
||||
local runId = run.id
|
||||
local actionCell = entry.cells[5]
|
||||
self:makeRowClickable(row, function(frame)
|
||||
-- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
|
||||
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||
frame:toggleRun(frame.getAdEnv(), runId)
|
||||
frame:requestRebuild()
|
||||
else
|
||||
frame.selectedRunId = runId
|
||||
frame:openEditPanel("flow", runId)
|
||||
end
|
||||
end)
|
||||
table.insert(self.flowListRows, entry)
|
||||
end
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:refreshFlowListRows(adEnv)
|
||||
local unpackFn = table.unpack or unpack
|
||||
for _, entry in ipairs(self.flowListRows or {}) do
|
||||
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
|
||||
if run ~= nil then
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
local level = self:flowLevel(adEnv, run)
|
||||
local color = ADFlowView.LEVEL_COLORS[level]
|
||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||
entry.cells[1]:setText(tostring(run.name))
|
||||
entry.cells[2]:setText(tostring(cells[2]))
|
||||
entry.cells[3]:setText(tostring(cells[5]))
|
||||
entry.cells[4]:setText(tostring(cells[6]))
|
||||
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
|
||||
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
|
||||
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
|
||||
|
||||
- [ ] **Step 4: `refreshFlowTab`** — replace body:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:refreshFlowTab(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
return
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
local running, total = 0, 0
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
total = total + 1
|
||||
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
|
||||
end
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
|
||||
|
||||
```lua
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
|
||||
canDelete = true,
|
||||
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
|
||||
title = function(frame, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
|
||||
end,
|
||||
fill = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
|
||||
end,
|
||||
status = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
|
||||
end,
|
||||
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
|
||||
delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
|
||||
}
|
||||
```
|
||||
In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
|
||||
After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
|
||||
|
||||
- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
|
||||
- [ ] **Step 7: Run all tests + XML check.**
|
||||
- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Etape 1 build + in-game test
|
||||
|
||||
- [ ] **Step 1:** bump `modDesc.xml` `<version>` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'` shows 1.58.0.0.
|
||||
- [ ] **Step 2:** commit + push gitea main.
|
||||
- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Ren statusfunktion for marker
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldView.lua`
|
||||
- Test: `tests/test_fieldStatus.lua` (ny)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
|
||||
|
||||
- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldView.lua")
|
||||
local V = ADFieldView
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
|
||||
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
|
||||
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
|
||||
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
|
||||
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
|
||||
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
|
||||
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
|
||||
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
|
||||
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
(If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
|
||||
- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
|
||||
- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- status pr. mark (Fields-listen) ----
|
||||
|
||||
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
|
||||
ADFieldView.ERROR_REASONS = {
|
||||
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
|
||||
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
|
||||
}
|
||||
|
||||
-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
|
||||
function ADFieldView.fieldStatus(s)
|
||||
if not s.hasMarker then return "error" end
|
||||
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
|
||||
if s.awaiting or s.startRequested then return "waiting" end
|
||||
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
|
||||
if s.reasonKey ~= nil then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
```
|
||||
- [ ] **Step 4: Run all tests** — green.
|
||||
- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Fields-fanen som farvet liste + "field" i vinduet
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
|
||||
- Modify: `l10n/*.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
|
||||
- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
|
||||
|
||||
- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
|
||||
|
||||
```xml
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
|
||||
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
```
|
||||
The header row (column titles) passes a 7th title `""`.
|
||||
- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:fieldLevel(id)
|
||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
||||
if field == nil or info == nil then return "stopped" end
|
||||
local reason = type(field.reason) == "table" and field.reason.key or nil
|
||||
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
|
||||
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
|
||||
reasonKey = reason})
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldActionText(id)
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
|
||||
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
|
||||
end
|
||||
```
|
||||
(If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
|
||||
- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
|
||||
|
||||
```lua
|
||||
self:makeRowClickable(cells[1].parent, function(frame)
|
||||
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
|
||||
frame:toggleField(fieldId)
|
||||
else
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:openEditPanel("field", fieldId)
|
||||
end
|
||||
end)
|
||||
```
|
||||
Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
|
||||
- [ ] **Step 4: Panel kind** —
|
||||
|
||||
```lua
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
||||
canDelete = false,
|
||||
exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
|
||||
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
|
||||
fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
|
||||
status = function(frame, adEnv, id)
|
||||
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
|
||||
startStop = function(frame, id) frame:toggleField(id) end,
|
||||
delete = function() end,
|
||||
}
|
||||
```
|
||||
`fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
|
||||
`getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
|
||||
- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
|
||||
- [ ] **Step 6: Run all tests + XML check.**
|
||||
- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Etape 2 build + in-game test
|
||||
|
||||
- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
|
||||
- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
|
||||
- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Guider til opsætning: marker og flows — design
|
||||
|
||||
Dato: 2026-09-26 · Status: udkast til godkendelse · Mod: FS25_ADSmartPickup (v1.30.4.0 → v1.31/v1.32)
|
||||
|
||||
Leveres i to etaper, der hver kan testes for sig:
|
||||
- **Etape A (v1.31):** fælles guide-ramme + Fields-fanen ("Opsæt mark").
|
||||
- **Etape B (v1.32):** Flows-fanen på samme ramme ("Opsæt flow"), så de to faner er ens at bruge.
|
||||
|
||||
## Formål
|
||||
|
||||
Fields-fanen er blevet en lang liste af valg, der er svær at overskue (brugerens ord: "uoverskueligt").
|
||||
Målet er, at man sætter en mark op med **ét spørgsmål ad gangen**, og at alt, der kan afgøres automatisk,
|
||||
aldrig bliver spurgt om. En eksisterende opsætning skal kunne rettes uden at gå hele guiden igennem.
|
||||
|
||||
Succes: brugeren kan sætte F22 op fra bunden på under et minut uden at scrolle, og kan rette ét valg
|
||||
(fx lager) med to tryk.
|
||||
|
||||
## Hvad brugeren ser
|
||||
|
||||
### Oversigt (fanens standardvisning)
|
||||
|
||||
1. Marktabellen som i dag (Mark · Afgrøde · Plan · Kørsel · Status (kort) · Maskiner).
|
||||
2. Når en mark er valgt: et **resumé** på én linje pr. emne, hver med sin egen række man kan klikke for at
|
||||
rette det ene trin ("Ret"):
|
||||
- Afgrøde: `Behold det der gror` / `-> Hvede`
|
||||
- Afgrødens valg: `Ensilage, river` / `Halm hakkes`
|
||||
- Klargøring: `Automatisk` / `Plov, kalk`
|
||||
- Pleje: `Gødning + ukrudt`
|
||||
- Maskiner: `3 faste` / `Ledige maskiner` · `1 ad gangen, 1 aflæsningsvogn`
|
||||
- Lager: `Marksilo` / `Automatisk` / `F22 Silo`
|
||||
- Status (hele teksten, ombrudt)
|
||||
3. Knapper i bundlinjen (når en mark er valgt): **[Opsæt mark]**, **[Start nu / Stop]**, **[Markflåde]**.
|
||||
Uden valgt mark: **[Markflåde]**.
|
||||
4. Den lange detaljeliste og den fælles markflåde nederst forsvinder fra oversigten.
|
||||
|
||||
### Guide (erstatter fanens indhold, mens den er åben)
|
||||
|
||||
Ét trin pr. skærm: overskrift ("Trin 3 af 7 — Græs: ensilage eller hø?"), en kort forklaring, valgene som
|
||||
rækker, og bundknapperne **[Næste]** (eller **[Gem]** på sidste trin), **[Tilbage]**, **[Annullér]**.
|
||||
Trin der ikke er relevante for marken springes over, og tællingen følger de viste trin.
|
||||
|
||||
| # | Trin | Vises når | Valg | Standard |
|
||||
|---|------|-----------|------|----------|
|
||||
| 1 | Mark | guiden er åbnet uden valgt mark | liste over marker (nr, afgrøde, status) | — |
|
||||
| 2 | Afgrøde | altid | "Behold det der gror" + afgrøder der kan sås | nuværende plan |
|
||||
| 3 | Afgrødens valg | slåafgrøde (nu eller mål): ensilage/hø + river ja/nej. Høstafgrøde med halm: hakkes/samles/presses | se "Vises når" | nuværende plan |
|
||||
| 4 | Klargøring | altid (efter høst / bar mark) | "Automatisk (anbefalet)" / "Vælg selv" → plov, kultivering, kalk, tromle, mulch | Automatisk |
|
||||
| 5 | Pleje | afgrøde der gror | gødning ja/nej, ukrudt ja/nej | ja/ja |
|
||||
| 6 | Maskiner | altid | "Faste maskiner til marken?" ja/nej; ja → liste (kun ledige + dem der allerede er faste her); antal ad gangen (1–5); aflæsningsvogne ved høst (1–5) | nej / 1 / 1 |
|
||||
| 7 | Lager | høst/slåning | marksilo findes: "Brug marksiloen?" ja/nej; ellers/nej: "Automatisk (anbefalet)" / "Vælg selv" → de 8 nærmeste siloer | Automatisk |
|
||||
| 8 | Oversigt | altid | resumé med [Ret] pr. linje; [Kør automatisk] (Auto) / [Gem uden at starte] (Manual) | — |
|
||||
|
||||
### Redigering
|
||||
|
||||
- **[Opsæt mark]** på en opsat mark åbner guiden med de nuværende valg udfyldt.
|
||||
- **[Ret]** på en resumé-linje (oversigt eller trin 8) åbner kun det ene trin; **[Gem]** der gemmer og går
|
||||
tilbage dit man kom fra.
|
||||
- **[Annullér]** kasserer ændringer i guiden (intet gemmes før [Gem]/[Kør automatisk]).
|
||||
|
||||
### Markflåde
|
||||
|
||||
Egen visning (knap **[Markflåde]**): "Brug alle ledige maskiner" + listen med flueben, "fast på F22",
|
||||
"i gang på F108", bredde-advarsler. Samme indhold som i dag, flyttet ud af markens detaljer.
|
||||
|
||||
## Hvad der ALDRIG spørges om (automatisk)
|
||||
|
||||
Scanning af marken og markøren, hvornår der høstes/slås/gødes/sprøjtes, påfyldning (eget lager før køb),
|
||||
tømning ved forkert vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter, gæringssilo ved
|
||||
ensilage, marksilo-tømning (advarsel hvis intet flow tømmer den).
|
||||
|
||||
## Etape B: Flows-fanen på samme måde
|
||||
|
||||
Brugerens ønske: "det samme for flow opsætningen så det bliver ensrettet".
|
||||
|
||||
**Oversigt:** flow-tavlen som i dag (én linje pr. flow med status). Valgt flow → resumé med [Ret]-linjer
|
||||
(Retning · Fra · Til/Lager · Varer · Regler · Vent ved · Traktorer · Status) og bundknapperne
|
||||
**[Opsæt flow]**, **[Start/Stop]**, **[Nyt flow]**, **[Omdøb]**, **[Slet]**, **[Start alle / Stop alle]**.
|
||||
Den lange detaljeliste forsvinder.
|
||||
|
||||
**Guiden "Opsæt flow"** — samme trin-skærm, knapper og [Ret]/[Annullér] som markguiden:
|
||||
|
||||
| # | Trin | Vises når | Valg | Standard |
|
||||
|---|------|-----------|------|----------|
|
||||
| 1 | Navn | nyt flow | tekst (FS25-tekstdialog) | "Flow N" |
|
||||
| 2 | Retning | altid | Ind til bygning (forsyning) / Ud fra bygning (udkørsel) | nuværende |
|
||||
| 3 | Fra / Til | altid | bygninger med markør, fast rækkefølge (navn, markør), "Vis alle" | nuværende |
|
||||
| 4 | Varer | altid | forsyning: "Automatisk (det bygningen mangler)" / "Vælg selv" → varer; udkørsel: pr. vare Sælg / Lager / Begge / Fra | Automatisk / nuværende |
|
||||
| 5 | Lager | udkørsel med Lager | "Automatisk (nærmeste med plads)" / "Vælg selv" → lagerbygninger | Automatisk |
|
||||
| 6 | Regler | altid | udkørsel: sælg ved pris-%, lager over %, samlerunde ja/nej; forsyning: antal ture, retur fra Wait under % | nuværende / standard |
|
||||
| 7 | Vent ved | altid | Wait-puljer | nuværende |
|
||||
| 8 | Traktorer | altid | ledige traktorer (+ dem i flowet); traktorer i andre flows skjult med linje "N i andre flows" | nuværende |
|
||||
| 9 | Oversigt | altid | resumé med [Ret]; [Start flow] / [Gem uden at starte] | — |
|
||||
|
||||
Automatisk (spørges aldrig): markør ved bygningen, rutetjek, bedste foder først/parallelt foder, læs-bytte,
|
||||
kildevalg (mindst/fyldeste først), fordeling på flere mål, hvile af tomme kilder, traktornavne efter flowet.
|
||||
|
||||
## Arkitektur
|
||||
|
||||
- **`adGuide.lua` (ny, ren Lua, testet):** den fælles guide-motor, uafhængig af marker/flows:
|
||||
trin-liste, aktuelt trin, kladde, næste/tilbage/spring (til [Ret]), "returnér til oversigt", tælling
|
||||
"Trin 3 af 7" over de synlige trin. Både markguiden og flowguiden er en trin-definition oven på den.
|
||||
- **`gui/SmartPickupGuide.lua` (ny):** den fælles trin-skærm i menuen (overskrift, forklaring, valgrækker,
|
||||
bundknapper Næste/Tilbage/Annullér/Gem) — ét sted, så de to guider ser og opfører sig ens.
|
||||
- **`adFlowGuide.lua` (ny, etape B, ren Lua, testet):** flowguidens trin, valg, standarder og resumé; gemmer
|
||||
via `ADRunsController.updateRun`/`assignVehicle` som i dag.
|
||||
- **`adFieldGuide.lua` (ny, ren Lua, testet):** markguidens trin (oven på `ADGuide`).
|
||||
- `ADFieldGuide.open(field, info, context)` → guide-tilstand `{fieldId, draft, steps, index, returnTo}`;
|
||||
`draft` er en kopi af markens plan (+ faste maskiner), så intet ændres før gem.
|
||||
- `ADFieldGuide.steps(draft, info, context)` → listen af synlige trin (tabellen ovenfor).
|
||||
- `ADFieldGuide.choices(step, draft, info, context)` → rækkerne for trinnet (tekstnøgler + værdier).
|
||||
- `ADFieldGuide.choose(guide, key, value)` → ny guide med ændret draft (immutabelt).
|
||||
- `ADFieldGuide.next/back/jump(guide, step)`; `ADFieldGuide.result(guide)` → `{plan, rigIds}` til gem.
|
||||
- `ADFieldGuide.summary(field, info, context)` → resumé-linjerne `{step, key, args}`.
|
||||
- `context` = det spil-afhængige: marksilo findes?, siloer i nærheden, maskiner (ledige/faste), afgrøder
|
||||
der kan sås. Hentes af GUI'et, så modellen kan testes uden spillet.
|
||||
- **`gui/SmartPickupFieldTab.lua`:** oversigt (tabel + resumé) og guide-visning; bygger rækker ud fra
|
||||
`ADFieldGuide`, gemmer via `ADFieldJobs.setPlan` og `ADFieldFleet.dedicate`. Fil-størrelse: guide-visningen
|
||||
lægges i `gui/SmartPickupFieldGuide.lua` (ny), så ingen fil passerer ~800 linjer.
|
||||
- **`gui/SmartPickupFrame.lua`:** bundknapper for fanen (Opsæt mark, Start/Stop, Markflåde, Næste/Tilbage/
|
||||
Annullér/Gem i guiden) og routing af klik.
|
||||
- **Uændret:** `ADFieldModel` (plan-felter og lagring), `ADFieldJobs`, `ADFieldWork`, gemt XML. Guiden er et
|
||||
nyt ansigt på de samme data — eksisterende opsætninger virker videre.
|
||||
|
||||
## Fejl og kanttilfælde
|
||||
|
||||
- Mark uden markør: guiden kan køres, men oversigten siger "sæt markøren F22" og [Kør automatisk] gemmer som
|
||||
Manual indtil markøren findes (samme regel som i dag).
|
||||
- Maskine der er fast på en anden mark vises ikke i trin 6 (som v1.30.3); linje "N maskiner er faste på
|
||||
andre marker".
|
||||
- Marken forsvinder (solgt/bygget over) mens guiden er åben: guiden lukkes, oversigten vises.
|
||||
- Guiden er åben og menuen lukkes: kladden kasseres.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adGuide.lua`: motoren — næste/tilbage over skjulte trin, spring+retur, tælling, annullér.
|
||||
- `tests/test_adFlowGuide.lua` (etape B): trin pr. retning, varer auto/vælg, lager-trin kun ved Lager, resumé.
|
||||
- `tests/test_adFieldGuide.lua`: synlige trin pr. marktype (slåafgrøde, høst med halm, bar mark, mål-afgrøde),
|
||||
standardvalg, udfyldning fra eksisterende plan, `choose` ændrer kun kladden, `result` giver plan + rigIds,
|
||||
[Ret]-spring og tilbage, "Automatisk" vs "Vælg selv" i klargøring og lager, marksilo-spørgsmålet kun når en
|
||||
findes, resumé-linjerne.
|
||||
- Hele suiten skal være grøn; GUI-routing verificeres in-game (testplan-punkt).
|
||||
|
||||
## Uden for scope
|
||||
|
||||
Vinmarker (egne roller), genoptagelse af Courseplay efter genstart, fanerne Factories/Silos/Barns/Pallet sheds.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Marksilo-flow — design
|
||||
|
||||
Dato: 2026-09-27 · Status: godkendt i chat, afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Høsten lægges ofte i siloer ude ved markerne (mellemlager). I dag skal spilleren selv huske at køre varerne
|
||||
hjem. Marksilo-flowet kigger jævnligt alle marksiloer igennem og sender selv op til **x** ledige vogne ud for at
|
||||
hente varerne hjem til gårdens lagre.
|
||||
|
||||
**Succes:** står der varer i en marksilo, som gården kan tage, bliver de kørt hjem uden at spilleren gør noget,
|
||||
med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre flows eller spilleren.
|
||||
|
||||
## Brugerens valg (fra brainstorm)
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvor er "hjem"? | Nærmeste af de gårdlagre, spilleren vælger i flowet, der tager varen og har plads |
|
||||
| Hvad er "x vogne"? | Lånte ledige vogne fra hele gården — ingen fast tilknytning — højst x ude samtidig |
|
||||
| Hvilke vogne må lånes? | Samme regel og flueben som markarbejdet (`fleetMode` "all"/"ticked", `ADFieldJobs.state.fleet`) |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
|
||||
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
|
||||
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
|
||||
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
|
||||
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
|
||||
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
|
||||
|
||||
## Flowet
|
||||
|
||||
Marksilo-flowet er ÉN fast opsætning pr. savegame — ikke en ny run-type. Beslutning (2026-09-27, under
|
||||
planlægning): run-editoren (`SmartPickupFrame`) er bygget til præcis to typer (forsyning/udkørsel) mange steder,
|
||||
og runs har faste vogne, Start/Stop pr. vogn og Wait-puljer, som intet af det passer til lånte vogne. En egen
|
||||
underfane + egen gemmefil giver langt færre indgreb i eksisterende kode.
|
||||
|
||||
Opsætning (`ADSmartPickup_fieldsilos.xml` i savegame-mappen, gemmes sammen med runs):
|
||||
- `enabled` — til/fra, standard fra
|
||||
- `storeIds` — gårdlagre (bygnings-id'er fra `ADBuildings`, kun `kind == "silo"`)
|
||||
- `maxVehicles` — x, 1–10, standard 2
|
||||
- `minLiters` — tærskel pr. silo og vare: 5000 / 10000 / 25000 / 50000, standard 10000
|
||||
|
||||
## Planlægning (ren logik, nyt modul `adFieldSiloPlanner.lua`, testes med luajit)
|
||||
|
||||
Input: marksiloer `{id, markerId|nil, stock = {[fillType] = liters}}`, gårdlagre `{id, markerId, accepts =
|
||||
{[fillType] = freeLiters}, x, z}`, aktive lån `{siloId, fillType, liters}`, ledige vogne `{id, x, z, capacity,
|
||||
carries = {[fillType] = true}}`, `maxVehicles`, `minLiters`.
|
||||
|
||||
1. **Kvalificerede opgaver:** pr. (silo, vare) hvor silo har markør, `liters >= minLiters`, og mindst ét
|
||||
gårdlager har `freeLiters >= minLiters` for varen. Resten = liters minus det aktive lån på samme (silo, vare)
|
||||
allerede er på vej med; kun opgaver med rest `>= minLiters` tæller.
|
||||
Siloer uden markør → status `noMarker`; varer intet gårdlager tager → status `noFarmStore`.
|
||||
2. **Rækkefølge:** fuldeste rest først; lige → lavest silo-id (stabilt).
|
||||
3. **Vogne:** ledige pladser = `maxVehicles - #aktive lån`. For hver opgave i rækkefølge, så længe der er
|
||||
pladser: nærmeste ledige vogn (til siloen), der kan bære varen; resten reduceres med vognens kapacitet;
|
||||
samme opgave får flere vogne, så længe resten stadig er `>= minLiters`.
|
||||
4. **Gårdlager pr. lån:** nærmeste gårdlager (til siloen) der tager varen med `freeLiters >= min(vognens
|
||||
kapacitet, rest)`; ellers det med mest plads; ellers intet (opgaven springes over denne runde).
|
||||
|
||||
Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId, liters}}, status = {...}}`.
|
||||
|
||||
## Udførelse (`adFieldSilos.lua`)
|
||||
|
||||
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
|
||||
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
|
||||
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
|
||||
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
|
||||
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
|
||||
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
|
||||
(flows tager allerede kun deres egne tilknyttede vogne).
|
||||
- **Afsend:** AutoDrive "Hent og aflever": første markør = silomarkør, anden = gårdlagerets markør, vare =
|
||||
lånets vare, `loopCounter = 1`, parkering ved job slut slået til (`enableParkAtJobFinished`); start som
|
||||
markarbejdet (`startAd` → mode:start). Log: `marksilo: '<vogn>' henter <liter> l <vare> i <silo> → <lager>`.
|
||||
- **Frigivelse:** lånet slettes når AD ikke længere er aktivt for vognen (turen og parkeringen er færdig),
|
||||
når spilleren sætter sig i den, eller når flowet stoppes (stop = AD stoppes på lånte vogne).
|
||||
- **Vagt:** er vognen ikke kommet i gang (ingen stigning i fyldning og står stille) efter 120 s, stoppes AD,
|
||||
lånet slettes, og der logges én advarsel pr. (silo, vare), så samme opgave ikke fejler i ring hvert minut
|
||||
(opgaven får 10 min pause).
|
||||
- **Genindlæsning:** lån gemmes ikke. Ved indlæsning er alle vogne frie for flowet; en vogn AutoDrive selv
|
||||
genstarter, kører sin tur færdig som almindelig AD-tur.
|
||||
|
||||
## GUI
|
||||
|
||||
Ny underfane "Marksiloer" efter "Måned". Indstillinger: gårdlagre (eksisterende vælger),
|
||||
"Maks. vogne ude" (1–10), "Mindste mængde" (5.000 / 10.000 / 25.000 / 50.000 l), Start/Stop.
|
||||
Status: `x/y vogne ude · n siloer venter`, og én række pr. marksilo med indhold: silo, vare, liter, på vej,
|
||||
status (`ok` / `mangler markør` / `ingen gård tager <vare>`). Tekster i `l10n_da` + `l10n_en` (`spu_fs_*`).
|
||||
|
||||
## Uden for scope
|
||||
|
||||
- Salg direkte fra marksilo (kan senere bruge udkørslens prisregel).
|
||||
- Faste vogne i flowet (valg 1 i brainstorm) — fravalgt.
|
||||
- Lån der overlever genindlæsning.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adFieldSiloPlanner.lua`: kvalificering (tærskel, markør, gård tager varen), fuldeste først,
|
||||
x-loft inkl. aktive lån, flere vogne til én stor silo, vogn der ikke kan bære varen springes over, nærmeste
|
||||
gårdlager med plads, fallback til mest plads, intet lager → ingen afsendelse.
|
||||
- `tests/test_adFieldSiloConfig.lua`: standarder, grænser (x 1–10, tærskel kun de fire værdier), til/fra af gårdlager, records frem og tilbage.
|
||||
- In-game: silo ved en mark med 50.000 l hvede, gårdlager = Farma-silo, x = 2 → to vogne kører, afleverer,
|
||||
parkerer; markarbejdet tager ikke de lånte vogne imens.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Flow-vogne — design
|
||||
|
||||
Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har
|
||||
det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`).
|
||||
|
||||
## Formål
|
||||
|
||||
Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke
|
||||
længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har
|
||||
noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen.
|
||||
|
||||
## Brugerens valg
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning |
|
||||
| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor |
|
||||
| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet |
|
||||
| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af
|
||||
flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml`
|
||||
(`wagons="traktorId=vognId;…"`).
|
||||
- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og
|
||||
"bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde
|
||||
stille uden at flowet er stoppet).
|
||||
- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to
|
||||
tjek i træk (60 s mellem tjek).
|
||||
|
||||
## Adfærd
|
||||
|
||||
1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor]
|
||||
= redskab` (ændringer gemmes med næste gem).
|
||||
2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn:
|
||||
- vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect <vognens parkering>", derefter startes
|
||||
traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn".
|
||||
- vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s.
|
||||
- vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør).
|
||||
3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med
|
||||
`ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets.
|
||||
4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke).
|
||||
5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere
|
||||
— stoppet, traktorer kørt hjem".
|
||||
|
||||
## Puljen og flows
|
||||
|
||||
- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**.
|
||||
- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab.
|
||||
- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå.
|
||||
|
||||
## Komponenter
|
||||
|
||||
- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning
|
||||
væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`,
|
||||
`sendHome(run)`).
|
||||
- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records.
|
||||
- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`,
|
||||
`startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`).
|
||||
- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller.
|
||||
- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af
|
||||
bygning væk, ejer-opslag (stoppet vs kørende).
|
||||
- `tests/test_adRuns.lua`: records med `stopped` og `wagons`.
|
||||
- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter
|
||||
deres egne vogne og kører.
|
||||
|
||||
## Uden for scope
|
||||
|
||||
- Valg af vogn i GUI (automatisk er valgt).
|
||||
- At et flow tager en anden vogn end sin egen.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Maskinpulje — design
|
||||
|
||||
Dato: 2026-09-28 · Status: godkendt i chat (afsnit 1–4), afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Plove, harver, kultivatorer, såmaskiner, tømmevogne m.m. bruges kun en gang imellem. I dag skal hvert af dem have
|
||||
en traktor koblet fast på hele året, fordi markarbejdet kun kan sende en traktor, der allerede har det rigtige
|
||||
redskab bag sig. Maskinpuljen gør traktorer og redskaber til fælles puljer: et job venter i kø, til en traktor er
|
||||
ledig; traktoren henter det redskab, jobbet kræver, udfører arbejdet, parkerer redskabet på dets plads og kører
|
||||
selv hjem til sin egen parkering.
|
||||
|
||||
**Succes:** med fx 3 ledige traktorer, 2 plove og 1 kultivator bliver markernes arbejde udført uden at nogen
|
||||
traktor står låst til et redskab — færre køretøjer at købe og vedligeholde. Flows, der kører hele tiden (fodring,
|
||||
slurry, mælk …), påvirkes ikke.
|
||||
|
||||
## Brugerens valg (fra brainstorm)
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvordan findes redskabets Connect-markør? | Navneregel: AD-parkering "X" ↔ markør "Connect X" |
|
||||
| Hvem er i puljen? | Automatisk ud fra opsætningen — ingen afkrydsning |
|
||||
| Hvilken traktor henter? | Nærmeste ledige med nok hk |
|
||||
| Omfang | Markarbejde + tømmevogne ved høst + marksilo-flowet. Flows er undtaget |
|
||||
| Tømmevogne ved høst | Også fra puljen, men dedikeret til marken, til høsten dér er færdig |
|
||||
| Arkitektur | Udvid markarbejdets eget maskinvalg (ikke en separat puljemanager) |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Puljeredskab:** et redskab eller en vogn, der holder **frakoblet**, har en AD-parkering (`advd.parkDestination`
|
||||
→ markør "X"), og hvor der findes en markør "Connect X" (`ADHitch.parseMarker` = attach, navn = X; store/små
|
||||
bogstaver ligegyldigt). Uden Connect-markør er redskabet ikke i puljen — det er spillerens måde at holde det
|
||||
udenfor.
|
||||
- **Puljevogn:** puljeredskab med rollen `unloader` (vogn med tip/snegl, `spec_trailer` + `spec_dischargeable`).
|
||||
- **Puljetraktor:** motoriseret køretøj uden noget koblet bag på (`ADHitch.rearImplements` tom), ikke i et flow
|
||||
(`ADRunsController.getRunForVehicle == nil`), ikke optaget (samme `busy`-regel som `getRigs`), ikke styret af
|
||||
spilleren, med egen AD-parkering (`vehicle.advd.parkDestination >= 1`) og flueben i markflåden
|
||||
(`ADFieldFleet.isEnabled`).
|
||||
- **Færdig kombination:** det markarbejdet bruger i dag — traktor med redskabet allerede koblet på (`getRigs`).
|
||||
- **Puljekombination:** {traktor, redskab, Connect-markør-id, redskabets parkering-id, traktorens parkering-id}.
|
||||
|
||||
AD-parkering: hver maskine har sin egen `advd.parkDestination`; AD bruger det valgte redskabs, når et er koblet på
|
||||
(`AutoDriveVehicleData.lua:88-95`). Traktorens egen parkering læses derfor altid på traktoren selv.
|
||||
|
||||
## Komponenter
|
||||
|
||||
### `adPool.lua` (nyt, ren logik + tynd spil-adapter)
|
||||
|
||||
Ren logik (testes med luajit):
|
||||
- `ADPool.roleOf(specs)` — rolle for et frakoblet redskab ud fra dets egne specs via `ADFieldFleet.ROLE_SPECS`;
|
||||
vogn med `spec_trailer` + `spec_dischargeable` og intet andet → `unloader`.
|
||||
- `ADPool.isPoolImplement(item)` — `item = {attached, parkMarkerName, connectMarkerId}` → frakoblet, med parkering
|
||||
og Connect-markør.
|
||||
- `ADPool.powerOk(tractorPower, neededPower)` — true når et af tallene mangler (nil/0), ellers `tractorPower >=
|
||||
neededPower`.
|
||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, reserved, accept)` — redskab med rollen nærmest marken
|
||||
(ikke reserveret, `accept(implement)` sand), derefter nærmeste ledige traktor til redskabet med `powerOk`.
|
||||
Returnerer `pair` eller `nil, why, args` (`noPoolImplement` / `noPoolTractor` / `tooWeak` med hk-tal).
|
||||
- `ADPool.queueOrder(fields)` — markers ventende rækkefølge: `waitingSince` stigende, lige → lavest mark-id.
|
||||
- `ADPool.isReturnDue(rearImplements, poolParkIds)` — traktor har et puljeredskab bag sig (dets parkering har en
|
||||
Connect-markør) → hjemtur skal køres.
|
||||
|
||||
Spil-adapter:
|
||||
- `ADPool.collect(adEnv)` → `implements, tractors` fra `g_currentMission.vehicleSystem.vehicles` (gårdens), med
|
||||
position, rolle, specs til `acceptFor`, hk og parkering/Connect-id.
|
||||
- hk: `g_storeManager:getItemByXMLFilename(vehicle.configFileName).specs` → traktor `power`, redskab
|
||||
`neededPower`; mangler de, bruges nil (→ tilladt).
|
||||
- Reservationer: `ADPool.reserve(implementId, tractorId, owner)` / `release(owner)`; i hukommelsen, ryddes ved
|
||||
genindlæsning (se Genindlæsning).
|
||||
- Karantæne: redskab, der ikke kunne kobles på, springes over i 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
||||
|
||||
### `adHitch.lua` (findes, v1.47.4 — udvides let)
|
||||
|
||||
Udfører kørslerne: `fetch` (kør til Connect + kobl på), `route` (trin-tur, afkobling ved redskabets parkering).
|
||||
Ny: `ADHitch.returnHome(adEnv, tractor, onDone)` = `route({redskabets parkering, traktorens parkering})`; ved
|
||||
parkeringen kobles puljeredskabet af (samme regel som i dag i ruter).
|
||||
|
||||
### Markarbejdet (`adFieldWork.lua`, `adFieldJobs.lua`)
|
||||
|
||||
- `dispatch`: som i dag først færdige kombinationer. Returnerer `pick`/`pickMany` intet (`noRig`/`allBusy`/
|
||||
`cannotDo`), forsøges `ADPool.pairFor` for de manglende. Puljekombinationens assignment får `phase = "fetching"`,
|
||||
`pool = {implementId, connectId, implementParkId, tractorParkId}`, nøgle = traktorens id.
|
||||
- `follow`: fase `fetching` → når `ADHitch.fetch` melder ok: bredde tjekkes med redskabet på (`checkWidth`; for
|
||||
bred → hjemtur + årsag `tooWide`), derefter samme vej som i dag (`startAd` til markens markør / påfyldning,
|
||||
`phase = "driving"`). Fejl → hjemtur, karantæne, markens `retryAt` som ved `noRig`.
|
||||
- `finishAssignment`: har traktoren et puljeredskab bag sig (`ADPool.isReturnDue`) → `ADHitch.returnHome` i
|
||||
stedet for den almindelige parkeringstur; fase `returning`; assignment slettes, når hjemturen er færdig (eller
|
||||
fejler — så med advarsel).
|
||||
- Kø: `tick` gennemgår ventende marker i `ADPool.queueOrder`. `waitingSince` sættes, når en mark første gang ikke
|
||||
fik en maskine, og nulstilles ved udsendelse. `DISPATCH_GAP_MS` (10 s) beholdes.
|
||||
- Fanen viser faserne "henter X" og "sætter X af" og årsagerne `noPoolTractor`/`tooWeak`.
|
||||
|
||||
### Tømning ved høst
|
||||
|
||||
- Helper-dispatch (`role = "unloader"`, `helper = true`) bruger samme fallback: puljevogn + puljetraktor.
|
||||
- Efter `fetch`: `startUnloader` som i dag. Assignmentet bliver hos marken, til høststadiet er slut (som i dag);
|
||||
først da `returnHome`. Ingen frigivelse mellem læs.
|
||||
|
||||
### Marksilo-flowet (`adFieldSilos.lua`, `adFieldSiloPlanner.lua`)
|
||||
|
||||
- `collectVehicles` tilføjer puljekombinationer (puljevogn der kan bære varen + nærmeste puljetraktor) som
|
||||
kandidater med `pool = {...}`; planneren behandler dem som vogne (position = vognens).
|
||||
- `withoutHarvestReserve` tæller puljevogne med, så høsten går først.
|
||||
- Lån med `pool`: `ADHitch.fetch` → AD "Hent og aflever" som i dag (`enableParkAtJobFinished`) → når AD er
|
||||
inaktiv: `returnHome` → lånet slettes, når traktoren er hjemme. Vagten (120 s uden fremgang) starter efter
|
||||
koblingen.
|
||||
|
||||
## Genindlæsning
|
||||
|
||||
Reservationer og assignments gemmes ikke (som i dag). Efter indlæsning ser en traktor med puljeredskab ud som en
|
||||
færdig kombination; når markarbejdet eller marksilo-flowet afslutter et job med den, køres `returnHome`. Reglen
|
||||
afhænger kun af, hvad der hænger bag traktoren, ikke af gemt tilstand. En traktor, som ingen af vores jobs bruger,
|
||||
køres aldrig hjem af sig selv (spilleren kan have koblet et puljeredskab på for at køre selv).
|
||||
|
||||
## Fejl
|
||||
|
||||
| Situation | Handling |
|
||||
|---|---|
|
||||
| Kobling fejler (`noAttachable`, `stoppedAway`) | Traktor hjem, redskab i karantæne 10 min, marken prøver igen (andet redskab eller venter) |
|
||||
| Redskab for bredt når koblet på | Hjemtur, årsag `tooWide` i fanen |
|
||||
| Spilleren sætter sig i traktoren | Assignment afbrydes som i dag; reservationer frigives |
|
||||
| Ingen traktor stærk nok | Årsag `tooWeak` med hk-tal: "ingen traktor med ≥180 hk ledig til 'Plov 1'" |
|
||||
| Hjemtur fejler | Advarsel i loggen; traktor og redskab frigives (spilleren må ordne det) |
|
||||
|
||||
## Undtaget / uden for scope
|
||||
|
||||
- Flows (runs) — deres køretøjer er aldrig i puljen (uændret regel i `getRigs`).
|
||||
- Vogne uden Connect-markør — dedikerede som i dag.
|
||||
- Genbrug af et redskab direkte til næste mark uden at parkere (senere optimering).
|
||||
- Flere redskaber pr. traktor (front + bag).
|
||||
- Egen GUI-side for puljen.
|
||||
|
||||
## Etaper
|
||||
|
||||
1. **v1.48.0.0 — markarbejde med pulje:** `adPool.lua`, `returnHome`, dispatch-fallback, fase fetching/returning,
|
||||
kø-rækkefølge, fane-tekster. In-game: savegame4, mark der skal harves/pløjes, redskab med parkering + Connect,
|
||||
solo-traktor henter → arbejder → parkerer → hjem.
|
||||
2. **v1.49.0.0 — tømmevogn ved høst fra puljen.**
|
||||
3. **v1.50.0.0 — marksilo med puljevogne** (inkl. høst-reserve over puljevogne).
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adPool.lua`: puljeredskab ja/nej (koblet på, uden parkering, uden Connect), `roleOf` inkl.
|
||||
`unloader`, `powerOk` (nok/for lidt/manglende data), `pairFor` (redskab nærmest marken, traktor nærmest redskabet,
|
||||
reserveret og karantæne springes over, `tooWeak`/`noPoolTractor`/`noPoolImplement`), `queueOrder`,
|
||||
`isReturnDue`.
|
||||
- `tests/test_adHitch.lua`: `returnHome`-rutens trin.
|
||||
- `tests/test_adFieldJobs.lua`: færdig kombination foretrækkes; fallback til pulje; kø længst ventende først.
|
||||
- `tests/test_adFieldSiloPlanner.lua` (etape 3): puljekandidater og høst-reserve med puljevogne.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Flows og Fields: farvet liste + redigeringsvindue
|
||||
|
||||
Dato: 2026-09-29 · Status: godkendt i chat ("men kør"), afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Flows- og Fields-fanen er i dag én lang rullende side: listen over flows/marker efterfulgt af den valgte
|
||||
posts redigeringslinjer. Det er svært at overskue (brugerens screenshot 29/9 17:15). Målet:
|
||||
|
||||
- Fanen viser **kun listen**, med farvet status og **Start/Stop pr. række**.
|
||||
- **Klik på en række** åbner et **redigeringsvindue** (popup) oven på listen med de nuværende linjer.
|
||||
|
||||
Succes = man kan se med ét blik hvad der kører/venter/fejler, starte/stoppe en post uden at vælge den først,
|
||||
og redigere i et vindue der lukkes med Luk/ESC — uden at noget af flow-/mark-logikken ændres.
|
||||
|
||||
## Ikke-mål
|
||||
|
||||
- Ingen ændring af flow-/mark-logik, guiderne ("Nyt flow", "Opsæt mark") eller deres trin.
|
||||
- Oversigt-tavlen og Fields' flåde-visning er uændrede.
|
||||
- Ingen ny FS25-GUI-skærm/dialog (fravalgt: al rækkekode skulle genbygges; fokus/ESC oven på ESC-menuen er
|
||||
skrøbeligt). "Popup" er et panel inde i samme frame.
|
||||
|
||||
## Brugeroplevelse
|
||||
|
||||
### Flows-fanen (liste)
|
||||
|
||||
Én række pr. flow: `● navn · varer · kørende/i alt · statustekst · [Start]/[Stop]`.
|
||||
|
||||
| Niveau | Prik/statusfarve | Hvornår |
|
||||
|---|---|---|
|
||||
| running | grøn | mindst én traktor kører og ingen er blokeret |
|
||||
| waiting | gul | startet, men venter: ingen varer at hente, venter på vogn, traktor optaget, holder ved Wait |
|
||||
| error | rød | kan ikke køre: sidste start afvist (fx ingen vogn), advarsel (`!!`) |
|
||||
| stopped | grå | stoppet / aldrig startet |
|
||||
|
||||
- `[Start]/[Stop]` bruger den eksisterende `ADRunsController.startRun/stopRun` (samme regler som knaplinjen, inkl.
|
||||
sprunget-over-besked i højre side).
|
||||
- Klik på rækken (ikke knappen) åbner redigeringsvinduet for flowet.
|
||||
- Knaplinjen (i dag Tilbage, Start/Stop, Opsæt flow, Nyt flow, Slet) bliver: Tilbage, Nyt flow. Start/Stop sidder på
|
||||
rækkerne; "Opsæt flow" og "Slet" flyttes ind i vinduet.
|
||||
|
||||
### Fields-fanen (liste)
|
||||
|
||||
Nuværende kolonner (Mark, Afgrøde, Plan, Kørsel, Status, Traktorer) + `●` forrest og `[Start]/[Stop]` bagerst.
|
||||
|
||||
| Niveau | Hvornår |
|
||||
|---|---|
|
||||
| running (grøn) | marken har aktive opgaver der arbejder |
|
||||
| waiting (gul) | startet men venter: på maskine, vejr, påfyldning, "startes når en maskine er ledig" |
|
||||
| error (rød) | mangler markør, arbejdsbredde for bred/ukendt, ingen egnet maskine |
|
||||
| stopped (grå) | ingen plan / stoppet |
|
||||
|
||||
- `[Start]/[Stop]` genbruger `onClickFieldStartStop` (inkl. "Overtag høster").
|
||||
- Klik på rækken åbner vinduet med markens resumé og [Ret]-linjer.
|
||||
|
||||
### Redigeringsvinduet (fælles)
|
||||
|
||||
- Mørk halvgennemsigtig baggrund over listen; centreret panel (~70 % bredde, ~80 % højde).
|
||||
- Top: titel ("Rediger: Cow Food" / "Mark F12"), statuslinje i niveau-farve.
|
||||
- Midte: egen rullende liste med præcis det indhold, der i dag vises under listen:
|
||||
flow → `fillFlowSummary`-linjerne; mark → `fillFieldOverview`-linjerne. [Edit]/[Ret] åbner trinnet
|
||||
(guiden i enkelt-trin-tilstand) **inde i vinduet** med Gem/Tilbage som i dag.
|
||||
- Bund: `Luk` · `Start/Stop` · `Slet` (kun flow). ESC/Tilbage lukker vinduet (ikke menuen); står et trin åbent,
|
||||
går ESC først tilbage til resuméet.
|
||||
- Mens vinduet er åbent, opdateres statuslinjen og listen bagved hvert sekund som i dag.
|
||||
|
||||
## Arkitektur
|
||||
|
||||
### Rene funktioner (testbare)
|
||||
|
||||
- `ADFlowView.flowStatus(s)` → `{level, text}` hvor `s = {running, total, canStop, lastStartSkipped, warning,
|
||||
waitReason, holdReason, stopped}`. Afgør niveau efter tabellen ovenfor.
|
||||
- `ADFieldView.fieldStatus(s)` → `{level, text}` hvor `s = {working, awaiting, starting, missingMarker,
|
||||
widthProblem, noMachine, hasPlan}`.
|
||||
- `ADFlowView.LEVEL_COLORS` (grøn/gul/rød/grå) — flyttes/udleveres fra `SmartPickupStorageView`s lokale tabel,
|
||||
som også eksporterer `setTextColor` til genbrug.
|
||||
|
||||
### GUI
|
||||
|
||||
- **XML** (`SmartPickupFrame.xml`): efter `boxLayout` et `GuiElement id="editOverlay"` (skjult) med
|
||||
`Bitmap id="editBackdrop"` (fuld flade, sort ~60 %), `Bitmap id="editPanel"` (centreret, baggrund som
|
||||
fs25-paneler) med `Text id="editTitle"`, `Text id="editStatus"`, `ScrollingLayout id="editLayout"` (+ slider) og
|
||||
en knaplinje med tre `Button`s (`editClose`, `editStartStop`, `editDelete`).
|
||||
- **Rækkeprefabs**: `flowPrefab` og `fieldPrefab` får en `Bitmap name="dot"` (profil `spuDot`) forrest og en
|
||||
`Button name="rowAction"` bagerst. Kolonnebredder justeres så rækken stadig er ≈835 px.
|
||||
- **Mål-layout for rækker**: `addRow`/`addTextRow`/`addCellRow`/`addLinkRow`/`addGuideRow` kloner i dag altid ind i
|
||||
`self.boxLayout`. Der indføres `self.rowTarget` (standard `boxLayout`); vinduet sætter det til `editLayout`
|
||||
mens det fyldes. `clearRows` holder klonerne pr. layout, så listen og vinduet kan genopbygges hver for sig.
|
||||
- **Ny fil** `gui/SmartPickupEditPanel.lua` (mixin på `SmartPickupFrame`, `source()`s som de andre):
|
||||
`openEditPanel(kind, id)`, `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`,
|
||||
knap-callbacks. `kind ∈ {"flow","field"}`.
|
||||
- **Input**: `backButtonInfo`-callback og ESC tjekker først `self.editPanelOpen` (luk trin → luk vindue →
|
||||
normal Tilbage). Klik på backdrop gør intet (blokerer klik til listen bagved via `mouseEvent` der returnerer
|
||||
"brugt").
|
||||
- **Guide i vinduet**: `openFlowStep`/`openFieldStep` sætter `returnTo = "panel"`, så Gem/Tilbage vender tilbage til
|
||||
vinduets resumé i stedet for at lukke guiden til fanen.
|
||||
|
||||
### Dataflow
|
||||
|
||||
Klik på række → `openEditPanel(kind, id)` → `selectedRunId`/`selectedFieldId` sættes (eksisterende felter) →
|
||||
`requestRebuild` → `rebuild` tegner listen i `boxLayout` og, når vinduet er åbent, vinduets indhold i
|
||||
`editLayout`. Start/Stop i række eller vindue kalder de eksisterende controller-funktioner og `requestRebuild`.
|
||||
|
||||
## Fejlhåndtering
|
||||
|
||||
- Alt nyt GUI-arbejde kører i `self:safe(...)` som resten af frame'n; fejl vises i statuslinjen og logges.
|
||||
- Forsvinder flowet/marken mens vinduet er åbent (slettet), lukkes vinduet og statuslinjen siger hvorfor.
|
||||
- `rowAction`-knappen er deaktiveret når handlingen ikke er mulig (fx mark uden markør → ingen Start).
|
||||
|
||||
## Test
|
||||
|
||||
- luajit: `tests/test_flowStatus.lua` og `tests/test_fieldStatus.lua` for alle niveauer i tabellerne +
|
||||
grænsetilfælde (0 traktorer, stoppet med advarsel, venter + afvist).
|
||||
- Eksisterende tests skal forblive grønne (`test_noStrayGlobals` dækker den nye fil).
|
||||
- In-game (brugeren): liste med farver, Start/Stop pr. række, åbn/luk vindue med klik/Luk/ESC, [Edit] → Gem inde i
|
||||
vinduet, Slet flow fra vinduet, klik på listen bag vinduet gør intet, mark "Overtag høster" virker fra rækken.
|
||||
|
||||
## Levering
|
||||
|
||||
To etaper, hver sin version og in-game test:
|
||||
|
||||
1. **Flows**: rene statusfunktioner + fælles vindue + Flows-liste.
|
||||
2. **Fields**: mark-status + Fields-liste genbruger vinduet.
|
||||
|
||||
## Risici
|
||||
|
||||
- **Klik gennem backdrop** til listen: afbødes ved at backdrop'en fanger `mouseEvent`; fallback: skjul listens rækker
|
||||
(`setVisible(false)`) mens vinduet er åbent.
|
||||
- **Musehjul/rulning** i to ScrollingLayouts: hver får sin egen slider; FS25 ruller det layout musen er over.
|
||||
- **Knapper i rækkeprefabs** (Button i en klonet Bitmap) er ikke brugt før i modden: onClick sættes efter
|
||||
`clone` pr. række (som `makeRowClickable`), ikke via XML-callback.
|
||||
@@ -234,5 +234,21 @@ do
|
||||
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
|
||||
end
|
||||
|
||||
|
||||
-- pickLoadMarker: en pallespawner viger for aflæsningen, når begge vil have samme markør og aflæsningen står
|
||||
-- tættere (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food", foderindkast 5 m -> Goat Food fik "Sheep Milk")
|
||||
do
|
||||
local food, milk = {id = 1, name = "Sheep Food 1", distance = 9}, {id = 2, name = "Sheep Milk 1", distance = 13}
|
||||
local unloadBest = {id = 1, name = "Sheep Food 1", distance = 5}
|
||||
check("PL spawner viger for tættere aflæsning", ADBuildings.pickLoadMarker({food, milk}, unloadBest, true).name, "Sheep Milk 1")
|
||||
check("PL rigtig læssetrigger beholder markøren (Sugarbeet Crusher)", ADBuildings.pickLoadMarker({food, milk}, unloadBest, false).name, "Sheep Food 1")
|
||||
-- in-game 27/9 fårestald 3 og 5: spawner 5 m fra "Sheep Food", foderindkast 9 m -> spawneren vandt stadig
|
||||
check("PL spawner viger også når den selv står tættest", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, true).name, "Sheep Milk 1")
|
||||
check("PL rigtig læssetrigger tættest beholder", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, false).name, "Sheep Food 1")
|
||||
check("PL ingen konflikt", ADBuildings.pickLoadMarker({milk}, unloadBest, true).name, "Sheep Milk 1")
|
||||
check("PL eneste kandidat er aflæsningens -> ingen læssemarkør", ADBuildings.pickLoadMarker({food}, unloadBest, true), nil)
|
||||
check("PL ingen kandidater", ADBuildings.pickLoadMarker({}, unloadBest, true), nil)
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Kør: luajit tests/test_adCache.lua (fra repo-roden)
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
g_currentMission = {time = 0}
|
||||
dofile("FS25_ADSmartPickup/adCache.lua")
|
||||
local calls = 0
|
||||
local function lookup() calls = calls + 1; return "station" end
|
||||
check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation")
|
||||
check("K1 to opslag", calls, 2)
|
||||
ADCache.enabled = true
|
||||
calls = 0
|
||||
ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup)
|
||||
check("K2 genbrugt", calls, 1)
|
||||
g_currentMission.time = 900
|
||||
ADCacheGet("b", "k", 1000, lookup)
|
||||
check("K2 inden for levetid", calls, 1)
|
||||
g_currentMission.time = 1600
|
||||
ADCacheGet("b", "k", 1000, lookup)
|
||||
check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2)
|
||||
local nothing = 0
|
||||
ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end)
|
||||
check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil)
|
||||
check("K4 kun ét opslag", nothing, 1)
|
||||
-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig
|
||||
g_currentMission.time = 10000
|
||||
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end
|
||||
local expired = 0
|
||||
g_currentMission.time = 22000
|
||||
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end
|
||||
check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true)
|
||||
ADCache.clear()
|
||||
check("K6 ryddet", next(ADCache.buckets), nil)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,195 @@
|
||||
-- Kør: luajit tests/test_adCourseCache.lua (fra repo-roden)
|
||||
-- Kursus-registret: hvilken mark/bredde/markform/indstillinger en maskines nuværende Courseplay-kursus blev
|
||||
-- lavet til. Genbrug kun når ALT matcher; egen maskine først, ellers en anden maskines kursus (kopi).
|
||||
dofile("FS25_ADSmartPickup/adCourseCache.lua")
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local function count(map)
|
||||
local n = 0
|
||||
for _ in pairs(map) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
local C = ADCourseCache
|
||||
local SIG = "n=4;a=960000;c=800,300"
|
||||
|
||||
-- ---- indstillings-nøglen ----
|
||||
check("K1 standard", C.settingsKey({}), "off=0.0;lane=-;multi=1;vine=-;skip=-")
|
||||
check("K2 nil = standard", C.settingsKey(nil), C.settingsKey({}))
|
||||
check("K3 alt sat", C.settingsKey({toolOffset = 1.25, lane = -1, multiTools = 2, vineOffset = -1, vineSkip = 1}),
|
||||
"off=1.3;lane=-1;multi=2;vine=-1.0;skip=1")
|
||||
check("K4 -0.0 = 0.0", C.settingsKey({toolOffset = -0.0001}), C.settingsKey({toolOffset = 0}))
|
||||
check("K5 float-støj i forskydning tæller ikke", C.settingsKey({toolOffset = 1.2000001}), C.settingsKey({toolOffset = 1.2}))
|
||||
check("K6 anden bane = anden nøgle", C.settingsKey({multiTools = 2, lane = 1}) ~= C.settingsKey({multiTools = 2, lane = -1}), true)
|
||||
check("K7 vin-offset indgår", C.settingsKey({vineOffset = 0}) ~= C.settingsKey({vineOffset = -1}), true)
|
||||
check("K8 vin-skip indgår", C.settingsKey({vineOffset = -1, vineSkip = 0}) ~= C.settingsKey({vineOffset = -1, vineSkip = 1}), true)
|
||||
|
||||
local settings = C.settingsKey({})
|
||||
local request = {field = 22, width = 30, sig = SIG, settings = settings}
|
||||
local own = C.newRecord({vehicle = "v1", field = 22, width = 30.0, sig = SIG, settings = settings, waypoints = 812})
|
||||
|
||||
-- ---- key/matches ----
|
||||
check("M1 samme alt", C.matches(own, request), true)
|
||||
check("M2 bredde som float32 fra XML (30.0000001)", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30.0000001, sig = SIG, settings = settings}), request), true)
|
||||
check("M3 ny bredde", C.matches(own, {field = 22, width = 12, sig = SIG, settings = settings}), false)
|
||||
check("M4 anden mark", C.matches(own, {field = 23, width = 30, sig = SIG, settings = settings}), false)
|
||||
check("M5 form ændret", C.matches(own, {field = 22, width = 30, sig = "n=5;a=960000;c=800,300", settings = settings}), false)
|
||||
check("M6 andre indstillinger", C.matches(own, {field = 22, width = 30, sig = SIG, settings = C.settingsKey({toolOffset = 2})}), false)
|
||||
check("M7 ukendt form (nil) matcher aldrig", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30, settings = settings}), {field = 22, width = 30, settings = settings}), false)
|
||||
check("M8 intet register", C.matches(nil, request), false)
|
||||
check("M9 key", C.key(request), "22|30.0|" .. SIG .. "|" .. settings)
|
||||
check("M10 key uden form = nil", C.key({field = 22, width = 30, settings = settings}), nil)
|
||||
|
||||
-- ---- grund til ny beregning ----
|
||||
check("R1 intet gemt", C.mismatchReason(nil, request), "intet kursus gemt")
|
||||
check("R2 form ændret", C.mismatchReason(own, {field = 22, width = 30, sig = "x", settings = settings}), "markens form ændret")
|
||||
check("R3 ny bredde", C.mismatchReason(own, {field = 22, width = 12, sig = SIG, settings = settings}), "ny bredde")
|
||||
check("R4 anden mark", C.mismatchReason(own, {field = 9, width = 30, sig = SIG, settings = settings}), "kursus til en anden mark")
|
||||
check("R5 indstillinger", C.mismatchReason(own, {field = 22, width = 30, sig = SIG, settings = "x"}), "andre indstillinger")
|
||||
check("R6 ukendt form", C.mismatchReason(own, {field = 22, width = 30, settings = settings}), "markens form ukendt")
|
||||
|
||||
-- ---- register: immutabelt ----
|
||||
local records = C.withRecord({}, own)
|
||||
check("I1 tilføjet", records.v1 ~= nil, true)
|
||||
local other = C.newRecord({vehicle = "v2", field = 22, width = 30, sig = SIG, settings = settings, waypoints = 812})
|
||||
local both = C.withRecord(records, other)
|
||||
check("I2 original urørt", count(records), 1)
|
||||
check("I3 begge", count(both), 2)
|
||||
check("I4 without", count(C.without(both, "v1")), 1)
|
||||
check("I5 without urørt original", count(both), 2)
|
||||
check("I6 newRecord uden vehicle = nil", C.newRecord({field = 22}), nil)
|
||||
|
||||
-- ---- findReusable ----
|
||||
local rec, how = C.findReusable(both, request, "v1")
|
||||
check("F1 egen maskine først", rec and rec.vehicle, "v1")
|
||||
check("F2 egen = own", how, "own")
|
||||
rec, how = C.findReusable(both, request, "v3")
|
||||
check("F3 anden maskines kursus", rec and rec.vehicle, "v1")
|
||||
check("F4 = copy", how, "copy")
|
||||
-- egen maskines kursus er til en anden bredde -> kopi fra v2
|
||||
local mixed = C.withRecord(both, C.newRecord({vehicle = "v1", field = 22, width = 12, sig = SIG, settings = settings}))
|
||||
rec, how = C.findReusable(mixed, request, "v1")
|
||||
check("F5 egen passer ikke -> kopi fra v2", rec and rec.vehicle, "v2")
|
||||
check("F6 kopi", how, "copy")
|
||||
-- intet passer -> grunden fra egen post
|
||||
local changed = {field = 22, width = 30, sig = "n=6;a=1;c=0,0", settings = settings}
|
||||
rec, how = C.findReusable(both, changed, "v1")
|
||||
check("F7 intet passer", rec, nil)
|
||||
check("F8 grund: form ændret", how, "markens form ændret")
|
||||
rec, how = C.findReusable({}, request, "v1")
|
||||
check("F9 tomt register", how, "intet kursus gemt")
|
||||
-- gyldighedstjek: egen maskines kursus er væk/ændret -> kopi fra v2; alle ugyldige -> ny
|
||||
rec, how = C.findReusable(both, request, "v1", function(r) return r.vehicle ~= "v1" end)
|
||||
check("F10 egen ugyldig -> kopi", rec and rec.vehicle, "v2")
|
||||
rec, how = C.findReusable(both, request, "v1", function() return false end)
|
||||
check("F11 alle ugyldige", rec, nil)
|
||||
check("F12 grund: kurset på maskinen er ændret", how, "kurset på maskinen er ændret")
|
||||
-- multi-tool: lederens kursus (2 sæt, bane -1) genbruges ikke til et enkelt sæt
|
||||
local multi = C.newRecord({vehicle = "v1", field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})})
|
||||
rec = C.findReusable(C.withRecord({}, multi), request, "v1")
|
||||
check("F13 multi-kursus != enkelt", rec, nil)
|
||||
rec, how = C.findReusable(C.withRecord({}, multi), {field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})}, "v1")
|
||||
check("F14 multi samme bane genbruges", how, "own")
|
||||
-- deterministisk: flere kandidater -> laveste id
|
||||
local many = C.withRecord(C.withRecord({}, C.newRecord({vehicle = "v9", field = 22, width = 30, sig = SIG, settings = settings})),
|
||||
C.newRecord({vehicle = "v4", field = 22, width = 30, sig = SIG, settings = settings}))
|
||||
rec = C.findReusable(many, request, "v1")
|
||||
check("F15 deterministisk valg", rec and rec.vehicle, "v4")
|
||||
|
||||
-- ---- invalidateField ----
|
||||
local fieldRecords = C.withRecord(both, C.newRecord({vehicle = "v5", field = 7, width = 30, sig = "n=3;a=50;c=3,3", settings = settings}))
|
||||
local kept, removed = C.invalidateField(fieldRecords, 22, "n=5;a=1;c=0,0")
|
||||
check("V1 to poster for marken slettet", removed, 2)
|
||||
check("V2 anden mark bevaret", kept.v5 ~= nil, true)
|
||||
check("V3 original urørt", count(fieldRecords), 3)
|
||||
kept, removed = C.invalidateField(fieldRecords, 22, SIG)
|
||||
check("V4 samme form: intet slettes", removed, 0)
|
||||
check("V4b samme tabel tilbage", kept, fieldRecords)
|
||||
kept, removed = C.invalidateField(fieldRecords, 22, nil)
|
||||
check("V5 ukendt form: intet slettes", removed, 0)
|
||||
kept, removed = C.invalidateField(nil, 22, SIG)
|
||||
check("V6 nil-register", removed, 0)
|
||||
|
||||
-- ---- lim: prepare/afterStart/checkField med falske spil-objekter ----
|
||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
||||
local saves, logs = 0, {}
|
||||
ADFieldJobs = {state = {courseRecords = {}}, save = function() saves = saves + 1 end,
|
||||
log = function(_, fmt, ...) table.insert(logs, string.format(fmt, ...)) end}
|
||||
ADRuns = {vehicleLabel = function(v) return v.label end}
|
||||
Logging = {warning = function(...) print("WARN", ...) end}
|
||||
local function fakeCourse(n)
|
||||
return {getNumberOfWaypoints = function() return n end}
|
||||
end
|
||||
local function fakeVehicle(id, width, course)
|
||||
return {label = id, course = course,
|
||||
getFieldWorkCourse = function(self) return self.course end,
|
||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return width end}} end}
|
||||
end
|
||||
local tractorA = fakeVehicle("A", 30, nil)
|
||||
local tractorB = fakeVehicle("B", 30, nil)
|
||||
ADRunsController = {getFarmVehicles = function() return {tractorA, tractorB} end,
|
||||
getVehicleId = function(v) return v.label end}
|
||||
local fieldObj, info = {id = 22}, {shapeSig = SIG, fruitName = "WHEAT"}
|
||||
local assignA = {vehicle = tractorA, vehicleId = "A", role = "cultivator"}
|
||||
|
||||
-- G1: intet gemt -> ny beregning, opts urørt (ingen reuse/course)
|
||||
local opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
|
||||
check("G1 ny beregning: ingen reuse", opts.reuse, nil)
|
||||
check("G1b ingen kursus", opts.course, nil)
|
||||
check("G1c log: nyt kursus (intet gemt)", logs[#logs]:find("nyt kursus beregnes", 1, true) ~= nil and logs[#logs]:find("intet kursus gemt", 1, true) ~= nil, true)
|
||||
-- CP har lavet kurset (812 punkter) -> registreres
|
||||
tractorA.course = fakeCourse(812)
|
||||
C.afterStart(tractorA, "A", ctx)
|
||||
check("G2 registreret", ADFieldJobs.state.courseRecords.A ~= nil and ADFieldJobs.state.courseRecords.A.waypoints, 812)
|
||||
check("G2b gemt til fil", saves, 1)
|
||||
-- G3: næste gang -> genbrug
|
||||
opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
|
||||
check("G3 genbrug", opts.reuse, true)
|
||||
check("G3b log genbrugt", logs[#logs]:find("kursus genbrugt", 1, true) ~= nil, true)
|
||||
C.afterStart(tractorA, "A", ctx)
|
||||
check("G3c uændret register gemmes ikke igen", saves, 1)
|
||||
-- G4: B skal samme mark -> kopi fra A
|
||||
local assignB = {vehicle = tractorB, vehicleId = "B", role = "cultivator"}
|
||||
opts, ctx = C.prepare({}, fieldObj, info, assignB, {})
|
||||
check("G4 kopi fra A", opts.course, tractorA.course)
|
||||
check("G4b log kopieret", logs[#logs]:find("kopieret fra 'A'", 1, true) ~= nil, true)
|
||||
tractorB.course = fakeCourse(812)
|
||||
C.afterStart(tractorB, "B", ctx)
|
||||
check("G4c B registreret", ADFieldJobs.state.courseRecords.B ~= nil, true)
|
||||
-- G5: spilleren har lavet et nyt kursus på A i CP's HUD (andet antal punkter) -> kopi fra B
|
||||
tractorA.course = fakeCourse(400)
|
||||
opts = C.prepare({}, fieldObj, info, assignA, {})
|
||||
check("G5 A's kursus ændret -> kopi fra B", opts.course, tractorB.course)
|
||||
-- G6: kursus kopieret fra slåmaskinen (opts.course) -> registret bruges ikke, maskinens post glemmes
|
||||
local mowerCourse = fakeCourse(99)
|
||||
opts, ctx = C.prepare({}, fieldObj, info, assignB, {course = mowerCourse})
|
||||
check("G6 opts urørt", opts.course, mowerCourse)
|
||||
check("G6b ingen kontekst", ctx, nil)
|
||||
tractorB.course = fakeCourse(99)
|
||||
C.afterStart(tractorB, "B", ctx)
|
||||
check("G6c B glemt", ADFieldJobs.state.courseRecords.B, nil)
|
||||
-- G7: vinmark: vin-offset indgår i nøglen
|
||||
local vineReq = C.requestFor(tractorA, 22, {shapeSig = SIG, fruitName = "GRAPE"}, "fertilizer", {})
|
||||
check("G7 vin i nøglen", vineReq.settings, C.settingsKey({vineOffset = -1, vineSkip = 1}))
|
||||
-- G8: markens form ændret -> A's post glemmes, log
|
||||
local before = saves
|
||||
check("G8 checkField sletter", C.checkField(22, "n=9;a=1;c=0,0"), 1)
|
||||
check("G8b register tomt for marken", ADFieldJobs.state.courseRecords.A, nil)
|
||||
check("G8c gemt", saves, before + 1)
|
||||
check("G9 samme form: intet", C.checkField(22, "n=9;a=1;c=0,0"), 0)
|
||||
|
||||
-- G10: fejl i spil-API'et -> falder tilbage til ny beregning (opts urørt), ingen crash
|
||||
local brokenVehicle = {label = "X", getFieldWorkCourse = function() error("boom") end}
|
||||
local plainOpts = {lane = 1}
|
||||
local okPrep, gotOpts, gotCtx = pcall(C.prepare, {}, fieldObj, nil, {vehicle = brokenVehicle, vehicleId = "X", role = "x"}, plainOpts)
|
||||
check("G10 ingen crash", okPrep, true)
|
||||
check("G10b opts urørt", gotOpts, plainOpts)
|
||||
check("G10c ingen kontekst", gotCtx, nil)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -70,6 +70,35 @@ check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
|
||||
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
|
||||
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
|
||||
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
|
||||
-- SR2: tom Bredal-spreder (tager gødning OG kalk) kan begge dele; fyldt med kalk er den primært kalkspreder
|
||||
local bredal = {FERTILIZER = true, LIME = true}
|
||||
local p1, r1, f1 = ADCpBridge.sprayRolesFor(nil, bredal)
|
||||
check("SR2 tom spreder primært gødning", p1, "fertilizer")
|
||||
check("SR2 tom spreder kan kalke", r1 and r1.limer, true)
|
||||
check("SR2 tom = intet fyld", f1, nil)
|
||||
local p2, r2, f2 = ADCpBridge.sprayRolesFor("LIME", bredal)
|
||||
check("SR2 med kalk = kalkspreder", p2, "limer")
|
||||
check("SR2 med kalk kan stadig gøde (efter tømning)", r2 and r2.fertilizer, true)
|
||||
check("SR2 fyldets rolle", f2, "limer")
|
||||
check("SR2 ukrudtssprøjte", (ADCpBridge.sprayRolesFor(nil, {HERBICIDE = true, WATER = true})), "weeder")
|
||||
check("SR2 vandvogn er ingen sprøjte", (ADCpBridge.sprayRolesFor(nil, {WATER = true})), nil)
|
||||
-- SR3: kun en lille rest må hældes ud (100.000 l gødning blev hældt ud in-game 26/9)
|
||||
check("SR3 tom", ADCpBridge.isDumpable(0, 4000), true)
|
||||
check("SR3 lille rest 300 af 4000", ADCpBridge.isDumpable(300, 4000), true)
|
||||
check("SR3 halv tank 2000 af 4000", ADCpBridge.isDumpable(2000, 4000), false)
|
||||
check("SR3 100000 l aldrig", ADCpBridge.isDumpable(100000, 1000000), false)
|
||||
check("SR3 over 2000 l aldrig", ADCpBridge.isDumpable(2500, 100000), false)
|
||||
-- VN: vinmark — i gangen for alt undtagen høsteren; almindelige marker røres ikke
|
||||
check("VN sprøjte på druer = venstre", ADCpBridge.vineOffsetFor("GRAPEB", "fertilizer"), -1)
|
||||
check("VN harve på druer = venstre", ADCpBridge.vineOffsetFor("grape", "cultivator"), -1)
|
||||
check("VN høster på druer = på vinen", ADCpBridge.vineOffsetFor("GRAPEB", "harvester"), 0)
|
||||
check("VN hvede røres ikke", ADCpBridge.vineOffsetFor("WHEAT", "fertilizer"), nil)
|
||||
check("VN ukendt afgrøde", ADCpBridge.vineOffsetFor(nil, "weeder"), nil)
|
||||
check("VN sprøjte springer 1 række over (begge sider)", ADCpBridge.vineSkipFor("GRAPEB", "weeder"), 1)
|
||||
check("VN gødning springer 1 over", ADCpBridge.vineSkipFor("GRAPEB", "fertilizer"), 1)
|
||||
check("VN harve springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "cultivator"), 0)
|
||||
check("VN høster springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "harvester"), 0)
|
||||
check("VN hvede røres ikke", ADCpBridge.vineSkipFor("WHEAT", "weeder"), nil)
|
||||
|
||||
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
|
||||
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
|
||||
@@ -86,5 +115,64 @@ check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
||||
|
||||
-- CR: kursus-genbrug — start på maskinens eget kursus uden at CP beregner et nyt (opts.reuse)
|
||||
do
|
||||
local generated, started = 0, 0
|
||||
AIJobType = {FIELDWORK_CP = 1}
|
||||
function getWorldTranslation() return 0, 0, 0 end
|
||||
local job = {cpJobParameters = {}}
|
||||
function job.applyCurrentState() end
|
||||
function job.setValues() end
|
||||
function job.validate() return true end
|
||||
function job.registerFieldBoundaryDetectionCallback() end
|
||||
function job.detectFieldBoundary() return true, false, "" end
|
||||
function job.onClickGenerateFieldWorkCourse(_, callback) generated = generated + 1; callback(true) end
|
||||
g_currentMission = {aiJobTypeManager = {createJob = function() return job end},
|
||||
aiSystem = {startJob = function() started = started + 1 end}}
|
||||
local course = {}
|
||||
local vehicle = {rootNode = 1, getOwnerFarmId = function() return 1 end, getFieldWorkCourse = function() return course end}
|
||||
local reason = "?"
|
||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
|
||||
check("CR1 genbrug: ingen beregning", generated, 0)
|
||||
check("CR2 genbrug: CP startet", started, 1)
|
||||
check("CR3 genbrug: reason", reason, "reused")
|
||||
-- ingen kursus på maskinen -> beregnes alligevel
|
||||
vehicle.getFieldWorkCourse = function() return nil end
|
||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
|
||||
check("CR4 intet kursus -> beregnes", generated, 1)
|
||||
check("CR5 reason ny", reason, "generated")
|
||||
-- uden reuse: beregnes som før
|
||||
vehicle.getFieldWorkCourse = function() return course end
|
||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {})
|
||||
check("CR6 uden reuse -> beregnes", generated, 2)
|
||||
AIJobType, g_currentMission = nil, nil
|
||||
end
|
||||
|
||||
-- forager: 2 runder og start på forageren (så der ikke køres ind i træer o.l. i hjørnerne)
|
||||
local recorded = {}
|
||||
local function setting(name) return {setValue = function(_, v) recorded[name] = v end} end
|
||||
local headlandVehicle = {getCourseGeneratorSettings = function() return {numberOfHeadlands = setting("headlands"), startOnHeadland = setting("startOnHeadland")} end}
|
||||
check("forager: sat", ADCpBridge.setHeadlands(headlandVehicle, 2), true)
|
||||
check("forager: 2 runder", recorded.headlands, 2)
|
||||
check("forager: start på forageren", recorded.startOnHeadland, true)
|
||||
ADCpBridge.setHeadlands(headlandVehicle, 0)
|
||||
check("forager: ingen -> ikke start på forageren", recorded.startOnHeadland, false)
|
||||
check("forager: køretøj uden CP", ADCpBridge.setHeadlands({}, 2), false)
|
||||
check("forager: standard er 2", ADCpBridge.HEADLANDS, 2)
|
||||
-- genbrugt kursus uden forager beregnes på ny
|
||||
check("forager i kursus: 2 runder", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 2 end}), true)
|
||||
check("forager i kursus: ingen", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 0 end}), false)
|
||||
check("forager i kursus: ukendt -> antag ja", ADCpBridge.courseHasHeadlands({}), true)
|
||||
check("forager i kursus: intet kursus", ADCpBridge.courseHasHeadlands(nil), false)
|
||||
-- isWorking: AutoDrive kører også som AI i FS25 — det er ikke Courseplay (in-game 29/9: Milk Store holdt ved
|
||||
-- Cow Wait med AD aktiv og "ventede på sig selv" som 'Courseplay kører')
|
||||
local function fakeVehicle(cp, ai, ad)
|
||||
return {getIsCpFieldWorkActive = function() return cp end, getIsAIActive = function() return ai end,
|
||||
ad = {stateModule = {isActive = function() return ad end}}}
|
||||
end
|
||||
check("CP arbejder: Courseplay markarbejde", ADCpBridge.isWorking(fakeVehicle(true, true, false)), true)
|
||||
check("CP arbejder: AI uden AD (CP-kørsel)", ADCpBridge.isWorking(fakeVehicle(false, true, false)), true)
|
||||
check("CP arbejder: AutoDrive kører -> ikke CP", ADCpBridge.isWorking(fakeVehicle(false, true, true)), false)
|
||||
check("CP arbejder: intet", ADCpBridge.isWorking(fakeVehicle(false, false, false)), false)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Kør: luajit tests/test_adCpResume.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adCpResume.lua")
|
||||
local R = ADCpResume
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
-- hvem skrives ved gem: CP kører, AD styrer ikke (AD genstarter selv sine)
|
||||
local ids = R.activeIds({{id = "a", cpActive = true}, {id = "b", cpActive = true, adActive = true}, {id = "c"}, {id = "d", cpActive = true}})
|
||||
check("gem: CP-aktive uden AD", table.concat(ids, " "), "a d")
|
||||
check("gem: tom liste", #R.activeIds({}), 0)
|
||||
-- records frem og tilbage
|
||||
local set = R.fromRecords({"a", "", "d", "a"})
|
||||
check("indlæs: a", set.a, true)
|
||||
check("indlæs: tomt id springes over", set[""], nil)
|
||||
-- hvem startes ved indlæsning
|
||||
local pending = {a = true, b = true, c = true, d = true}
|
||||
local vehicles = {{id = "a"}, {id = "b", aiActive = true}, {id = "c", controlled = true}, {id = "d"}, {id = "e"}}
|
||||
local start = R.toStart(pending, vehicles)
|
||||
check("start: kun gemte, ikke allerede kørende/spiller i", table.concat(start, " "), "a d")
|
||||
check("start: ingen gemt liste -> nil (gammel regel bruges)", R.toStart(nil, vehicles), nil)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -82,5 +82,64 @@ check("FL10 grund", whyMany, "notEnough")
|
||||
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
|
||||
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
|
||||
|
||||
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
|
||||
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
|
||||
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
|
||||
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
|
||||
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
|
||||
|
||||
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
|
||||
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
|
||||
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
|
||||
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
|
||||
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
|
||||
|
||||
-- FL-spreder: tom spreder med rollerne {gødning, kalk} vælges til kalkning; spreder med forkert vare
|
||||
-- på (kalk, skal gøde) kun hvis ingen anden kan — og den nærmeste med rigtig/ingen vare først
|
||||
local spreaders = {
|
||||
{id = "kalk", role = "limer", roles = {fertilizer = true, limer = true}, filledRole = "limer", enabled = true, x = 1, z = 0, width = 12},
|
||||
{id = "tom", role = "fertilizer", roles = {fertilizer = true, limer = true}, enabled = true, x = 50, z = 0, width = 12},
|
||||
}
|
||||
check("FLS tom spreder tager kalkning", F.pick({spreaders[2]}, "limer", 0, 0) and F.pick({spreaders[2]}, "limer", 0, 0).id, "tom")
|
||||
check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "fertilizer", 0, 0).id, "tom")
|
||||
check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk")
|
||||
check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk")
|
||||
-- FLS2: fuld af en anden vare (ikke dumpable) bruges slet ikke til opgaven
|
||||
local fullOther = {id = "fuld", role = "fertilizer", roles = {fertilizer = true, weeder = true}, filledRole = "fertilizer", dumpable = false, enabled = true, x = 0, z = 0, width = 30}
|
||||
check("FLS2 fuld gødningssprøjte bruges ikke til ukrudt", F.pick({fullOther}, "weeder", 0, 0), nil)
|
||||
check("FLS2 men gerne til gødning", F.pick({fullOther}, "fertilizer", 0, 0).id, "fuld")
|
||||
|
||||
-- FLD: faste maskiner pr. mark
|
||||
local sprayA = {id = "sprøjte", role = "fertilizer"}
|
||||
local sprayB = {id = "spreder", role = "fertilizer"}
|
||||
local harve = {id = "harve", role = "cultivator"}
|
||||
local all = {sprayA, sprayB, harve}
|
||||
local byField = {[22] = {"sprøjte"}}
|
||||
local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end
|
||||
check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte")
|
||||
check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve")
|
||||
check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve")
|
||||
check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve")
|
||||
local moved = F.dedicate(byField, 109, "sprøjte", true)
|
||||
check("FLD flyt: væk fra F22", #moved[22], 0)
|
||||
check("FLD flyt: fast på F109", moved[109][1], "sprøjte")
|
||||
check("FLD original urørt", byField[22][1], "sprøjte")
|
||||
check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0)
|
||||
|
||||
-- FLV: menuen skjuler maskiner der er faste på en anden mark end den valgte
|
||||
local vrigs = {{id = "vogn"}, {id = "harve"}, {id = "plov"}}
|
||||
local vis, hid, flds = F.visibleForField(vrigs, 18, {[22] = {"vogn", "harve"}})
|
||||
check("FLV F18 ser kun de frie", #vis .. "/" .. hid .. "/" .. tostring(flds[1]), "1/2/22")
|
||||
check("FLV F22 ser alle", #(F.visibleForField(vrigs, 22, {[22] = {"vogn", "harve"}})), 3)
|
||||
check("FLV ingen valgt = alle", #(F.visibleForField(vrigs, nil, {[22] = {"vogn"}})), 3)
|
||||
|
||||
-- FL-plov: FS25-plove har også spec_cultivator — de er plove, men må også tage kultivering
|
||||
check("FLP plov med kultivator-spec er plov", F.roleFromSpecs({spec_plow = true, spec_cultivator = true}), "plow")
|
||||
check("FLP plov kan kultivere", F.rolesFor("plow").cultivator, true)
|
||||
check("FLP plov kan pløje", F.rolesFor("plow").plow, true)
|
||||
check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil)
|
||||
local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3}
|
||||
check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p")
|
||||
check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldGuide.lua")
|
||||
local FG, G = ADFieldGuide, ADGuide
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
|
||||
local RIGS = {
|
||||
{id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
|
||||
{id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
|
||||
{id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
|
||||
{id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
|
||||
}
|
||||
local function ctx(info, extra)
|
||||
local context = {
|
||||
info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
|
||||
rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
|
||||
stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
|
||||
fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
|
||||
}
|
||||
for key, value in pairs(extra or {}) do context[key] = value end
|
||||
return context
|
||||
end
|
||||
local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
|
||||
local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
|
||||
local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
|
||||
local BARE = {id = 4, kind = "bare", markerId = 8}
|
||||
|
||||
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
|
||||
local function field(id, plan)
|
||||
local f = ADFieldModel.newField(id)
|
||||
return ADFieldModel.with(f, {plan = plan or {}})
|
||||
end
|
||||
local function findRow(rows, id, item)
|
||||
for _, row in ipairs(rows) do
|
||||
if row.id == id and (item == nil or row.item == item) then return row end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function countType(rows, rowType)
|
||||
local n = 0
|
||||
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
|
||||
return n
|
||||
end
|
||||
|
||||
-- ---- synlige trin pr. marktype ----
|
||||
check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
|
||||
check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
|
||||
check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
|
||||
check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
|
||||
check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
|
||||
check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
|
||||
check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
|
||||
check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
|
||||
check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
|
||||
local picker = FG.openPicker(ctx(GRASS))
|
||||
check("FG1 vælger: kun mark", steps(picker), "field")
|
||||
check("FG1 vælger: trin", picker.step, "field")
|
||||
check("FG1 vælger: position uden total", (G.position(picker)), 1)
|
||||
|
||||
-- ---- standarder og udfyldning ----
|
||||
local g = FG.open(field(2), ctx(WHEAT))
|
||||
check("FG2 starter på afgrøde", g.step, "crop")
|
||||
check("FG2 klargøring auto", g.draft.prepAuto, true)
|
||||
check("FG2 ingen faste", g.draft.useDedicated, false)
|
||||
check("FG2 lager auto", g.draft.storeMode, "auto")
|
||||
check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
|
||||
local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
|
||||
check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
|
||||
local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
|
||||
check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
|
||||
local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
|
||||
check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
|
||||
local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
|
||||
check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
|
||||
check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
|
||||
|
||||
-- ---- choose er immutabel ----
|
||||
local chosen = FG.choose(g, "targetFruit", "BARLEY")
|
||||
check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
|
||||
check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
|
||||
local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
|
||||
check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
|
||||
check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
|
||||
check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
|
||||
check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
|
||||
check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
|
||||
|
||||
-- ---- rækker ----
|
||||
local cropRows = FG.rows(g)
|
||||
local target = findRow(cropRows, "targetFruit")
|
||||
check("FG4 afgrøde: valg", target.type, "choice")
|
||||
check("FG4 afgrøde: behold først", target.options[1].value, "")
|
||||
check("FG4 afgrøde: såbare", #target.options, 5)
|
||||
check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
|
||||
check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
|
||||
check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
|
||||
|
||||
local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
|
||||
check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
|
||||
check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
|
||||
check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
|
||||
local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
|
||||
check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
|
||||
check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
|
||||
|
||||
local prepRows = FG.rows(G.jump(g, "prep", "close"))
|
||||
check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
|
||||
check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
|
||||
local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
|
||||
check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
|
||||
check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
|
||||
check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
|
||||
check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
|
||||
|
||||
-- pleje: Automatisk/Manuelt som klargøring (brugerønske 29/9); knapperne vises under Manuelt
|
||||
local careAutoRows = FG.rows(G.jump(g, "care", "close"))
|
||||
check("FG7 pleje: valg auto/manuel", findRow(careAutoRows, "careAuto").type, "choice")
|
||||
check("FG7 pleje auto: ingen knapper", findRow(careAutoRows, "fertilize"), nil)
|
||||
check("FG7 standard: pleje auto", g.draft.careAuto, true)
|
||||
check("FG7 uden gødning -> manuel", FG.open(field(2, {fertilize = false}), ctx(WHEAT)).draft.careAuto, false)
|
||||
local autoCare = FG.result(FG.choose(FG.open(field(2, {fertilize = false, weed = false}), ctx(WHEAT)), "careAuto", true), true).plan
|
||||
check("FG7 auto slår gødning til", autoCare.fertilize, true)
|
||||
check("FG7 auto slår ukrudt til", autoCare.weed, true)
|
||||
local careGrass = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "careAuto", false), "care", "close"))
|
||||
check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
|
||||
check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
|
||||
local careWheat = FG.rows(G.jump(FG.choose(g, "careAuto", false), "care", "close"))
|
||||
check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
|
||||
check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
|
||||
|
||||
local machinesOff = FG.rows(G.jump(g, "machines", "close"))
|
||||
check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
|
||||
check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
|
||||
check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
|
||||
check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
|
||||
local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
|
||||
check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
|
||||
check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
|
||||
local hiddenText = nil
|
||||
for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
|
||||
check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
|
||||
check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
|
||||
local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
|
||||
local firstCheck = nil
|
||||
for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
|
||||
check("FG8 egne først", firstCheck.item, "v2")
|
||||
check("FG8 egne sat", firstCheck.value, true)
|
||||
|
||||
local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
|
||||
check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
|
||||
check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
|
||||
check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
|
||||
local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
|
||||
check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
|
||||
local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
|
||||
local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
|
||||
check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
|
||||
local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
|
||||
check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
|
||||
|
||||
local pickRows = FG.rows(picker)
|
||||
check("FG10 vælger: links", countType(pickRows, "link"), 2)
|
||||
check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
|
||||
local picked = FG.pickField(picker, field(2), ctx(WHEAT))
|
||||
check("FG10 valgt mark", picked.draft.fieldId, 2)
|
||||
check("FG10 videre til afgrøde", picked.step, "crop")
|
||||
check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
|
||||
|
||||
-- ---- resultat ----
|
||||
local r = FG.result(g, true)
|
||||
check("FG11 auto ved start", r.plan.mode, "auto")
|
||||
check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
|
||||
local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
|
||||
check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
|
||||
check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
|
||||
check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
|
||||
check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
|
||||
check("FG11 mål uændret", r.targetChanged, false)
|
||||
check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
|
||||
local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
|
||||
check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
|
||||
local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
|
||||
check("FG12 auto: jord", autoPrep.soil, "auto")
|
||||
check("FG12 auto: mulch", autoPrep.mulch, false)
|
||||
check("FG12 auto: kalk", autoPrep.lime, true)
|
||||
check("FG12 auto: tromle", autoPrep.roll, false)
|
||||
local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
|
||||
check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
|
||||
check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
|
||||
check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
|
||||
check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
|
||||
check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
|
||||
check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
|
||||
check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
|
||||
|
||||
-- ---- faste maskiner på tværs af marker ----
|
||||
local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
|
||||
check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
|
||||
check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
|
||||
check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
|
||||
|
||||
-- ---- resumé ----
|
||||
local function lineFor(lines, step)
|
||||
for _, line in ipairs(lines) do if line.step == step then return line end end
|
||||
return nil
|
||||
end
|
||||
local function partKeys(line)
|
||||
local keys = {}
|
||||
for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
|
||||
return table.concat(keys, ",")
|
||||
end
|
||||
local sum = FG.summary(g)
|
||||
check("FG16 linjer for synlige trin", #sum, 6)
|
||||
check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
|
||||
check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
|
||||
check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
|
||||
check("FG16 pleje auto", partKeys(lineFor(sum, "care")), "spu_guide_sum_auto")
|
||||
check("FG16 pleje manuel", partKeys(lineFor(FG.summary(FG.choose(g, "careAuto", false)), "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
|
||||
check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
|
||||
check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
|
||||
check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
|
||||
local sumTarget = FG.summary(chosen)
|
||||
check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
|
||||
check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
|
||||
local sumRigs = FG.summary(withRigs)
|
||||
check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
|
||||
check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
|
||||
local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
|
||||
check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
|
||||
check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
|
||||
check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
|
||||
local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
|
||||
check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
|
||||
local sumNoCare = FG.summary(FG.choose(FG.choose(FG.choose(g, "careAuto", false), "fertilize", false), "weed", false))
|
||||
check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
|
||||
local sumSow = FG.summary(FG.choose(g, "sow", true))
|
||||
check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
|
||||
|
||||
-- ---- oversigts-trinnet: links pr. linje ----
|
||||
local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
|
||||
check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
|
||||
check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -52,6 +52,9 @@ local withHarvester = {{phase = "working", stage = "harvesting", helper = false}
|
||||
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
||||
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
||||
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
||||
-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn
|
||||
local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}}
|
||||
check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none")
|
||||
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
||||
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
||||
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
||||
@@ -89,5 +92,131 @@ local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
||||
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
||||
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
||||
|
||||
-- J12: bar mark med mål-afgrøde klargøres straks — kun selve såningen venter på såtiden
|
||||
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
|
||||
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
|
||||
local canSow = {plantable = function(name) return name == "WHEAT" end}
|
||||
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
|
||||
check("J12 bar + ikke såtid -> start alligevel (klargøring)", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "start")
|
||||
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
|
||||
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
|
||||
|
||||
-- J13: afbrudt slåning efter genindlæsning (in-game 27/9 F90): genvækst i mellemhøjde -> vent; hjørne efter en tur -> slå straks
|
||||
local regrowing = {id = 90, markerId = 9, mowable = true, readyRatio = 0.5, immatureRatio = 0.5}
|
||||
local dRegrow = J.decide(mowing, regrowing, {}, 0)
|
||||
check("J13 genvækst -> vent", dRegrow.action, "wait"); check("J13 grund", dRegrow.reason, "regrowing")
|
||||
check("J13 genvækst men tur færdig -> slå hjørnet", J.decide(mowing, regrowing, {{phase = "finished"}}, 0).action, "dispatch")
|
||||
check("J13 halvt slået uden genvækst -> slå resten", J.decide(mowing, {id = 90, markerId = 9, mowable = true, readyRatio = 0.6, immatureRatio = 0}, {}, 0).action, "dispatch")
|
||||
|
||||
-- J14: kultivering på en mark med uhøstede kartofler (in-game 27/9 F16) -> tilbage til klar; rest efter høst -> ikke
|
||||
local cultivatingField = M.with(auto, {stage = "cultivating", kind = "harvest"})
|
||||
check("J14 uhøstet -> spol tilbage", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 1}, {}, 0).action, "rewind")
|
||||
check("J14 høstrest 30 % -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 0.3}, {}, 0).action ~= "rewind", true)
|
||||
check("J14 stub -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "bare", phase = "cut", readyRatio = 0}, {}, 0).action ~= "rewind", true)
|
||||
|
||||
|
||||
-- resumeField: efter genindlæsning stopper spillet alle arbejdere; en CP-maskine der står PÅ en mark hvis
|
||||
-- aktuelle trin passer til maskinens rolle, meldes ind på den mark igen (in-game 27/9)
|
||||
do
|
||||
local square = {{x = 0, z = 0}, {x = 100, z = 0}, {x = 100, z = 100}, {x = 0, z = 100}}
|
||||
local fields = {
|
||||
{id = 90, stage = "collecting", verts = square},
|
||||
{id = 111, stage = "harvesting", verts = {{x = 200, z = 0}, {x = 300, z = 0}, {x = 300, z = 100}, {x = 200, z = 100}}},
|
||||
}
|
||||
check("RF høster på F111", ADFieldJobs.resumeField(250, 50, "harvester", fields), 111)
|
||||
check("RF læssevogn på F90", ADFieldJobs.resumeField(50, 50, "collector", fields), 90)
|
||||
check("RF forkert rolle til trinnet -> nil", ADFieldJobs.resumeField(250, 50, "collector", fields), nil)
|
||||
check("RF uden for alle marker -> nil", ADFieldJobs.resumeField(150, 50, "harvester", fields), nil)
|
||||
check("RF mark uden omrids -> nil", ADFieldJobs.resumeField(50, 50, "collector", {{id = 1, stage = "collecting", verts = {}}}), nil)
|
||||
check("RF på kanten tæller som indenfor", ADFieldJobs.resumeField(0.5, 50, "collector", fields), 90)
|
||||
end
|
||||
|
||||
|
||||
-- startAt: hvor Courseplay skal starte i kurset (CP's standard er "first" = forfra, også på en halvt høstet mark;
|
||||
-- in-game 27/9: mejetærskeren startede F111 forfra)
|
||||
check("SA eget kursus med husket waypoint -> last", ADFieldJobs.startAtFor({reuse = true, remembered = true, stage = "harvesting", standingRatio = 0.4}), "last")
|
||||
check("SA halvt høstet -> nearest", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.4}), "nearest")
|
||||
check("SA halvt slået -> nearest", ADFieldJobs.startAtFor({stage = "mowing", standingRatio = 0.5}), "nearest")
|
||||
check("SA urørt mark -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 1}), "first")
|
||||
check("SA næsten urørt (97 %) -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.97}), "first")
|
||||
check("SA ukendt fremdrift -> first", ADFieldJobs.startAtFor({stage = "harvesting"}), "first")
|
||||
check("SA klargøring (ingen afgrøde at måle) -> first", ADFieldJobs.startAtFor({stage = "cultivating", standingRatio = 0.3}), "first")
|
||||
check("SA husket waypoint uden eget kursus -> ikke last", ADFieldJobs.startAtFor({reuse = false, remembered = true, stage = "cultivating"}), "first")
|
||||
|
||||
|
||||
-- shouldResume: kun maskiner der stadig er midt i jobbet genstartes (in-game 27/9: to ROPA'er med et husket
|
||||
-- waypoint fra et gammelt job kørte ud ad vejene mod det gamle kursus)
|
||||
check("SR står på en mark i gang med sit trin -> ja", ADFieldJobs.shouldResume({fieldId = 111, distance = 400}), true)
|
||||
check("SR lige ved sit huskede waypoint -> ja", ADFieldJobs.shouldResume({distance = 12}), true)
|
||||
check("SR langt fra waypointet og ingen mark -> nej", ADFieldJobs.shouldResume({distance = 850}), false)
|
||||
check("SR ukendt afstand og ingen mark -> nej", ADFieldJobs.shouldResume({}), false)
|
||||
|
||||
|
||||
-- mayDispatch: markarbejdet sender højst én maskine ud pr. DISPATCH_GAP_MS på tværs af alle marker
|
||||
-- (in-game 27/9: 6 maskiner kørte fra parkeringen i samme sekund efter Start alle)
|
||||
check("MD første udsendelse", ADFieldJobs.mayDispatch(5000, nil), true)
|
||||
check("MD for tæt på forrige", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS - 1, 5000), false)
|
||||
check("MD efter mellemrummet", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS, 5000), true)
|
||||
|
||||
|
||||
-- halvt høstet mark (in-game 27/9 F111): høsten er ikke færdig før næsten intet står; en mark i klargøring med
|
||||
-- stående afgrøde spoles tilbage
|
||||
do
|
||||
local halfInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.45, standingRatio = 0.45, fruitIndex = 1}
|
||||
check("HH halvt høstet uden høster -> send høster (ikke videre)", J.decide(harvesting, halfInfo, {}, 0).action, "dispatch")
|
||||
local doneInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.02, standingRatio = 0.02, fruitIndex = 1}
|
||||
check("HH næsten intet står -> videre", J.decide(harvesting, doneInfo, {}, 0).action, "advance")
|
||||
local cultivating = M.with(harvesting, {stage = "cultivating"})
|
||||
check("HH klargøring med halv mark stående -> spol tilbage", J.decide(cultivating, halfInfo, {}, 0).action, "rewind")
|
||||
end
|
||||
|
||||
|
||||
-- startTimedOut: CP må beregne kursus i op til 5 min (in-game 27/9 F90: 140 ha tog over 60 s, moden opgav midt i)
|
||||
check("ST start uden beregning > 60 s -> opgiv", ADFieldJobs.startTimedOut(61000, false), true)
|
||||
check("ST beregner stadig efter 90 s -> vent", ADFieldJobs.startTimedOut(90000, true), false)
|
||||
check("ST beregner over 5 min -> opgiv", ADFieldJobs.startTimedOut(301000, true), true)
|
||||
check("ST under 60 s -> vent", ADFieldJobs.startTimedOut(30000, false), false)
|
||||
|
||||
|
||||
-- shouldEmptyHarvester: høsten er færdig, men høsteren har korn i tanken og står ved marken -> tøm først
|
||||
-- (in-game 27/9 F15: marken gik til halm med fuld tank, ingen tømmevogn kom)
|
||||
check("EH korn i tanken ved marken -> tøm", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true}), true)
|
||||
check("EH tom tank -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 0, near = true}), false)
|
||||
check("EH langt væk -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = false}), false)
|
||||
check("EH optaget -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, busy = true}), false)
|
||||
check("EH spiller i -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, controlled = true}), false)
|
||||
check("EH allerede forsøgt -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, tried = true}), false)
|
||||
|
||||
|
||||
-- nearestPoint: CP bruger markens punkt som kursets startsted — tag det nærmest traktoren (in-game 27/9 F17: første
|
||||
-- punkt lå 280 m væk i den anden ende, og CP kørte gennem skoven)
|
||||
do
|
||||
local pts = {{x = 1600, z = -1839}, {x = 1350, z = -1740}, {x = 1500, z = -1800}}
|
||||
local p = ADFieldJobs.nearestPoint(pts, 1340, -1733)
|
||||
check("NP nærmest traktoren", p.x .. "," .. p.z, "1350,-1740")
|
||||
check("NP uden punkter -> nil", ADFieldJobs.nearestPoint({}, 0, 0), nil)
|
||||
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
|
||||
end
|
||||
|
||||
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
|
||||
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
|
||||
-- JP: klargøring der allerede er gjort (fx efter genindlæsning) går videre uden at sende maskinen ud igen
|
||||
-- (in-game 28/9: F17 blev kultiveret 3 gange — opgaver gemmes ikke, så "færdig" blev aldrig set)
|
||||
local cultField = M.with(M.newField(17), {stage = "cultivating", kind = "bare", plan = {mode = "auto"}})
|
||||
local cultInfo = {id = 17, markerId = 5, kind = "bare", readyRatio = 0, needs = {}}
|
||||
cultInfo.prepared = 0.98
|
||||
check("JP kultiveret -> videre", J.decide(cultField, cultInfo, {}, 0).action, "advance")
|
||||
cultInfo.prepared = 0.4
|
||||
check("JP halvt kultiveret -> send maskine", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
|
||||
cultInfo.prepared = nil
|
||||
check("JP ukendt jord -> som før (send)", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
|
||||
cultInfo.prepared = 0.98
|
||||
check("JP kultivator i gang -> vent på den", J.decide(cultField, cultInfo, {{phase = "working", stage = "cultivating"}}, 0).action, "none")
|
||||
local plowField = M.with(cultField, {stage = "plowing"})
|
||||
check("JP kultiveret er ikke pløjet -> send plov", J.decide(plowField, cultInfo, {}, 0).action, "dispatch")
|
||||
cultInfo.plowed = 0.97
|
||||
check("JP pløjet -> videre", J.decide(plowField, cultInfo, {}, 0).action, "advance")
|
||||
local sowField = M.with(cultField, {stage = "sowing"})
|
||||
check("JP såning berøres ikke af klargjort jord", J.decide(sowField, cultInfo, {}, 0).action ~= "advance", true)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -67,6 +67,14 @@ check("M7 maks slå", M.maxRigs("mowing"), 1)
|
||||
local okWide, whyWide = M.checkWidth("mower", 60, nil)
|
||||
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
|
||||
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
|
||||
-- M7b: bredde under 1,5 m er ikke læst rigtigt (spreder gav 0,5 m -> CP-kursus hver halve meter på 72 ha
|
||||
-- -> spillet frøs, in-game 26/9) — sendes aldrig ud
|
||||
local okTiny, whyTiny = M.checkWidth("fertilizer", 0.5, nil)
|
||||
check("M7b 0,5 m afvises", okTiny, false)
|
||||
check("M7b grund", whyTiny, "widthUnknown")
|
||||
check("M7b 3 m strigle ok", (M.checkWidth("weeder", 3, nil)), true)
|
||||
check("M7b aflæsningsvogn uden bredde ok", (M.checkWidth("unloader", 0, nil)), true)
|
||||
check("M7b ballesamler uden bredde ok", (M.checkWidth("baleCollector", 0, nil)), true)
|
||||
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
|
||||
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
|
||||
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
|
||||
@@ -82,7 +90,7 @@ check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
|
||||
|
||||
-- M8: gem/indlæs bevarer trin, plan og slåbredde
|
||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
|
||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}}
|
||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}}
|
||||
local back = M.fromRecord(M.toRecord(saved))
|
||||
check("M8 id", back.id, 109)
|
||||
check("M8 stage", back.stage, saved.stage)
|
||||
@@ -90,6 +98,7 @@ check("M8 mode", back.plan.mode, "auto")
|
||||
check("M8 product", back.plan.product, "hay")
|
||||
check("M8 rake", back.plan.rake, true)
|
||||
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
|
||||
check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2")
|
||||
check("M8 slåbredde", back.mowWidth, 40.8)
|
||||
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
|
||||
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
|
||||
@@ -234,5 +243,82 @@ check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(
|
||||
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
||||
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
||||
|
||||
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
|
||||
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
|
||||
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
|
||||
check("M19 mål -> auto", wheatNow.mode, "auto")
|
||||
check("M19 høst byg -> så hvede", wheatNow.sow, true)
|
||||
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
|
||||
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
|
||||
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
|
||||
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
|
||||
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
|
||||
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
|
||||
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
|
||||
local bareTarget = M.applyTarget(target.plan, "bare", nil)
|
||||
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
|
||||
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
|
||||
local started = M.start(target, "harvest", {}, "BARLEY")
|
||||
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
|
||||
check("M19 start: høst først", started.stage, "harvesting")
|
||||
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
|
||||
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
|
||||
-- M20: skal en bar mark med mål starte? (altid — kun såningen venter på såtiden)
|
||||
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
|
||||
check("M20 bar + ikke såtid -> klargør alligevel (kun såningen venter)", M.shouldStartBare(target, "bare"), true)
|
||||
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
|
||||
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
|
||||
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
|
||||
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
|
||||
|
||||
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
|
||||
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
|
||||
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
|
||||
check("M21 færdig -> venter", chosen.stage, "idle")
|
||||
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
|
||||
local cleared = M.setTarget(chosen, "")
|
||||
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
|
||||
|
||||
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
|
||||
local strawBase = {soil = "none", sow = false, lime = false}
|
||||
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
|
||||
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
|
||||
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
|
||||
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
|
||||
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
|
||||
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
|
||||
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
|
||||
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
|
||||
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
|
||||
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
|
||||
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
|
||||
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
|
||||
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
|
||||
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
|
||||
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
|
||||
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
|
||||
|
||||
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
|
||||
local calm = M.newField(95)
|
||||
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
|
||||
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
|
||||
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
|
||||
check("M24 manuel venter = passiv", M.isPassive(calm), true)
|
||||
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
|
||||
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
|
||||
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
|
||||
check("M24 i gang = ikke passiv", M.isPassive(started), false)
|
||||
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
|
||||
|
||||
-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper
|
||||
local stoppedBare = M.stop(M.with(target, {stage = "cultivating"}))
|
||||
check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false)
|
||||
check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true)
|
||||
check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true)
|
||||
check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil)
|
||||
-- gemt produkt (silage/hø) gælder kun slåafgrøder: en vinmark der engang fejlagtigt blev "slåbar" (F22, 29/9)
|
||||
check("produkt: gemt silage på høstmark -> høst", ADFieldModel.product({product = "silage"}, "harvest"), "harvest")
|
||||
check("produkt: gemt hø på slåmark -> hø", ADFieldModel.product({product = "hay"}, "mowable"), "hay")
|
||||
check("produkt: auto på slåmark -> silage", ADFieldModel.product({product = "auto"}, "mowable"), "silage")
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
||||
local C = ADFieldSiloConfig
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local cfg = C.new()
|
||||
check("standard fra", cfg.enabled, false)
|
||||
check("standard x=2", cfg.maxVehicles, 2)
|
||||
check("standard tærskel 10000", cfg.minLiters, 10000)
|
||||
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
|
||||
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
|
||||
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
|
||||
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
|
||||
check("with muterer ikke", cfg.maxVehicles, 2)
|
||||
local on = C.toggleStore(cfg, "silo1")
|
||||
check("toggle til", C.hasStore(on, "silo1"), true)
|
||||
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
|
||||
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
|
||||
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
|
||||
local back = C.fromRecord(C.toRecord(full))
|
||||
check("record: enabled", back.enabled, true)
|
||||
check("record: lagre", table.concat(back.storeIds, " "), "a b")
|
||||
check("record: x", back.maxVehicles, 4)
|
||||
check("record: tærskel", back.minLiters, 25000)
|
||||
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
|
||||
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
|
||||
-- kør til produktion + foderreserve
|
||||
check("standard: ikke til produktion", cfg.toProduction, false)
|
||||
check("standard foderreserve 7 døgn", cfg.reserveDays, 7)
|
||||
check("reserve snappes (10 -> 7)", C.with(cfg, {reserveDays = 10}).reserveDays, 7)
|
||||
check("reserve 40 -> 30", C.with(cfg, {reserveDays = 40}).reserveDays, 30)
|
||||
local prod = C.with(C.toggleStore(cfg, "a"), {toProduction = true, reserveDays = 14})
|
||||
check("with bevarer lagre", C.hasStore(prod, "a"), true)
|
||||
check("with muterer ikke (produktion)", cfg.toProduction, false)
|
||||
local prodBack = C.fromRecord(C.toRecord(prod))
|
||||
check("record: til produktion", prodBack.toProduction, true)
|
||||
check("record: foderreserve", prodBack.reserveDays, 14)
|
||||
check("gammel record uden felter -> standard", C.fromRecord({enabled = 1}).reserveDays, 7)
|
||||
check("toggleStore bevarer produktion", C.toggleStore(prod, "b").toProduction, true)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,159 @@
|
||||
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
||||
local P = ADFieldSiloPlanner
|
||||
local WHEAT, CANOLA = 1, 2
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
|
||||
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
|
||||
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
|
||||
local function base(over)
|
||||
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
|
||||
for k, v in pairs(over or {}) do input[k] = v end
|
||||
return input
|
||||
end
|
||||
|
||||
-- kvalificering
|
||||
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
|
||||
check("under tærskel -> ingen status", #r.status, 0)
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("uden markør -> noMarker", r.status[1].state, "noMarker")
|
||||
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
|
||||
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
|
||||
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
|
||||
|
||||
-- afsendelse, fuldeste først, x-loft
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
|
||||
check("x=2 -> to afsendt", #r.dispatch, 2)
|
||||
check("fuldeste silo først", r.dispatch[1].siloId, "B")
|
||||
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
|
||||
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
|
||||
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
|
||||
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
|
||||
check("anden tur = resten", r.dispatch[2].liters, 30000)
|
||||
|
||||
-- aktive lån tæller mod x og mod siloens rest
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
|
||||
check("status viser på vej", r.status[1].onWay, 50000)
|
||||
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
|
||||
check("frie pladser 0", r.freeSlots, 0)
|
||||
|
||||
-- flere varer i én silo = hver sin opgave
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
|
||||
check("to varer -> to afsendelser", #r.dispatch, 2)
|
||||
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
|
||||
|
||||
-- vogne der ikke kan
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
|
||||
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
|
||||
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
|
||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
|
||||
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
|
||||
|
||||
-- lagervalg
|
||||
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
|
||||
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
|
||||
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
|
||||
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
|
||||
|
||||
-- input muteres ikke
|
||||
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
|
||||
P.plan(input)
|
||||
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
|
||||
|
||||
|
||||
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
|
||||
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
|
||||
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
|
||||
|
||||
-- kør til produktion: foder først, overskud til fabrik
|
||||
local function factory(id, x, free) return {id = id, kind = "production", markerId = id .. "m", x = x, z = 0, free = free} end
|
||||
local farmAndFactory = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}
|
||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 20000}}))
|
||||
check("P1 reserve ikke dækket -> gårdlager selv om fabrik er nærmere", r.dispatch[1].storeId, "farm")
|
||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 150000}}))
|
||||
check("P2 reserve dækket -> fabrik", r.dispatch[1].storeId, "mill")
|
||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("P3 ikke foder (ingen reserve) -> fabrik", r.dispatch[1].storeId, "mill")
|
||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 100000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)},
|
||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 70000}}))
|
||||
check("P4 første læs dækker resten af reserven -> gård", r.dispatch[1].storeId, "farm")
|
||||
check("P4 andet læs er overskud -> fabrik", r.dispatch[2].storeId, "mill")
|
||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 60000}, loans = {{siloId = "B", fillType = WHEAT, liters = 50000, storeId = "farm"}}}))
|
||||
check("P5 læs på vej til gården tæller med i reserven -> fabrik", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "mill")
|
||||
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
vehicles = {vehicle("v1", 400, 50000)}, farmStock = {[WHEAT] = 999999}}))
|
||||
check("P6 ingen fabrik tager varen -> gårdlager", r.dispatch[1].storeId, "farm")
|
||||
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
vehicles = {vehicle("v1", 400, 50000)}, feedReserve = {[WHEAT] = 100000}}))
|
||||
check("P7 reserve mangler men ingen gård tager varen -> fabrik", r.dispatch[1].storeId, "mill")
|
||||
check("P7 status ok (ikke noFarmStore)", r.status[1].state, "ok")
|
||||
check("P7 status viser destination", r.status[1].toProduction, true)
|
||||
|
||||
-- fabrik kun når hele læsset kan være der (in-game 28/9: vogn med 252.000 l ventede timevis ved en fuld fabrik)
|
||||
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("P8 fabrik uden plads til hele læsset -> gårdlager", r.dispatch[1].storeId, "farm")
|
||||
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||
vehicles = {vehicle("v1", 400, 50000)}}))
|
||||
check("P9 kun fabrik uden plads til hele læsset -> ingen afsendelse", #r.dispatch, 0)
|
||||
|
||||
-- høsten først: tømmevogne holdes tilbage til høstere der arbejder
|
||||
local vs = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000, {[CANOLA] = true}), vehicle("v3", 400, 50000)}
|
||||
local left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 1}})
|
||||
check("R1 én hvede-vogn holdt tilbage", #left, 2)
|
||||
check("R1 raps-vognen er fri", left[1].id == "v2" or left[2].id == "v2", true)
|
||||
left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 5}})
|
||||
check("R2 alle hvede-vogne holdt tilbage", #left, 1)
|
||||
check("R3 intet behov -> alle fri", #P.withoutHarvestReserve(vs, {}), 3)
|
||||
check("R4 input muteres ikke", #vs, 3)
|
||||
|
||||
-- Tom silo: lånt vogn med last står ved en tom marksilo -> resten køres videre (AD venter ellers til fuld)
|
||||
-- (in-game 28/9: Tømmevogn 2 og 4 stod ved tom F17 Unload med kartofler)
|
||||
local restState = {atSilo = true, stillMs = 45000, siloStock = 0, cargo = 12000}
|
||||
check("RS1 tom silo med last -> kør videre", P.restDue(restState), true)
|
||||
check("RS2 siloen har stadig varer -> vent", P.restDue({atSilo = true, stillMs = 45000, siloStock = 30000, cargo = 12000}), false)
|
||||
check("RS3 ikke ved siloen -> nej", P.restDue({atSilo = false, stillMs = 45000, siloStock = 0, cargo = 12000}), false)
|
||||
check("RS4 lige ankommet -> vent", P.restDue({atSilo = true, stillMs = 5000, siloStock = 0, cargo = 12000}), false)
|
||||
check("RS5 tom vogn -> nej", P.restDue({atSilo = true, stillMs = 45000, siloStock = 0, cargo = 0}), false)
|
||||
local restStores = {
|
||||
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 5000}},
|
||||
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
|
||||
}
|
||||
check("RS6 fabrik uden plads til hele resten -> gårdsilo", P.restStore(restStores, {x = 0, z = 0}, 10, 12000).id, "gaard")
|
||||
check("RS7 fabrik med plads -> fabrik", P.restStore(restStores, {x = 0, z = 0}, 10, 4000).id, "fab")
|
||||
check("RS8 intet tager varen", P.restStore(restStores, {x = 0, z = 0}, 99, 4000), nil)
|
||||
-- Fuld fabrik: lånt vogn med last holder ved et lager uden plads -> resten til et andet lager (gård)
|
||||
check("SF1 fuld fabrik -> videre", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 0}), true)
|
||||
check("SF2 fabrikken tager stadig imod -> vent", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 8000}), false)
|
||||
check("SF3 lige ankommet -> vent", P.storeFullDue({atStore = true, stillMs = 20000, cargo = 12000, storeFree = 0}), false)
|
||||
check("SF4 ikke ved lageret -> nej", P.storeFullDue({atStore = false, stillMs = 70000, cargo = 12000, storeFree = 0}), false)
|
||||
local fullStores = {
|
||||
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 0}},
|
||||
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
|
||||
}
|
||||
check("SF5 andet lager end det fulde", P.restStore(fullStores, {x = 0, z = 0}, 10, 12000, "fab").id, "gaard")
|
||||
check("SF6 kun det fulde tager varen -> intet", P.restStore({fullStores[1]}, {x = 0, z = 0}, 10, 12000, "fab"), nil)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,159 @@
|
||||
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
|
||||
local S = ADFieldSilos
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
|
||||
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
|
||||
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
|
||||
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
|
||||
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
|
||||
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
|
||||
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
|
||||
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
|
||||
|
||||
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
|
||||
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
|
||||
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
|
||||
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
|
||||
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
|
||||
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
|
||||
|
||||
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
|
||||
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
|
||||
do
|
||||
local WHEAT = 1
|
||||
Logging = {info = function() end, warning = function() end}
|
||||
g_currentMission = {getFarmId = function() return 1 end}
|
||||
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
|
||||
function getWorldTranslation(node) return node.x, 0, node.z end
|
||||
ADRuns = {vehicleLabel = function(v) return v.name end}
|
||||
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
|
||||
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
||||
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
|
||||
ADBuildings = {list = function() return {fieldSilo, farm} end,
|
||||
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
|
||||
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
|
||||
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
|
||||
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
|
||||
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
|
||||
local function makeVehicle(name, x)
|
||||
local module = {active = false}
|
||||
function module:setMode() end
|
||||
function module:setFirstMarkerByWayPointId(id) self.first = id end
|
||||
function module:setSecondMarkerByWayPointId(id) self.second = id end
|
||||
function module:setFillType() end
|
||||
function module:setLoopsDone() end
|
||||
function module:raiseDirtyFlag() end
|
||||
function module:isActive() return self.active end
|
||||
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
|
||||
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
|
||||
end
|
||||
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
|
||||
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
|
||||
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
|
||||
getFillUnitCapacity = function() return 40000 end,
|
||||
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
|
||||
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
|
||||
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
|
||||
ADFieldWork = {
|
||||
getRigs = function()
|
||||
local rigs = {}
|
||||
-- som spillets vehicleSystem: slettede køretøjer er ikke med
|
||||
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
|
||||
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
|
||||
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
|
||||
return rigs
|
||||
end,
|
||||
enableParkAtJobFinished = function() end,
|
||||
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
|
||||
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
|
||||
}
|
||||
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
|
||||
local plan = S.planNow(adEnv)
|
||||
check("RØG én afsendelse (x=1)", #started, 1)
|
||||
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
|
||||
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
|
||||
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
|
||||
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
|
||||
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
|
||||
check("RØG vognen er lånt", S.isLoaned(near), true)
|
||||
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
|
||||
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||
check("RØG frakobling stopper AD", near.stopped, true)
|
||||
check("RØG frakobling frigiver", S.isLoaned(near), false)
|
||||
|
||||
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
|
||||
started = {}
|
||||
noPark[near] = true
|
||||
near.stopped = nil
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||
S.planNow(adEnv)
|
||||
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||
noPark[near] = nil
|
||||
|
||||
-- I3: ingen rute til siloen -> ingen afsendelse
|
||||
started = {}
|
||||
noRoute["fs-load"] = true
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||
S.planNow(adEnv)
|
||||
check("I3 ingen rute -> ingen afsendelse", #started, 0)
|
||||
noRoute["fs-load"] = nil
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||
|
||||
-- I6: silo der er kilde/mål i et flow er ikke marksilo
|
||||
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
|
||||
local silos = S.collectBuildings(adEnv)
|
||||
check("I6 silo i et flow tømmes ikke", #silos, 0)
|
||||
ADRunsController.store.runs = {}
|
||||
|
||||
-- in-game 27/9: marksilo-flowet flyttede sukkerroer fra en Farma-silo til nabo-Farma-siloen (samme lagerklynge)
|
||||
local neighbour = {id = "nb", name = "Nabo-Farma", kind = "silo", placeable = {rootNode = {x = 40, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
||||
ADFields.fieldSilos = function() return {fieldSilo, farm, neighbour} end
|
||||
local withNeighbour = S.collectBuildings(adEnv)
|
||||
local names = {}
|
||||
for _, silo in ipairs(withNeighbour) do table.insert(names, silo.id) end
|
||||
check("gårdens nabosilo (40 m fra gårdlager) er ikke marksilo", table.concat(names, ","), "fs")
|
||||
ADFields.fieldSilos = function() return {fieldSilo, farm} end
|
||||
|
||||
-- in-game 27/9: gårdens Farma-klynge er 300 m lang — siloer over 100 m fra et valgt gårdlager blev marksiloer.
|
||||
-- En silo i samme klynge (≤ 60 m til en nabo) som et gårdlager er gårdens.
|
||||
local c1 = {id = "c1", name = "Klynge 1", kind = "silo", placeable = {rootNode = {x = 50, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
||||
local c2 = {id = "c2", name = "Klynge 2", kind = "silo", placeable = {rootNode = {x = 105, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
||||
ADBuildings.list = function() return {fieldSilo, farm, c1, c2} end
|
||||
ADFields.fieldSilos = function() return {fieldSilo, farm, c1, c2} end
|
||||
local clustered = {}
|
||||
for _, silo in ipairs(S.collectBuildings(adEnv)) do table.insert(clustered, silo.id) end
|
||||
check("gårdens klynge (105 m via nabo) er ikke marksilo", table.concat(clustered, ","), "fs")
|
||||
ADBuildings.list = function() return {fieldSilo, farm} end
|
||||
ADFields.fieldSilos = function() return {fieldSilo, farm} end
|
||||
|
||||
-- M5: rest under 100 l tæller ikke som last
|
||||
residue = 50
|
||||
local vehicles = S.collectVehicles(adEnv)
|
||||
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
|
||||
residue = 0
|
||||
|
||||
-- M4: slettet lånt vogn fjernes uden at vælte tick
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||
started = {}
|
||||
S.planNow(adEnv)
|
||||
local loaned = started[1] ~= nil and started[1].v or nil
|
||||
if loaned ~= nil then loaned.isDeleted = true end
|
||||
local okTick = pcall(S.tick, adEnv, 2000)
|
||||
check("M4 tick tåler slettet vogn", okTick, true)
|
||||
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
|
||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -38,5 +38,73 @@ for key, value in pairs(record) do
|
||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
||||
end
|
||||
|
||||
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
|
||||
dofile("FS25_ADSmartPickup/adCourseCache.lua")
|
||||
local course = ADCourseCache.newRecord({vehicle = "v7", field = 22, width = 30.0, sig = "n=4;a=960000;c=800,300",
|
||||
settings = ADCourseCache.settingsKey({multiTools = 2, lane = -1}), waypoints = 812})
|
||||
for key in pairs(course) do
|
||||
check("ST3 kursus-felt med i lagringen: " .. key, ADFieldStorage.COURSE_TYPES[key] ~= nil, true)
|
||||
end
|
||||
local cxml = fakeXml()
|
||||
ADFieldStorage.writeCourseRecord(cxml, "c", course)
|
||||
local cback = ADFieldStorage.readCourseRecord(cxml, "c")
|
||||
for key, value in pairs(course) do
|
||||
check("ST4 kursus-rundtur: " .. key, tostring(cback[key]), tostring(value))
|
||||
end
|
||||
check("ST4b matcher stadig efter rundtur", ADCourseCache.matches(cback, {field = 22, width = 30, sig = course.sig, settings = course.settings}), true)
|
||||
|
||||
-- hel fil: save -> load med falsk XMLFile (iterate over indekserede nøgler)
|
||||
local function fileXml(store)
|
||||
local xml = fakeXml()
|
||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
||||
xml["set" .. kind] = function(_, key, value) store[key] = value end
|
||||
xml["get" .. kind] = function(_, key) return store[key] end
|
||||
end
|
||||
function xml.iterate(_, base, fn)
|
||||
local index = 0
|
||||
while true do
|
||||
local prefix = string.format("%s(%d)", base, index)
|
||||
local found = false
|
||||
for key in pairs(store) do
|
||||
if key:sub(1, #prefix + 1) == prefix .. "#" or key:sub(1, #prefix + 1) == prefix .. "." then found = true break end
|
||||
end
|
||||
if not found then return end
|
||||
fn(index + 1, prefix)
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
function xml.save() end
|
||||
function xml.delete() end
|
||||
return xml
|
||||
end
|
||||
local disk = {}
|
||||
ADRunsStorage = {FILE_NAME = "runs.xml", getPath = function() return "/save/runs.xml" end}
|
||||
XMLFile = {create = function() return fileXml(disk) end, load = function() return fileXml(disk) end}
|
||||
function fileExists() return true end
|
||||
check("ST5 gem", ADFieldStorage.save({fields = {[77] = field}, fleet = {}, fleetMode = "all",
|
||||
courseRecords = ADCourseCache.withRecord({}, course)}), true)
|
||||
local loaded = ADFieldStorage.load()
|
||||
check("ST6 kursus indlæst", loaded.courseRecords.v7 ~= nil and loaded.courseRecords.v7.waypoints, 812)
|
||||
check("ST6b mark indlæst", loaded.fields[77] ~= nil, true)
|
||||
-- ST7: gammel fil uden <courses> -> tomt register (bagudkompatibelt)
|
||||
disk = {}
|
||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
|
||||
local old = ADFieldStorage.load()
|
||||
check("ST7 gammel fil: tomt register", next(old.courseRecords), nil)
|
||||
|
||||
-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom
|
||||
disk = {}
|
||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}})
|
||||
check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true)
|
||||
check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil)
|
||||
|
||||
-- ST9: pause ("Stop alt og parkér") gemmes, så intet starter af sig selv efter indlæsning
|
||||
disk = {}
|
||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", paused = true})
|
||||
check("ST9 pause indlæst", ADFieldStorage.load().paused, true)
|
||||
disk = {}
|
||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
|
||||
check("ST9 ingen pause", ADFieldStorage.load().paused, false)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -38,5 +38,29 @@ local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs =
|
||||
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
|
||||
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
|
||||
|
||||
-- cropText: høstet = stub (afgrøde), bar = bar jord, ellers afgrøden
|
||||
local texts = {spu_field_stubble = "stub (%s)", spu_field_bare = "bar jord"}
|
||||
local function T(key) return texts[key] end
|
||||
check("FVcrop stub", ADFieldView.cropText({kind = "bare", stubbleOf = "Hvede", cropTitle = "Hvede"}, T), "stub (Hvede)")
|
||||
check("FVcrop bar", ADFieldView.cropText({kind = "bare"}, T), "bar jord")
|
||||
check("FVcrop afgrøde", ADFieldView.cropText({kind = "harvest", cropTitle = "Majs"}, T), "Majs")
|
||||
|
||||
-- FVstore: marksilo først, gæringssilo, så afstand; valgte vises altid; kun de nærmeste af resten
|
||||
local stores = {
|
||||
{id = "fjern", distance = 3000},
|
||||
{id = "nær", distance = 200},
|
||||
{id = "mark", distance = 90, fieldSilo = true},
|
||||
{id = "valgt", distance = 5000, chosen = true},
|
||||
{id = "gær", distance = 800, fermenter = true},
|
||||
{id = "midt", distance = 1200},
|
||||
}
|
||||
local storeRows, hiddenStores = ADFieldView.storeRows(stores, 2)
|
||||
local order = {}
|
||||
for _, e in ipairs(storeRows) do table.insert(order, e.id) end
|
||||
check("FVstore rækkefølge", table.concat(order, ","), "mark,gær,nær,midt,valgt")
|
||||
check("FVstore skjulte", hiddenStores, 1)
|
||||
check("FVstore meter", ADFieldView.distanceText(90.4), "90 m")
|
||||
check("FVstore km", ADFieldView.distanceText(1420), "1,4 km")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Kør: luajit tests/test_adFieldWorkBuying.lua (fra repo-roden)
|
||||
-- Påfyldning ved købestation (in-game 26/9: "Liquid Buy Station", FarmFillStations buyingStationConfigurable):
|
||||
-- ingen lagerbeholdning, så varen læses fra udbud/triggere/AI-varer.
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
Logging = {info = function() end, warning = function() end}
|
||||
dofile("FS25_ADSmartPickup/adFieldWork.lua")
|
||||
local HERBICIDE, LIME, SEEDS = 20, 21, 22
|
||||
local W = ADFieldWork
|
||||
check("BS1 udbudte varer", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, HERBICIDE), true)
|
||||
check("BS2 triggerens varer", W.buyingSupports({loadTriggers = {{fillTypes = {[HERBICIDE] = true}}}}, HERBICIDE), true)
|
||||
check("BS3 AI-varer", W.buyingSupports({getAISupportedFillTypes = function() return {[SEEDS] = true} end}, SEEDS), true)
|
||||
check("BS4 sælger ikke kalk", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, LIME), false)
|
||||
check("BS5 fejl i opslag = nej", W.buyingSupports({getAISupportedFillTypes = function() error("x") end}, LIME), false)
|
||||
check("BS6 ingen station", W.buyingSupports(nil, LIME), false)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -61,5 +61,38 @@ check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
|
||||
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
|
||||
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
|
||||
|
||||
-- F8: slåafgrøde fuldt udvokset (Dakota-græs 3 = mellemhøjde, 4-5 = fuld)
|
||||
check("F8 prime græs", F.primeState({minHarvestingGrowthState = 3, maxHarvestingGrowthState = 5}), 4)
|
||||
check("F8 prime ét klar-stadie", F.primeState({minHarvestingGrowthState = 6, maxHarvestingGrowthState = 6}), 6)
|
||||
check("F8 prime ukendt", F.primeState({}), nil)
|
||||
check("F8 mellemhøjde tæller ikke", F.isPrime(3, 4), false)
|
||||
check("F8 fuld højde tæller", F.isPrime(5, 4), true)
|
||||
check("F8 ukendt stadie tæller", F.isPrime(nil, 4), true)
|
||||
check("F8 ingen krav", F.isPrime(3, nil), true)
|
||||
|
||||
-- F9: kartofler (Dakota): 6 = klar men kun forberedelig, 10 = toppet (høstklar)
|
||||
local potato = {minHarvestingGrowthState = 10, maxHarvestingGrowthState = 10, minPreparingGrowthState = 6, maxPreparingGrowthState = 6, witheredState = 7, cutState = 8}
|
||||
check("F9 utoppet er forberedelig", F.isPreparable(6, potato), true)
|
||||
check("F9 voksende ikke", F.isPreparable(5, potato), false)
|
||||
check("F9 fase klar", F.growthPhase(6, potato), "ready")
|
||||
check("F9 toppet klar", F.growthPhase(10, potato), "ready")
|
||||
check("F9 ingen forberedelse", F.isPreparable(6, {minPreparingGrowthState = -1}), false)
|
||||
|
||||
-- jordtype -> klargjort/pløjet (værdierne kommer fra spillets fieldGroundSystem)
|
||||
local groundValues = {STUBBLE_TILLAGE = 1, CULTIVATED = 2, SEEDBED = 3, PLOWED = 4, ROLLED_SEEDBED = 5, SOWN = 6}
|
||||
local function cls(v) local p, pl = ADFields.classifyGround(v, groundValues) return tostring(p) .. "|" .. tostring(pl) end
|
||||
check("jord: pløjet", cls(4), "true|true")
|
||||
check("jord: kultiveret", cls(2), "true|false")
|
||||
check("jord: stubbearbejdet", cls(1), "true|false")
|
||||
check("jord: sået", cls(6), "false|false")
|
||||
check("jord: ukendt værdi", cls(nil), "nil|nil")
|
||||
groundValues.NONE = 0
|
||||
check("jord: ingen markjord tæller ikke med", cls(0), "nil|nil")
|
||||
-- vin/oliven er aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen (in-game 29/9: F22 hvide druer fik
|
||||
-- planen "Silage + så" og en skårlægger sendt ud)
|
||||
check("vin: druer ikke slåbare", ADFields.isMowableFruit("GRAPE", {windrow = "GRASS_WINDROW"}), false)
|
||||
check("vin: oliven ikke slåbare", ADFields.isMowableFruit("oliveb", {windrow = "X"}), false)
|
||||
check("græs slåbart", ADFields.isMowableFruit("GRASS", {windrow = "GRASS_WINDROW"}), true)
|
||||
check("ikke på MOWER-listen", ADFields.isMowableFruit("WHEAT", nil), false)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
-- Kør: luajit tests/test_adFieldsPolygon.lua (fra repo-roden)
|
||||
-- FS25-marker har ingen polygonPoints — omridset hentes med field:getDensityMapPolygon():getVerticesList().
|
||||
-- In-game 26/9 (Dakota 16x, marker på 1,7 x 0,6 km): uden omrids blev kun markskiltet (posX/posZ) prøvet;
|
||||
-- F108 med majs viste "bar jord" og "mangler markør" (markøren stod ved kanten, 890 m fra skiltet).
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
Logging = {warning = function() end, info = function() end}
|
||||
|
||||
local MAIZE = 10
|
||||
function getWorldTranslation() return 0, 0, 0 end
|
||||
function getTerrainHeightAtWorldPos() return 0 end
|
||||
-- markskiltet står i marken, men ved en stribe uden afgrøde (x > 1400)
|
||||
function cropAt(x) return x <= 1400 and MAIZE or nil end
|
||||
FSDensityMapUtil = {
|
||||
getFieldDataAtWorldPosition = function() return 1, 2 end,
|
||||
getFruitTypeIndexAtWorldPos = function(x, z) return cropAt(x) end,
|
||||
getFruitArea = function() return 0, 10 end,
|
||||
}
|
||||
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
|
||||
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
|
||||
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() return 1, 1 end} end}
|
||||
g_fruitTypeManager = {
|
||||
getFruitTypeByIndex = function() return {name = "MAIZE", fillType = {index = 60, title = "Majs"}, minHarvestingGrowthState = 7, maxHarvestingGrowthState = 7, cutState = 9} end,
|
||||
getConverterDataByName = function() return {} end,
|
||||
}
|
||||
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
|
||||
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
|
||||
local polygon = {getVerticesList = function() return {0, 0, 1600, 0, 1600, 600, 0, 600} end}
|
||||
local field = {getId = function() return 108 end, farmland = {id = 108}, posX = 1500, posZ = 50,
|
||||
getDensityMapPolygon = function() return polygon end}
|
||||
g_fieldManager = {getFields = function() return {field} end}
|
||||
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
|
||||
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
|
||||
local adEnv = {ADGraphManager = {
|
||||
getMapMarkers = function() return {{id = 900, name = "F108"}} end,
|
||||
-- markøren står 5 m uden for marken ved den modsatte kant: 1505 m fra skiltet
|
||||
getWayPointById = function() return {x = -5, z = 300} end,
|
||||
}}
|
||||
|
||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
||||
local now = 0
|
||||
for _ = 1, 1000 do
|
||||
if ADFields.getInfo(108) ~= nil then break end
|
||||
ADFields.scanStep(adEnv, 1, now)
|
||||
now = now + 16
|
||||
end
|
||||
local info = ADFields.getInfo(108)
|
||||
check("PG1 marken er scannet", info ~= nil, true)
|
||||
check("PG2 omrids fra getDensityMapPolygon", info and #info.verts, 4)
|
||||
check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, "MAIZE")
|
||||
check("PG4 ikke bar mark", info and info.kind, "harvest")
|
||||
check("PG5 markør ved kanten findes (afstand til omridset)", info and info.markerId, 900)
|
||||
check("PG6 afstand = til kanten", info and info.markerDistance, 5)
|
||||
check("PG6b markens form-signatur (kursus-genbrug)", info and info.shapeSig, "n=4;a=960000;c=800,300")
|
||||
|
||||
-- PG7-9: field.fieldState er spillets sandhed (fields.xml: MAIZE growthState 2); jord-bits fra
|
||||
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
|
||||
|
||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2}
|
||||
FSDensityMapUtil.getFieldDataAtWorldPosition = function() return true, 0 end -- jord-bits 0 lignede "ingen vækst"
|
||||
local function rescan()
|
||||
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
|
||||
for _ = 1, 2000 do
|
||||
if ADFields.getInfo(108) ~= nil then break end
|
||||
ADFields.scanStep(adEnv, 1, now)
|
||||
now = now + 16
|
||||
end
|
||||
return ADFields.getInfo(108)
|
||||
end
|
||||
info = rescan()
|
||||
check("PG7 majs i vækst (fieldState), ikke bar", info and info.kind, "harvest")
|
||||
check("PG7 fase vokser", info and info.phase, "growing")
|
||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 9}
|
||||
info = rescan()
|
||||
check("PG8 høstet (cutState) = bar", info and info.kind, "bare")
|
||||
check("PG8 stub af majs", info and info.stubbleOf, "Majs")
|
||||
field.fieldState = {fruitTypeIndex = 0, growthState = 0}
|
||||
cropAt = function() return nil end
|
||||
info = rescan()
|
||||
check("PG9 ingen afgrøde = bar uden stub", info and info.kind .. "/" .. tostring(info.stubbleOf), "bare/nil")
|
||||
-- PG10-12: bygget-over mark (fields.xml: groundType NONE — gårdsplads/stald/biogas) er ikke med i listen
|
||||
field.fieldState = {fruitTypeIndex = 0, growthState = 0, groundType = 0}
|
||||
info = rescan()
|
||||
check("PG10 ingen markjord = kan ikke dyrkes", info and info.cultivable, false)
|
||||
check("PG10 ikke i marklisten", #ADFields.list(), 0)
|
||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2, groundType = 7}
|
||||
cropAt = function(x) return MAIZE end
|
||||
info = rescan()
|
||||
check("PG11 markjord = kan dyrkes", info and info.cultivable, true)
|
||||
check("PG11 i marklisten", #ADFields.list(), 1)
|
||||
check("PG12 ingen punkter på markjord", ADFields.isCultivable(nil, 0), false)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,94 @@
|
||||
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
|
||||
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
|
||||
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
|
||||
|
||||
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
|
||||
local GRASS, WHEAT = 1, 3
|
||||
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
|
||||
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
||||
function getTerrainHeightAtWorldPos() return 0 end
|
||||
local calls = 0
|
||||
FSDensityMapUtil = {
|
||||
getFieldDataAtWorldPosition = function() calls = calls + 1; return true, 170 end, -- på mark (2. værdi = jord-bits, IKKE vækst)
|
||||
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
|
||||
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
|
||||
}
|
||||
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
|
||||
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
|
||||
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
|
||||
g_fruitTypeManager = {
|
||||
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
|
||||
getConverterDataByName = function() return {} end,
|
||||
}
|
||||
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
|
||||
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
|
||||
-- spillets egen marktilstand (field.fieldState, som fields.xml): afgrøde + vækststadie
|
||||
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50,
|
||||
fieldState = {fruitTypeIndex = WHEAT, growthState = 6}}
|
||||
g_fieldManager = {getFields = function() return {field} end}
|
||||
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
|
||||
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
|
||||
local adEnv = {ADGraphManager = {
|
||||
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
|
||||
getWayPointById = function(_, id) return {x = 40, z = 40} end,
|
||||
}}
|
||||
|
||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
||||
|
||||
local maxPerFrame, frames = 0, 0
|
||||
local now = 0
|
||||
for _ = 1, 200 do
|
||||
calls = 0
|
||||
ADFields.scanStep(adEnv, 1, now)
|
||||
if calls > maxPerFrame then maxPerFrame = calls end
|
||||
frames = frames + 1
|
||||
now = now + 16
|
||||
if ADFields.getInfo(7) ~= nil then break end
|
||||
end
|
||||
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
|
||||
for _ = 1, 400 do
|
||||
if ADFields.getInfo(7) ~= nil then break end
|
||||
calls = 0
|
||||
ADFields.scanStep(adEnv, 1, now)
|
||||
if calls > maxPerFrame then maxPerFrame = calls end
|
||||
now = now + 16
|
||||
end
|
||||
local info = ADFields.getInfo(7)
|
||||
check("SC1 marken er scannet", info ~= nil, true)
|
||||
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
|
||||
check("SC3 art = høst", info and info.kind, "harvest")
|
||||
check("SC4 klar-andel", info and info.readyRatio, 1)
|
||||
check("SC5 fase klar", info and info.phase, "ready")
|
||||
check("SC6 markør F7", info and info.markerId, 900)
|
||||
check("SC7 prøvepunkter", info and #info.points, 5)
|
||||
check("SC8 gødning læst", info and info.fert, 1)
|
||||
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
|
||||
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
|
||||
check("SC10 marklisten", #ADFields.list(), 1)
|
||||
|
||||
-- SC11: med ur kører flere opgaver pr. frame inden for budgettet (0,5 ms), højst MAX_TASKS_PER_FRAME
|
||||
local fakeTime = 0
|
||||
netGetTime = function() fakeTime = fakeTime + 0.1; return fakeTime end -- hver opgave "koster" 0,1 ms
|
||||
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
|
||||
local frames2 = 0
|
||||
for _ = 1, 400 do
|
||||
if ADFields.getInfo(7) ~= nil then break end
|
||||
ADFields.scanStep(adEnv, 1, now)
|
||||
now = now + 16
|
||||
frames2 = frames2 + 1
|
||||
end
|
||||
check("SC11 scannet igen med ur", ADFields.getInfo(7) ~= nil, true)
|
||||
check("SC11 færre frames end én opgave pr. frame", frames2 < frames + 0, true)
|
||||
print(" (frames uden ur: " .. frames .. ", med ur: " .. frames2 .. ")")
|
||||
netGetTime = nil
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,44 @@
|
||||
-- Kør: luajit tests/test_adFieldsShape.lua (fra repo-roden)
|
||||
-- Markens form-fingeraftryk (genbrug af Courseplay-kurser): antal hjørner, areal og tyngdepunkt, afrundet så
|
||||
-- små float-forskelle ikke tæller — men en ændret mark (nyt hjørne, flyttet kant) giver en ny signatur.
|
||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local function rect(x0, z0, w, h)
|
||||
return {{x = x0, z = z0}, {x = x0 + w, z = z0}, {x = x0 + w, z = z0 + h}, {x = x0, z = z0 + h}}
|
||||
end
|
||||
|
||||
-- S1: rektangel 1600 x 600 -> 4 hjørner, 960.000 m², tyngdepunkt (800, 300)
|
||||
check("S1 rektangel", ADFields.shapeSignature(rect(0, 0, 1600, 600)), "n=4;a=960000;c=800,300")
|
||||
-- S2: omløbsretning er ligegyldig (arealet er absolut)
|
||||
local reversed = {}
|
||||
for index = 4, 1, -1 do table.insert(reversed, rect(0, 0, 1600, 600)[index]) end
|
||||
check("S2 modsat omløb", ADFields.shapeSignature(reversed), "n=4;a=960000;c=800,300")
|
||||
-- S3: små float-forskelle (cm) tæller ikke
|
||||
local noisy = rect(0.0003, -0.0002, 1600.0004, 599.9997)
|
||||
check("S3 float-støj", ADFields.shapeSignature(noisy), "n=4;a=960000;c=800,300")
|
||||
-- S4: flyttet kant (1 m bredere) -> ny signatur
|
||||
check("S4 kant flyttet", ADFields.shapeSignature(rect(0, 0, 1601, 600)) ~= ADFields.shapeSignature(rect(0, 0, 1600, 600)), true)
|
||||
-- S5: ekstra hjørne på kanten (samme areal) -> ny signatur
|
||||
local extra = {{x = 0, z = 0}, {x = 800, z = 0}, {x = 1600, z = 0}, {x = 1600, z = 600}, {x = 0, z = 600}}
|
||||
check("S5 ekstra hjørne", ADFields.shapeSignature(extra), "n=5;a=960000;c=800,300")
|
||||
-- S6: samme form et andet sted på kortet -> anden signatur
|
||||
check("S6 flyttet mark", ADFields.shapeSignature(rect(100, 0, 1600, 600)), "n=4;a=960000;c=900,300")
|
||||
-- S7: tom/ingen polygon -> nil
|
||||
check("S7 tom", ADFields.shapeSignature({}), nil)
|
||||
check("S8 nil", ADFields.shapeSignature(nil), nil)
|
||||
-- S9: under 3 hjørner er ingen flade -> nil
|
||||
check("S9 to punkter", ADFields.shapeSignature({{x = 0, z = 0}, {x = 1, z = 1}}), nil)
|
||||
-- S10: trekant: areal 50, tyngdepunkt (10/3, 10/3) -> (3, 3)
|
||||
check("S10 trekant", ADFields.shapeSignature({{x = 0, z = 0}, {x = 10, z = 0}, {x = 0, z = 10}}), "n=3;a=50;c=3,3")
|
||||
-- S11: negative koordinater afrundes symmetrisk
|
||||
check("S11 negative", ADFields.shapeSignature(rect(-1000, -500, 200, 100)), "n=4;a=20000;c=-900,-450")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Kør: luajit tests/test_adFleetNames.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFleetNames.lua")
|
||||
local N = ADFleetNames
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local plan = N.plan({
|
||||
{id = "b", model = "Puma 260 CVX"}, {id = "a", model = "Puma 260 CVX"}, {id = "c", model = "Fastrac 4220"},
|
||||
{id = "d", model = "Cow Food", inRun = true, name = "Cow Food 1"}, {id = "e", model = "CR11", selfPropelled = true, name = "Mejetærsker"},
|
||||
})
|
||||
local byId = {}
|
||||
for _, entry in ipairs(plan) do byId[entry.id] = entry end
|
||||
check("navne: samme model nummereres (efter id)", byId.a.name, "Puma 260 CVX 1")
|
||||
check("navne: anden af samme model", byId.b.name, "Puma 260 CVX 2")
|
||||
check("navne: enlig model uden nummer", byId.c.name, "Fastrac 4220")
|
||||
check("navne: flow-traktor beholder navn", byId.d.name, "Cow Food 1")
|
||||
check("navne: selvkørende beholder navn", byId.e.name, "Mejetærsker")
|
||||
check("parkering: P + navn", byId.a.parkName, "P Puma 260 CVX 1")
|
||||
check("parkering: flow-traktor", byId.d.parkName, "P Cow Food 1")
|
||||
check("omdøb: kun hvis ændret", byId.d.rename, false)
|
||||
check("omdøb: ny navn", byId.a.rename, true)
|
||||
-- R7: parkeringer omdøbes kun for maskiner der selv omdøbes, og ikke delte parkeringer
|
||||
check("R7 flow-traktor: parkering røres ikke", byId.d.renamePark, false)
|
||||
check("R7 selvkørende: parkering røres ikke", byId.e.renamePark, false)
|
||||
check("R7 omdøbt traktor: parkering omdøbes", byId.a.renamePark, true)
|
||||
local shared = N.plan({{id = "x", model = "M", park = 5}, {id = "y", model = "Q", park = 5}}, {[5] = 2})
|
||||
check("R7 delt parkering røres ikke", shared[1].renamePark, false)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,314 @@
|
||||
-- Kør: luajit tests/test_adFlowGuide.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
||||
dofile("FS25_ADSmartPickup/adFlowGuide.lua")
|
||||
local FG, G = ADFlowGuide, ADGuide
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local function newRun(kind, changes)
|
||||
local store, run = ADRuns.createRun(ADRuns.newStore(), "Mælk ud", kind)
|
||||
if changes ~= nil then store = ADRuns.updateRun(store, run.id, changes) end
|
||||
return ADRuns.getRun(store, run.id)
|
||||
end
|
||||
local function ctx(extra)
|
||||
local context = {
|
||||
buildings = {
|
||||
{id = "b1", name = "Kostald", label = "Kostald M", hasMarker = true, text = "Kostald — M 5 m"},
|
||||
{id = "b2", name = "Silo", label = "Silo M", hasMarker = true, text = "Silo — M 3 m"},
|
||||
{id = "b3", name = "Lade", hasMarker = false, text = "Lade — INGEN MARKØR"},
|
||||
},
|
||||
fillTypes = {{name = "HAY", title = "Hø"}, {name = "STRAW", title = "Halm"}},
|
||||
products = {{name = "MILK", title = "Mælk"}, {name = "EGG", title = "Æg"}},
|
||||
storeBuildings = {{id = "s1", name = "Silo Nord", text = "Silo Nord — M 5 m"}, {id = "s2", name = "Palleskur", text = "Palleskur — M 2 m"}},
|
||||
waitPools = {{group = "Wait A", count = 2}},
|
||||
vehicles = {
|
||||
{id = "t1", label = "Traktor 1", text = "Traktor 1 — 1000 l — OK"},
|
||||
{id = "t2", text = "Traktor 2", otherRunId = 9, otherRunName = "Andet"},
|
||||
{id = "t3", text = "Traktor 3", blocked = true},
|
||||
{id = "t4", text = "Traktor 4"},
|
||||
},
|
||||
}
|
||||
for key, value in pairs(extra or {}) do context[key] = value end
|
||||
return context
|
||||
end
|
||||
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
|
||||
local function findRow(rows, id, item)
|
||||
for _, row in ipairs(rows) do
|
||||
if row.id == id and (item == nil or row.item == item) then return row end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function countType(rows, rowType)
|
||||
local n = 0
|
||||
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
|
||||
return n
|
||||
end
|
||||
local function textKeys(rows)
|
||||
local keys = {}
|
||||
for _, row in ipairs(rows) do if row.type == "text" and row.label.key ~= nil then table.insert(keys, row.label.key) end end
|
||||
return table.concat(keys, ",")
|
||||
end
|
||||
local function at(guide, step) return G.jump(guide, step, "close") end
|
||||
|
||||
-- ---- trin ----
|
||||
local supply = FG.open(newRun("supply"), ctx())
|
||||
check("FL1 forsyning", steps(supply), "direction,buildings,goods,rules,wait,tractors,summary")
|
||||
local out = FG.open(newRun("outbound"), ctx())
|
||||
check("FL1 udkørsel uden lager-vare", steps(out), "direction,buildings,goods,rules,wait,tractors,summary")
|
||||
local outStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sellElseStore"}}}), ctx())
|
||||
check("FL1 udkørsel med lager", steps(outStore), "direction,buildings,goods,store,rules,wait,tractors,summary")
|
||||
local new = FG.openNew("Flow 3", ctx())
|
||||
check("FL1 nyt flow har navn", steps(new), "name,direction,buildings,goods,rules,wait,tractors,summary")
|
||||
check("FL1 nyt flow starter på navn", new.step, "name")
|
||||
check("FL1 nyt flow er forsyning", new.draft.kind, "supply")
|
||||
check("FL1 nyt flow navn", new.draft.name, "Flow 3")
|
||||
check("FL1 eksisterende starter på retning", supply.step, "direction")
|
||||
|
||||
-- ---- titler følger retningen ----
|
||||
check("FL2 til", FG.stepKey("buildings", supply), "spu_guide_flow_step_to")
|
||||
check("FL2 fra", FG.stepKey("buildings", out), "spu_guide_flow_step_from")
|
||||
check("FL2 andre", FG.stepKey("rules", out), "spu_guide_flow_step_rules")
|
||||
check("FL2 hint", FG.hintKey("goods", out), "spu_guide_flow_hint_goods_out")
|
||||
check("FL2 hint forsyning", FG.hintKey("goods", supply), "spu_guide_flow_hint_goods_in")
|
||||
|
||||
-- ---- choose er immutabel ----
|
||||
local toOut = FG.choose(supply, "kind", "outbound")
|
||||
check("FL3 retning skiftet", toOut.draft.kind, "outbound")
|
||||
check("FL3 gammel urørt", supply.draft.kind, "supply")
|
||||
local withTarget = FG.choose(supply, "buildings", {item = "b1", on = true})
|
||||
check("FL3 forsyning: mål", table.concat(withTarget.draft.targetBuildingIds, ","), "b1")
|
||||
check("FL3 forsyning: kilder urørt", #withTarget.draft.sourceBuildingIds, 0)
|
||||
local withSource = FG.choose(out, "buildings", {item = "b2", on = true})
|
||||
check("FL3 udkørsel: kilde", table.concat(withSource.draft.sourceBuildingIds, ","), "b2")
|
||||
local policy = FG.choose(out, "policy", {item = "MILK", value = "store"})
|
||||
check("FL3 politik", policy.draft.products[1].policy, "store")
|
||||
check("FL3 politik gør lager-trin synligt", FG.isVisible("store", policy), true)
|
||||
local off = FG.choose(policy, "policy", {item = "MILK", value = "off"})
|
||||
check("FL3 politik fra fjerner", #off.draft.products, 0)
|
||||
local replaced = FG.choose(policy, "policy", {item = "MILK", value = "sell"})
|
||||
check("FL3 politik erstattes", #replaced.draft.products, 1)
|
||||
check("FL3 politik ny", replaced.draft.products[1].policy, "sell")
|
||||
check("FL3 navn", FG.choose(new, "name", "Æg til salg").draft.name, "Æg til salg")
|
||||
check("FL3 tomt navn ignoreres", FG.choose(new, "name", " ").draft.name, "Flow 3")
|
||||
|
||||
-- ---- rækker ----
|
||||
local dirRows = FG.rows(supply)
|
||||
check("FL4 retning valg", findRow(dirRows, "kind").type, "choice")
|
||||
check("FL4 retning værdi", findRow(dirRows, "kind").value, "supply")
|
||||
local buildingRows = FG.rows(at(supply, "buildings"))
|
||||
check("FL4 bygninger med markør", countType(buildingRows, "check"), 2)
|
||||
check("FL4 vis alle", findRow(buildingRows, "showAllBuildings").type, "toggle")
|
||||
local allRows = FG.rows(at(FG.choose(supply, "showAllBuildings", true), "buildings"))
|
||||
check("FL4 vis alle: tre", countType(allRows, "check"), 3)
|
||||
local noHidden = FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {{id = "b1", name = "Kostald", hasMarker = true, text = "K"}}})), "buildings"))
|
||||
check("FL4 ingen vis-alle uden skjulte", findRow(noHidden, "showAllBuildings"), nil)
|
||||
local chosenFirst = FG.rows(at(FG.choose(supply, "buildings", {item = "b2", on = true}), "buildings"))
|
||||
local firstCheck
|
||||
for _, row in ipairs(chosenFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
|
||||
check("FL4 fast rækkefølge (navn)", firstCheck.item, "b1")
|
||||
check("FL4 valgt markeret", findRow(chosenFirst, "buildings", "b2").value, true)
|
||||
check("FL4 ingen bygninger", textKeys(FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {}})), "buildings"))), "spu_noBuildings")
|
||||
|
||||
local goodsAuto = FG.rows(at(supply, "goods"))
|
||||
check("FL5 forsyning auto", findRow(goodsAuto, "ingredientMode").value, "auto")
|
||||
check("FL5 forsyning auto: ingen liste", countType(goodsAuto, "check"), 0)
|
||||
local goodsManual = FG.rows(at(FG.choose(supply, "ingredientMode", "manual"), "goods"))
|
||||
check("FL5 forsyning vælg: varer", countType(goodsManual, "check"), 2)
|
||||
check("FL5 forsyning vælg uden varer", textKeys(FG.rows(at(FG.choose(FG.open(newRun("supply"), ctx({fillTypes = {}})), "ingredientMode", "manual"), "goods"))), "spu_guide_flow_noGoodsTo")
|
||||
local goodsOut = FG.rows(at(policy, "goods"))
|
||||
check("FL5 udkørsel: politik pr. vare", countType(goodsOut, "choice"), 2)
|
||||
check("FL5 udkørsel: mælk lager", findRow(goodsOut, "policy", "MILK").value, "store")
|
||||
check("FL5 udkørsel: æg fra", findRow(goodsOut, "policy", "EGG").value, "off")
|
||||
check("FL5 udkørsel: politikker", #findRow(goodsOut, "policy", "EGG").options, 4)
|
||||
check("FL5 udkørsel uden varer", textKeys(FG.rows(at(FG.open(newRun("outbound"), ctx({products = {}})), "goods"))), "spu_guide_flow_noProducts")
|
||||
|
||||
local storeRows = FG.rows(at(outStore, "store"))
|
||||
check("FL6 lager auto", findRow(storeRows, "storeAuto").value, true)
|
||||
check("FL6 lager auto: ingen liste", countType(storeRows, "check"), 0)
|
||||
local storeManual = FG.rows(at(FG.choose(outStore, "storeAuto", false), "store"))
|
||||
check("FL6 lager vælg selv", countType(storeManual, "check"), 2)
|
||||
local chosenStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}, storeBuildingIds = {"s2"}}), ctx())
|
||||
check("FL6 valgt lager -> vælg selv", chosenStore.draft.storeAuto, false)
|
||||
|
||||
local rulesOut = FG.rows(at(out, "rules"))
|
||||
check("FL7 udkørsel: sælg fra", findRow(rulesOut, "sellAtPercent").value, 90)
|
||||
check("FL7 udkørsel: sælg-trin", #findRow(rulesOut, "sellAtPercent").options, 11)
|
||||
check("FL7 udkørsel: lager over", findRow(rulesOut, "storeAbovePercent").value, 80)
|
||||
check("FL7 udkørsel: samlerunde", findRow(rulesOut, "collectRound").type, "toggle")
|
||||
check("FL7 udkørsel: ingen ture", findRow(rulesOut, "loops"), nil)
|
||||
local rulesIn = FG.rows(at(supply, "rules"))
|
||||
check("FL7 forsyning: ture", findRow(rulesIn, "loops").value, 0)
|
||||
check("FL7 forsyning: ture-valg", #findRow(rulesIn, "loops").options, ADRuns.MAX_LOOPS + 1)
|
||||
check("FL7 forsyning: 0 = uendeligt", findRow(rulesIn, "loops").options[1].label.key, "spu_loops_infinite")
|
||||
check("FL7 forsyning: retur", findRow(rulesIn, "returnBelowPercent").value, 50)
|
||||
check("FL7 forsyning: ingen salg", findRow(rulesIn, "sellAtPercent"), nil)
|
||||
|
||||
local waitRows = FG.rows(at(supply, "wait"))
|
||||
check("FL8 vent: alle først", findRow(waitRows, "waitPoolGroup").options[1].value, "")
|
||||
check("FL8 vent: puljer", #findRow(waitRows, "waitPoolGroup").options, 2)
|
||||
|
||||
local assigned = FG.open(newRun("supply", {}), ctx())
|
||||
assigned = FG.choose(assigned, "vehicleIds", {item = "t4", on = true})
|
||||
local tractorRows = FG.rows(at(assigned, "tractors"))
|
||||
check("FL9 traktorer: kun egne + ledige", countType(tractorRows, "check"), 2)
|
||||
check("FL9 egne først", (function() for _, r in ipairs(tractorRows) do if r.type == "check" then return r.item end end end)(), "t4")
|
||||
check("FL9 skjult andre flows", findRow(tractorRows, "vehicleIds", "t2"), nil)
|
||||
check("FL9 skjult blokeret", findRow(tractorRows, "vehicleIds", "t3"), nil)
|
||||
check("FL9 linjer", textKeys(tractorRows), "spu_guide_flow_otherFlows")
|
||||
-- blokerede traktorer: foldet sammen som standard (in-game 27/9: 37 linjer der ikke kunne bruges fyldte trinnet);
|
||||
-- linjen med antallet er et link der folder navn + grund ud og ind
|
||||
local function blockedLine(rows)
|
||||
for _, r in ipairs(rows) do if r.type == "text" and r.label.text ~= nil and r.label.text:find("Traktor 3", 1, true) then return r end end
|
||||
return nil
|
||||
end
|
||||
local function blockedLink(rows)
|
||||
for _, r in ipairs(rows) do if r.type == "link" and r.id == "showBlocked" then return r end end
|
||||
return nil
|
||||
end
|
||||
check("FL9 blokerede foldet sammen som standard", blockedLine(tractorRows), nil)
|
||||
check("FL9 link med antal", blockedLink(tractorRows) ~= nil and blockedLink(tractorRows).label.key, "spu_guide_flow_blockedShow")
|
||||
local expanded = FG.choose(assigned, "showBlocked", true)
|
||||
local expandedRows = FG.rows(at(expanded, "tractors"))
|
||||
check("FL9 foldet ud viser navn + grund", blockedLine(expandedRows) ~= nil, true)
|
||||
check("FL9 foldet ud: link skjuler igen", blockedLink(expandedRows).label.key, "spu_guide_flow_blockedHide")
|
||||
check("FL9 fold ud ændrer ikke kladden", expanded.draft.vehicleIds[1], "t4")
|
||||
check("FL9 fold sammen igen", blockedLine(FG.rows(at(FG.choose(expanded, "showBlocked", true), "tractors"))), nil)
|
||||
local inThisRun = FG.open(newRun("supply"), ctx({vehicles = {{id = "t2", text = "T2", otherRunId = 1, otherRunName = "Mælk ud"}}}))
|
||||
check("FL9 traktor i dette flow er ikke 'andet flow'", countType(FG.rows(at(inThisRun, "tractors")), "check"), 1)
|
||||
|
||||
-- ---- resumé ----
|
||||
local function lineFor(lines, step)
|
||||
for _, line in ipairs(lines) do if line.step == step then return line end end
|
||||
return nil
|
||||
end
|
||||
local function partText(line)
|
||||
local parts = {}
|
||||
for _, part in ipairs(line.parts) do table.insert(parts, part.key or ("=" .. tostring(part.text))) end
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
local sumIn = FG.summary(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}))
|
||||
check("FL10 navn", partText(lineFor(sumIn, "name")), "=Mælk ud")
|
||||
check("FL10 retning", partText(lineFor(sumIn, "direction")), "spu_direction_in")
|
||||
check("FL10 til", partText(lineFor(sumIn, "buildings")), "=Kostald")
|
||||
check("FL10 til-titel", lineFor(sumIn, "buildings").title.key, "spu_guide_flow_step_to")
|
||||
check("FL10 varer auto", partText(lineFor(sumIn, "goods")), "spu_guide_flow_goodsAuto")
|
||||
check("FL10 regler", partText(lineFor(sumIn, "rules")), "spu_guide_flow_sumLoopsInf,spu_guide_flow_sumReturn")
|
||||
check("FL10 vent", partText(lineFor(sumIn, "wait")), "spu_waitPool_all")
|
||||
check("FL10 traktorer", partText(lineFor(sumIn, "tractors")), "=Traktor 1")
|
||||
check("FL10 intet lager-trin", lineFor(sumIn, "store"), nil)
|
||||
-- traktor der ikke findes længere (solgt): aldrig rå id i resuméet (in-game 29/9)
|
||||
local withGhost = FG.choose(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}), "vehicleIds", {item = "vehicleGONE", on = true})
|
||||
check("FL10b resumé springer manglende over", partText(lineFor(FG.summary(withGhost), "tractors")), "=Traktor 1")
|
||||
local onlyGhost = FG.choose(withTarget, "vehicleIds", {item = "vehicleGONE", on = true})
|
||||
check("FL10b kun manglende -> ingen traktorer", partText(lineFor(FG.summary(onlyGhost), "tractors")), "spu_guide_flow_sumNoTractors")
|
||||
local sumOut = FG.summary(FG.choose(FG.choose(policy, "collectRound", true), "sellAtPercent", 95))
|
||||
check("FL11 retning ud", partText(lineFor(sumOut, "direction")), "spu_direction_out")
|
||||
check("FL11 fra tom", partText(lineFor(sumOut, "buildings")), "spu_guide_flow_none")
|
||||
check("FL11 varer", partText(lineFor(sumOut, "goods")), "spu_policy_store")
|
||||
check("FL11 varenavn foran", lineFor(sumOut, "goods").parts[1].prefix, "Mælk: ")
|
||||
check("FL11 lager auto", partText(lineFor(sumOut, "store")), "spu_guide_sum_auto")
|
||||
-- varen er kun "lager": salgsgrænsen vises ikke (in-game 29/9)
|
||||
check("FL11 regler (kun lager)", partText(lineFor(sumOut, "rules")), "spu_guide_flow_sumStoreAbove,spu_guide_flow_sumRound")
|
||||
check("FL11 lager over", lineFor(sumOut, "rules").parts[1].args[1], 80)
|
||||
check("FL11 ingen traktorer", partText(lineFor(sumOut, "tractors")), "spu_guide_flow_sumNoTractors")
|
||||
local manyGoods = FG.summary(FG.choose(FG.choose(FG.choose(supply, "ingredientMode", "manual"), "ingredients", {item = "HAY", on = true}), "ingredients", {item = "STRAW", on = true}))
|
||||
check("FL12 valgte varer", partText(lineFor(manyGoods, "goods")), "=Hø + Halm")
|
||||
check("FL12 oversigt: links", countType(FG.rows(G.new(supply.def, supply.draft, supply.context, {step = "summary"})), "link"), 7)
|
||||
|
||||
-- ---- resultat ----
|
||||
local r = FG.result(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))
|
||||
check("FL13 ikke ny", r.isNew, false)
|
||||
check("FL13 run-id", r.runId, 1)
|
||||
check("FL13 retning", r.changes.kind, "outbound")
|
||||
check("FL13 lager valgt", table.concat(r.changes.storeBuildingIds, ","), "s1")
|
||||
check("FL13 varer", r.changes.products[1].fillType, "MILK")
|
||||
check("FL13 lager-resumé bruger navn", (function() for _, l in ipairs(FG.summary(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))) do if l.step == "store" then return l.parts[1].text end end end)(), "Silo Nord")
|
||||
local rAuto = FG.result(FG.choose(chosenStore, "storeAuto", true))
|
||||
check("FL13 lager auto -> tom", #rAuto.changes.storeBuildingIds, 0)
|
||||
local hiddenStore = FG.open(newRun("outbound", {storeBuildingIds = {"s2"}}), ctx())
|
||||
check("FL13 lager usynligt: bevares", table.concat(FG.result(hiddenStore).changes.storeBuildingIds, ","), "s2")
|
||||
local rNew = FG.result(FG.choose(new, "vehicleIds", {item = "t1", on = true}))
|
||||
check("FL14 nyt", rNew.isNew, true)
|
||||
check("FL14 navn", rNew.name, "Flow 3")
|
||||
check("FL14 traktorer", table.concat(rNew.vehicleIds, ","), "t1")
|
||||
check("FL14 ingen run-id", rNew.runId, nil)
|
||||
local added, removed = FG.vehicleChanges({"a", "b"}, {"b", "c"})
|
||||
check("FL15 tilføjet", table.concat(added, ","), "c")
|
||||
check("FL15 fjernet", table.concat(removed, ","), "a")
|
||||
|
||||
-- ---- forhåndsvisning (til controllerens opslag) ----
|
||||
local markers = {b1 = {101, 102}, b2 = {201}}
|
||||
local preview = FG.previewRun(withSource.draft, function(id) return markers[id] or {} end)
|
||||
check("FL16 kilder -> markører", table.concat(preview.sourceWayPointIds, ","), "201")
|
||||
local previewIn = FG.previewRun(withTarget.draft, function(id) return markers[id] or {} end)
|
||||
check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), "101,102")
|
||||
check("FL16 primært mål", previewIn.targetWayPointId, 101)
|
||||
check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true)
|
||||
|
||||
|
||||
-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ----
|
||||
local soloCtx = ctx({
|
||||
vehicles = {
|
||||
{id = "s1", label = "Solo 1", text = "Solo 1", solo = true},
|
||||
{id = "s2", label = "Solo 2", text = "Solo 2", solo = true},
|
||||
{id = "s3", label = "Solo 3", text = "Solo 3", solo = true},
|
||||
{id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"},
|
||||
},
|
||||
wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}},
|
||||
})
|
||||
local soloGuide = FG.open(newRun("supply"), soloCtx)
|
||||
local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true})
|
||||
check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1")
|
||||
local two = FG.choose(one, "vehicleIds", {item = "s2", on = true})
|
||||
check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2")
|
||||
check("WG2 første beholder sin", two.draft.wagons.s1, "w1")
|
||||
local three = FG.choose(two, "vehicleIds", {item = "s3", on = true})
|
||||
check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil)
|
||||
local soloRows = FG.rows(at(three, "tractors"))
|
||||
local wagonRow = findRow(soloRows, "wagon", "s1")
|
||||
check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice")
|
||||
check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1")
|
||||
check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1)
|
||||
local noFree = false
|
||||
for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end
|
||||
check("WG5 solo uden ledig vogn får besked", noFree, true)
|
||||
check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil)
|
||||
local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"})
|
||||
check("WG6 skift vogn", swapped.draft.wagons.s1, "w2")
|
||||
local off = FG.choose(two, "vehicleIds", {item = "s1", on = false})
|
||||
check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil)
|
||||
local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true})
|
||||
check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1")
|
||||
check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2")
|
||||
check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil)
|
||||
local remembered = newRun("supply", {vehicleIds = {"s2"}})
|
||||
remembered.wagons = {s2 = "w2"}
|
||||
local reopened = FG.open(remembered, soloCtx)
|
||||
check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2")
|
||||
check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1")
|
||||
check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2")
|
||||
|
||||
|
||||
-- regler: kun de relevante for varernes politik
|
||||
local storeOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}}), ctx())
|
||||
local function keys(rows) local r = {} for _, row in ipairs(rows) do if row.id then table.insert(r, row.id) end end return table.concat(r, ",") end
|
||||
check("RL1 kun lager: ingen salgsvalg", keys(FG.rows(at(storeOnly, "rules"))), "storeAbovePercent,collectRound")
|
||||
local sellOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sell"}}}), ctx())
|
||||
check("RL2 kun salg: ingen lagervalg", keys(FG.rows(at(sellOnly, "rules"))), "sellAtPercent,sellAbovePercent,collectRound")
|
||||
local function ruleKeys(guide)
|
||||
for _, line in ipairs(FG.summary(guide)) do
|
||||
if line.step == "rules" then local r = {} for _, p in ipairs(line.parts) do table.insert(r, p.key) end return table.concat(r, ",") end
|
||||
end
|
||||
end
|
||||
check("RL3 resumé kun lager", ruleKeys(storeOnly), "spu_guide_flow_sumStoreAbove")
|
||||
check("RL4 resumé kun salg", ruleKeys(sellOnly), "spu_guide_flow_sumSellAt")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -57,13 +57,49 @@ local buildings = {
|
||||
{id = "a", name = "Anlæg", hasMarker = true},
|
||||
}
|
||||
local chosen = {s10 = true, y = true}
|
||||
check("FV3 standard: valgte først, så med markør", ids(V.buildingRows(buildings, chosen, false)), "y,s10,a,s2")
|
||||
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,s10,a,s2,x")
|
||||
-- fast rækkefølge (et tryk må ikke flytte rækken — in-game 26/9 hoppede valgte drops op i toppen)
|
||||
check("FV3 standard: fast rækkefølge efter navn, valgte bliver hvor de er", ids(V.buildingRows(buildings, chosen, false)), "y,a,s2,s10")
|
||||
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,a,s2,s10,x")
|
||||
-- ens bygningsnavne (5 x "BGA (1) : DOB 115") sorteres efter markørnavnet
|
||||
local drops = {
|
||||
{id = "d5", name = "DOB", label = "Biogas Drop 5", hasMarker = true},
|
||||
{id = "d3", name = "DOB", label = "Biogas Drop 3", hasMarker = true},
|
||||
{id = "d1", name = "DOB", label = "Biogas Drop 1", hasMarker = true},
|
||||
{id = "d4", name = "DOB", label = "Biogas Drop 4", hasMarker = true},
|
||||
{id = "d2", name = "DOB", label = "Biogas Drop 2", hasMarker = true},
|
||||
}
|
||||
check("FV3 ens navne efter markør", ids(V.buildingRows(drops, {d2 = true}, false)), "d1,d2,d3,d4,d5")
|
||||
|
||||
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
|
||||
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
|
||||
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
|
||||
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
|
||||
|
||||
-- FV-wrap: lange tekstrækker ombrydes ved mellemrum; indrykning gentages; kort tekst uændret
|
||||
local wrapped = V.wrapText(" Prepared and sown with Sugar Beet as soon as it is sowing time for it", 30)
|
||||
check("FVwrap flere linjer", #wrapped > 1, true)
|
||||
check("FVwrap første linje indrykket", string.sub(wrapped[1], 1, 4), " ")
|
||||
check("FVwrap anden linje indrykket", string.sub(wrapped[2], 1, 4), " ")
|
||||
local maxLen = 0
|
||||
for _, line in ipairs(wrapped) do if #line > maxLen then maxLen = #line end end
|
||||
check("FVwrap ingen linje over grænsen", maxLen <= 30, true)
|
||||
check("FVwrap intet ord tabt", table.concat(wrapped, " "):gsub("%s+", " "):gsub("^ ", ""), "Prepared and sown with Sugar Beet as soon as it is sowing time for it")
|
||||
check("FVwrap kort tekst", V.wrapText("Kort", 30)[1], "Kort")
|
||||
check("FVwrap langt ord deles ikke", V.wrapText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10)[1], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
||||
|
||||
|
||||
-- salgs-/lagerregler vises kun når en vare bruger dem (in-game 29/9: 'sell from 90 %' på et rent lager-flow)
|
||||
local sells, stores = ADFlowView.policyUse({"store"})
|
||||
check("politik: kun lager -> sælger ikke", sells, false)
|
||||
check("politik: kun lager -> lagrer", stores, true)
|
||||
sells, stores = ADFlowView.policyUse({"sell"})
|
||||
check("politik: kun salg", tostring(sells) .. tostring(stores), "truefalse")
|
||||
sells, stores = ADFlowView.policyUse({"sellElseStore", "off"})
|
||||
check("politik: sælg ellers lager -> begge", tostring(sells) .. tostring(stores), "truetrue")
|
||||
sells, stores = ADFlowView.policyUse({})
|
||||
check("politik: ingen varer endnu -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
|
||||
sells, stores = ADFlowView.policyUse({"off"})
|
||||
check("politik: kun fra -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||
dofile("FS25_ADSmartPickup/adFlowWagons.lua")
|
||||
local W = ADFlowWagons
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
-- gem/indlæs: traktor -> vogn
|
||||
local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"})
|
||||
check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2")
|
||||
local parsed = W.parse(text)
|
||||
check("indlæs: A", parsed.vehicleA, "wagon1")
|
||||
check("indlæs: B", parsed.vehicleB, "wagon2")
|
||||
check("indlæs: tom", next(W.parse("")), nil)
|
||||
check("indlæs: nil", next(W.parse(nil)), nil)
|
||||
check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil)
|
||||
-- husk vogn: ny tabel, gammel urørt
|
||||
local before = {t1 = "w1"}
|
||||
local after = W.remember(before, "t1", "w9")
|
||||
check("husk: ny vogn", after.t1, "w9")
|
||||
check("husk: original urørt", before.t1, "w1")
|
||||
check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after)
|
||||
-- start: hent / vent / start
|
||||
check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start")
|
||||
check("start: ingen husket vogn", W.startAction({}), "start")
|
||||
check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start")
|
||||
check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start")
|
||||
check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait")
|
||||
check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait")
|
||||
check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch")
|
||||
-- ejer: flowets vogn er flowets — også når flowet er stoppet (in-game 29/9: marksilo lånte Cow Food-vognen)
|
||||
local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}}
|
||||
check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true)
|
||||
check("ejer: stoppet flow holder stadig", W.isHeld(runs, "w2"), true)
|
||||
check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false)
|
||||
check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2)
|
||||
-- bygning væk: to tjek i træk
|
||||
local count, gone = W.missingStep(0, true)
|
||||
check("væk: første tjek -> ikke endnu", gone, false)
|
||||
count, gone = W.missingStep(count, true)
|
||||
check("væk: andet tjek -> væk", gone, true)
|
||||
count, gone = W.missingStep(1, false)
|
||||
check("væk: fundet igen nulstiller", count, 0)
|
||||
check("væk: fundet igen -> ikke væk", gone, false)
|
||||
-- manglende bygninger i et flow
|
||||
local exists = {a = true, b = true}
|
||||
check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists, exists), 0)
|
||||
check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists, {a = true, c = true})[1], "c")
|
||||
-- review v1.51
|
||||
check("R1 traktoren er lånt ud af puljen -> vent", W.startAction({tractorBusy = true, hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "wait")
|
||||
local runs2 = {{id = 1, stopped = true, wagons = {t1 = "w1"}}, {id = 2, stopped = false, wagons = {t9 = "w1"}}}
|
||||
check("R4 holdt hvis ET kørende flow har vognen", W.isHeld(runs2, "w1"), true)
|
||||
local seen = {a = true, c = true}
|
||||
check("R+ bygning aldrig set i sessionen tæller ikke som væk", #W.missingBuildings({targetBuildingIds = {"b"}}, {a = true}, seen), 0)
|
||||
check("R+ bygning set og nu væk", W.missingBuildings({targetBuildingIds = {"c"}}, {a = true}, seen)[1], "c")
|
||||
-- gæt flowets vogn ud fra parkeringsnavnet (ingen husket vogn: vognene var sat på plads før v1.51)
|
||||
local cands = {{id = "v1", parkName = "P Cow Food vogn 1"}, {id = "v3", parkName = "P Cow Food vogn 3"},
|
||||
{id = "g1", parkName = "P Goat Food vogn 1"}, {id = "v2", parkName = "P Cow Food vogn 2"}}
|
||||
check("gæt: samme nummer", W.guessWagon("Cow Food", 3, cands, {}), "v3")
|
||||
check("gæt: nummer optaget -> anden med flowets navn", W.guessWagon("Cow Food", 3, cands, {v3 = true}), "v1")
|
||||
check("gæt: andet flow", W.guessWagon("Goat Food", 2, cands, {}), "g1")
|
||||
check("gæt: intet passer", W.guessWagon("Pig Food", 1, cands, {}), nil)
|
||||
check("gæt: store/små bogstaver", W.guessWagon("cow food", 2, cands, {}), "v2")
|
||||
check("gæt: 'Cow Food 1' matcher ikke 'Cow Food 10'", W.guessWagon("Cow Food", 1, {{id = "x", parkName = "P Cow Food vogn 10"}, {id = "y", parkName = "P Cow Food vogn 1"}}, {}), "y")
|
||||
-- tøm vognen før den parkeres (flow stoppet): mål -> silo/fabrik -> hjem
|
||||
check("tøm: tom vogn -> hjem", W.emptyStep("start", {cargo = 0}), "home")
|
||||
check("tøm: last -> flowets mål", W.emptyStep("start", {cargo = 5000, hasTarget = true}), "target")
|
||||
check("tøm: last uden mål -> silo", W.emptyStep("start", {cargo = 5000, hasTarget = false}), "dump")
|
||||
check("tøm: kører stadig -> vent", W.emptyStep("target", {cargo = 5000, adActive = true}), "wait")
|
||||
check("tøm: mål fuldt, rest -> silo", W.emptyStep("target", {cargo = 2000}), "dump")
|
||||
check("tøm: målet tog det hele -> hjem", W.emptyStep("target", {cargo = 0}), "home")
|
||||
check("tøm: silo tog det -> hjem", W.emptyStep("dump", {cargo = 0}), "home")
|
||||
check("tøm: silo kunne ikke -> giv op, hjem", W.emptyStep("dump", {cargo = 2000}), "giveUp")
|
||||
check("tøm: for længe -> giv op", W.emptyStep("target", {cargo = 5000, adActive = true, elapsedMs = W.EMPTY_TIMEOUT_MS}), "giveUp")
|
||||
-- flowet startes igen mens traktoren tømmer/kører hjem fra et stop: hjemturen afbrydes, den kører videre med vognen
|
||||
-- (in-game 29/9: Stop+Start med 1 s mellemrum -> tømning ved forkert mål i 10 min, vogn sat af, traktor i garage)
|
||||
check("genstart: tømmer -> afbryd", W.abortHomeTripOnStart({emptying = true}), true)
|
||||
check("genstart: flowets hjemtur -> afbryd", W.abortHomeTripOnStart({flowHoming = true}), true)
|
||||
check("genstart: puljens hjemtur (markarbejde) -> rør ikke", W.abortHomeTripOnStart({poolReturning = true}), false)
|
||||
check("genstart: intet i gang", W.abortHomeTripOnStart({}), false)
|
||||
-- tøm-mål: kun flowets egne mål — AD's 2. markør kan være forældet (æg-markør) eller et Wait-punkt (in-game 29/9)
|
||||
check("tøm-mål: AD's mål er et af flowets", W.emptyTarget(12638, {12636, 12638}), 12638)
|
||||
check("tøm-mål: forældet/Wait-markør -> flowets første mål", W.emptyTarget(12658, {12636, 12638}), 12636)
|
||||
check("tøm-mål: intet AD-mål -> flowets første", W.emptyTarget(nil, {12636}), 12636)
|
||||
check("tøm-mål: flow uden mål -> intet (silo)", W.emptyTarget(12658, {}), nil)
|
||||
check("tøm-mål: ingen liste -> intet", W.emptyTarget(12658, nil), nil)
|
||||
-- ledig traktor (puljen/markarbejde/marksilo må låne den): kun solo fra et stoppet flow
|
||||
check("ledig: intet flow", W.tractorFree({}), true)
|
||||
check("ledig: kørende flow", W.tractorFree({hasRun = true}), false)
|
||||
check("ledig: stoppet flow, solo", W.tractorFree({hasRun = true, stopped = true}), true)
|
||||
check("ledig: stoppet flow, flow-vogn bagpå", W.tractorFree({hasRun = true, stopped = true, carriesFlowWagon = true}), false)
|
||||
check("ledig: stoppet flow, tømmer/kører hjem", W.tractorFree({hasRun = true, stopped = true, homeTrip = true}), false)
|
||||
-- venteårsag i loggen (før stod der altid "i brug andetsteds")
|
||||
check("vent-årsag: traktor optaget", W.waitReason({tractorBusy = true}), "tractorBusy")
|
||||
check("vent-årsag: vogn på en anden", W.waitReason({wagonAttached = true}), "wagonAttached")
|
||||
check("vent-årsag: vogn reserveret", W.waitReason({wagonBusy = true}), "wagonReserved")
|
||||
-- tømning går i stå ved et fuldt mål (in-game 29/9: fåre-/BGA-vogne stod 10 min ved fuldt mål efter Stop alt)
|
||||
check("tøm: står ved fuldt mål -> silo", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = true}), "dump")
|
||||
check("tøm: står ved silo uden at tømme -> næste silo", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = 1}), "dump")
|
||||
check("tøm: 3 siloer prøvet -> giv op", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = W.MAX_DUMP_ATTEMPTS}), "giveUp")
|
||||
check("tøm: kører mod målet (ikke i stå) -> vent", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = false}), "wait")
|
||||
check("i stå: tæt på, uændret længe", W.isStalled({nearTarget = true, sinceChangeMs = W.EMPTY_STALL_MS}), true)
|
||||
check("i stå: tæt på, lige ankommet", W.isStalled({nearTarget = true, sinceChangeMs = 5000}), false)
|
||||
check("i stå: på vej (langt væk)", W.isStalled({nearTarget = false, sinceChangeMs = 999999}), false)
|
||||
-- hent vogn: ét nyt forsøg efter "stoppedAway" (AD standsede undervejs — fx spærret af en anden traktor)
|
||||
check("hent igen: stoppedAway første gang", W.retryFetch("stoppedAway (37 m fra markøren)", 0), true)
|
||||
check("hent igen: stoppedAway anden gang -> nej", W.retryFetch("stoppedAway", 1), false)
|
||||
check("hent igen: noAttachable -> nej (markøren skal rettes)", W.retryFetch("noAttachable", 0), false)
|
||||
-- manuel påkobling (spilleren sendte traktoren til Connect): traktor i et kørende flow startes bagefter
|
||||
-- (in-game 29/9: Cow Food 3 hentede sin vogn manuelt efter et fejlet forsøg og blev holdende)
|
||||
check("manuel kobling: kørende flow, egen vogn -> start", W.startAfterManualAttach({flowRunning = true, ownWagon = true}), true)
|
||||
check("manuel kobling: stoppet flow -> nej", W.startAfterManualAttach({flowRunning = false, ownWagon = true}), false)
|
||||
check("manuel kobling: AD kører allerede -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = true, adActive = true}), false)
|
||||
check("manuel kobling: fremmed vogn -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = false}), false)
|
||||
-- husk vogn ved hjemtur: kun når flowet KØRTE med den (stop); ikke ved oprydning efter indlæsning, hvor traktoren
|
||||
-- kan have et puljeredskab på fra et lån (in-game 29/9: Biogas Silage "huskede" en læssevogn fra markarbejdet)
|
||||
check("husk ved stop af kørende flow", W.shouldRememberOnHome("stop"), true)
|
||||
check("husk ikke ved oprydning efter indlæsning", W.shouldRememberOnHome("afterLoad"), false)
|
||||
-- forældreløs påkobling (hentet før genindlæsning, opgaven tabt): sættes tilbage efter 2 min uden ejer
|
||||
check("forældreløs: ingen ejer, længe nok -> hjem", W.orphanAction({owned = false, idleMs = W.ORPHAN_MS}), "home")
|
||||
check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = W.ORPHAN_MS}), "none")
|
||||
check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait")
|
||||
-- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad)
|
||||
check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none")
|
||||
|
||||
-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk)
|
||||
local ranked = W.rankWagons("Sugarbeet Cut", {
|
||||
{id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"},
|
||||
{id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}})
|
||||
local order = {}
|
||||
for _, w in ipairs(ranked) do table.insert(order, w.id) end
|
||||
check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a")
|
||||
check("rank: tom liste", #W.rankWagons("X", {}), 0)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
||||
local G = ADGuide
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
-- trin a..e + summary; b vises kun når draft.showB, d aldrig
|
||||
local def = {
|
||||
order = {"a", "b", "c", "d", "e", "summary"},
|
||||
isVisible = function(step, guide)
|
||||
if step == "b" then return guide.draft.showB == true end
|
||||
if step == "d" then return false end
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local guide = G.new(def, {showB = false, value = 1}, {x = 1})
|
||||
check("G1 start på første synlige", guide.step, "a")
|
||||
local index, total = G.position(guide)
|
||||
check("G1 position", index, 1)
|
||||
check("G1 total over synlige", total, 4)
|
||||
check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
|
||||
|
||||
-- næste springer skjulte over
|
||||
local g2, event = G.next(guide)
|
||||
check("G2 næste springer b over", g2.step, "c")
|
||||
check("G2 intet event", event, nil)
|
||||
check("G2 gammel guide urørt", guide.step, "a")
|
||||
local g3 = G.next(g2)
|
||||
check("G3 springer d over", g3.step, "e")
|
||||
check("G3 position", (G.position(g3)), 3)
|
||||
local g4 = G.next(g3)
|
||||
check("G4 summary er sidst", G.isLast(g4), true)
|
||||
local _, done = G.next(g4)
|
||||
check("G4 næste på sidste = done", done, "done")
|
||||
|
||||
-- tilbage
|
||||
local back = G.back(g3)
|
||||
check("G5 tilbage til c", back.step, "c")
|
||||
local _, cancel = G.back(guide)
|
||||
check("G5 tilbage på første = cancel", cancel, "cancel")
|
||||
|
||||
-- valg der gør et trin synligt ændrer tælling og næste
|
||||
local shown = G.withDraft(guide, {showB = true, value = 1})
|
||||
check("G6 withDraft ny guide", guide.draft.showB, false)
|
||||
check("G6 b synlig", G.next(shown).step, "b")
|
||||
check("G6 total", select(2, G.position(shown)), 5)
|
||||
|
||||
-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
|
||||
local onB = G.new(def, {showB = true}, {}, {step = "b"})
|
||||
local hidden = G.withDraft(onB, {showB = false})
|
||||
check("G7 position nil for skjult trin", (G.position(hidden)), nil)
|
||||
check("G7 næste efter skjult", G.next(hidden).step, "c")
|
||||
check("G7 tilbage fra skjult", G.back(hidden).step, "a")
|
||||
|
||||
-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
|
||||
local fromSummary = G.jump(g4, "c", "summary")
|
||||
check("G8 spring trin", fromSummary.step, "c")
|
||||
check("G8 single", fromSummary.single, true)
|
||||
local returned, ev = G.next(fromSummary)
|
||||
check("G8 retur til summary", returned.step, "summary")
|
||||
check("G8 ikke single mere", returned.single, false)
|
||||
check("G8 intet event", ev, nil)
|
||||
-- tilbage i spring gendanner kladden fra før springet
|
||||
local changed = G.withDraft(fromSummary, {showB = false, value = 99})
|
||||
local undone = G.back(changed)
|
||||
check("G9 tilbage gendanner kladde", undone.draft.value, 1)
|
||||
check("G9 tilbage til summary", undone.step, "summary")
|
||||
|
||||
-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
|
||||
local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
|
||||
local _, singleDone = G.next(single)
|
||||
check("G10 single close næste = done", singleDone, "done")
|
||||
local _, singleCancel = G.back(single)
|
||||
check("G10 single close tilbage = cancel", singleCancel, "cancel")
|
||||
check("G10 single isLast", G.isLast(single), true)
|
||||
|
||||
-- withContext
|
||||
local ctxGuide = G.withContext(guide, {x = 2})
|
||||
check("G11 ny context", ctxGuide.context.x, 2)
|
||||
check("G11 gammel context", guide.context.x, 1)
|
||||
|
||||
-- deepCopy / toggleItem
|
||||
local original = {list = {1, 2}, nested = {a = {b = 1}}}
|
||||
local copy = G.deepCopy(original)
|
||||
copy.list[1] = 9
|
||||
copy.nested.a.b = 7
|
||||
check("G12 deepCopy liste", original.list[1], 1)
|
||||
check("G12 deepCopy indlejret", original.nested.a.b, 1)
|
||||
local list = {"x", "y"}
|
||||
check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
|
||||
check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
|
||||
check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
|
||||
check("G13 original urørt", #list, 2)
|
||||
|
||||
-- tekster
|
||||
local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
|
||||
local function T(key) return texts[key] or key end
|
||||
check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
|
||||
check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
|
||||
check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
|
||||
check("G14 prefix", G.labelText({key = "plain", prefix = "Mælk: "}, T), "Mælk: Ingen")
|
||||
check("G14 nil label", G.labelText(nil, T), "")
|
||||
check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
|
||||
check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user