50 KiB
Marksilo-flow 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: Et fast flow der hvert 60. s kigger alle marksiloer igennem og låner op til x ledige vogne til at køre varerne hjem til nærmeste valgte gårdlager.
Architecture: Ren planlægger (adFieldSiloPlanner.lua) + ren opsætningsmodel (adFieldSiloConfig.lua), begge testet med luajit. Et tyndt spil-lag (adFieldSilos.lua) samler input fra spillet, låner vogne via AutoDrive "Hent og aflever" og frigiver dem. Egen underfane gui/SmartPickupFieldSiloTab.lua. Markflådens getRigs markerer lånte vogne som optaget.
Tech Stack: Lua 5.1 (FS25 / LuaJIT), AutoDrive 3.0.1.2 API, Courseplay-specs via ADCpBridge, tests med luajit tests/test_*.lua fra repo-roden.
Spec: docs/superpowers/specs/2026-09-27-marksilo-flow-design.md
Global Constraints
- Branch:
feature/marksilo-flow; merge tilmainefter review; version1.36.0.0imodDesc.xml. - Log-præfiks
ADSmartPickup:(viaLogging.info/warning), tekster på dansk i log, l10n-nøglerspu_fs_*i bådel10n_da.xmlogl10n_en.xml. - Tal fra spec'en: planlægning hvert
60000ms,maxVehicles1–10 standard2,minLiters∈ {5000, 10000, 25000, 50000} standard10000, vagt120000ms, pause efter vagt600000ms, marksilo-afstand =ADFields.FIELD_SILO_DISTANCE(150). - Kun vogne med rolle
unloaderfraADFieldWork.getRigsmå lånes;enabled and not busy and not controlled. - Gårdlagre er bygninger med
kind == "silo"; en valgt gårdlager-bygning er aldrig marksilo. - Lån gemmes IKKE; opsætningen gemmes i
ADSmartPickup_fieldsilos.xmlsammen med runs. - Ingen mutation af input i de rene moduler (projektets stil: ny tabel ud).
Review Focus
- Vogn med rest af en ANDEN vare på sig → må ikke lånes til en ny vare (planner-test "vogn med anden vare springes over").
- Gårdlager fyldes op midt i runden → to vogne må ikke begge sendes til et lager med plads til kun én (planner reserverer plads pr. lager; test).
- Marksilo med flere varer (fx hvede + raps) → hver vare er sin egen opgave, og lån tælles pr. (silo, vare) (test).
- Spilleren sætter sig i en lånt vogn → lånet slettes straks, og vognen bruges ikke igen før den er ledig (loan-lifecycle-test i Task 3).
- Flowet slås fra mens vogne er ude → AD stoppes på lånte vogne, og lånene slettes (Task 3-test).
File Structure
| Fil | Ansvar |
|---|---|
Create FS25_ADSmartPickup/adFieldSiloPlanner.lua |
Ren planlægning: kvalificering, rækkefølge, vogn- og lagervalg |
Create FS25_ADSmartPickup/adFieldSiloConfig.lua |
Ren opsætningsmodel + records til/fra XML |
Create FS25_ADSmartPickup/adFieldSilos.lua |
Spil-lag: input, lån, AD-start, frigivelse, vagt, gem/indlæs |
Create FS25_ADSmartPickup/gui/SmartPickupFieldSiloTab.lua |
Underfanen: indstillinger + status |
Create tests/test_adFieldSiloPlanner.lua, tests/test_adFieldSiloConfig.lua, tests/test_adFieldSilos.lua |
Tests |
Modify FS25_ADSmartPickup/modDesc.xml |
sourceFiles + version |
Modify FS25_ADSmartPickup/adSmartPickup.lua |
tick + source af fane-fil |
Modify FS25_ADSmartPickup/adFieldWork.lua:133 |
busy inkluderer lån |
Modify FS25_ADSmartPickup/adRunsController.lua (save/load) |
gem/indlæs opsætning |
Modify FS25_ADSmartPickup/gui/SmartPickupFrame.lua |
fane-konstanter, rebuild, knapper, klik-routing |
Modify FS25_ADSmartPickup/l10n/l10n_da.xml, l10n_en.xml |
tekster |
Task 1: Planlæggeren
Files:
- Create:
FS25_ADSmartPickup/adFieldSiloPlanner.lua - Test:
tests/test_adFieldSiloPlanner.lua
Interfaces:
-
Produces:
ADFieldSiloPlanner.key(siloId, fillType) -> string;ADFieldSiloPlanner.plan(input) -> {dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeId, storeMarkerId, liters}}, status = {{siloId, siloName, fillType, liters, onWay, state}}, freeSlots}hvorinput = {silos = {{id, name, markerId|nil, x, z, stock = {[fillType] = liters}}}, stores = {{id, markerId, x, z, free = {[fillType] = liters}}}, loans = {{siloId, fillType, liters}}, vehicles = {{id, x, z, capacity, carries = {[fillType] = true}, cargo = fillType|nil}}, paused = {[key] = true}, maxVehicles, minLiters};state∈"ok" | "noMarker" | "noFarmStore" | "paused". -
Step 1: Write the failing test —
tests/test_adFieldSiloPlanner.lua:
-- 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)
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_adFieldSiloPlanner.lua
Expected: FAIL (fil adFieldSiloPlanner.lua findes ikke).
- Step 3: Write implementation —
FS25_ADSmartPickup/adFieldSiloPlanner.lua:
-- 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. 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
-- Nærmeste lager med plads til hele turen; ellers det med mest plads (over tærsklen).
local function chooseStore(stores, silo, fillType, liters, minLiters, reserved)
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 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
return best or most
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
function ADFieldSiloPlanner.plan(input)
local minLiters = input.minLiters or 10000
local reserved, onWay = {}, {}
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)
end
local status, tasks = {}, {}
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)
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 = chooseStore(input.stores, task.silo, task.fillType, liters, minLiters, reserved)
if store == nil then break 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
- Step 4: Run test to verify it passes
Run: luajit tests/test_adFieldSiloPlanner.lua
Expected: ALLE TESTS OK
- Step 5: Commit
git add FS25_ADSmartPickup/adFieldSiloPlanner.lua tests/test_adFieldSiloPlanner.lua
git commit -m "feat(marksilo): ren planlægger — kvalificering, fuldeste først, x-loft, vogn- og lagervalg med reservation"
Task 2: Opsætningsmodellen
Files:
- Create:
FS25_ADSmartPickup/adFieldSiloConfig.lua - Test:
tests/test_adFieldSiloConfig.lua
Interfaces:
-
Produces:
ADFieldSiloConfig.new() -> cfg({enabled=false, storeIds={}, maxVehicles=2, minLiters=10000});ADFieldSiloConfig.with(cfg, changes) -> cfg(changes ⊆{enabled, maxVehicles, minLiters}; clamps);ADFieldSiloConfig.toggleStore(cfg, id) -> cfg;ADFieldSiloConfig.hasStore(cfg, id) -> bool;ADFieldSiloConfig.MIN_LITERS_CHOICES = {5000, 10000, 25000, 50000};ADFieldSiloConfig.MAX_VEHICLES = 10;ADFieldSiloConfig.toRecord(cfg) -> {enabled=0|1, stores="id id", maxVehicles, minLiters};ADFieldSiloConfig.fromRecord(record) -> cfg. -
Step 1: Write the failing test —
tests/test_adFieldSiloConfig.lua:
-- 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)
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_adFieldSiloConfig.lua — Expected: FAIL (fil findes ikke).
- Step 3: Write implementation —
FS25_ADSmartPickup/adFieldSiloConfig.lua:
-- 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
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}
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 snapLiters(value)
value = tonumber(value)
if value == nil then return ADFieldSiloConfig.DEFAULT_MIN_LITERS end
local best, bestDiff = nil, nil
for _, choice in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do
local diff = math.abs(choice - value)
if bestDiff == nil or diff < bestDiff then best, bestDiff = choice, diff end
end
return best
end
function ADFieldSiloConfig.new()
return {enabled = false, storeIds = {}, maxVehicles = ADFieldSiloConfig.DEFAULT_MAX_VEHICLES, minLiters = ADFieldSiloConfig.DEFAULT_MIN_LITERS}
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
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}
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)
return cfg
end
-
Step 4: Run test —
luajit tests/test_adFieldSiloConfig.lua→ALLE TESTS OK -
Step 5: Commit
git add FS25_ADSmartPickup/adFieldSiloConfig.lua tests/test_adFieldSiloConfig.lua
git commit -m "feat(marksilo): opsætningsmodel (til/fra, gårdlagre, x 1-10, tærskel) + records"
Task 3: Spil-laget — lån, afsendelse, frigivelse, gem/indlæs
Files:
- Create:
FS25_ADSmartPickup/adFieldSilos.lua - Modify:
FS25_ADSmartPickup/adFieldWork.lua(eksportérstartAd,enableParkAtJobFinished;busyinkluderer lån) - Modify:
FS25_ADSmartPickup/adSmartPickup.lua(tick iupdate) - Modify:
FS25_ADSmartPickup/adRunsController.lua(save/loadkalderADFieldSilos.save/load) - Modify:
FS25_ADSmartPickup/modDesc.xml(sourceFiles:adFieldSiloPlanner.lua,adFieldSiloConfig.lua,adFieldSilos.lua— efteradFieldWork.lua) - Test:
tests/test_adFieldSilos.lua
Interfaces:
-
Consumes:
ADFieldSiloPlanner.plan/key(Task 1),ADFieldSiloConfig.*(Task 2),ADFieldWork.getRigs(adEnv),ADFields.list(),ADFields.fieldSilos(adEnv, farmId, info),ADBuildings.list(adEnv, farmId),ADBuildings.getMarkers(building, role),ADRunsController.getVehicleId(vehicle),ADRuns.vehicleLabel(vehicle),ADSmartPickup.getFarmId(vehicle). -
Produces:
ADFieldSilos.config(cfg),ADFieldSilos.setConfig(cfg),ADFieldSilos.isLoaned(vehicle) -> bool,ADFieldSilos.tick(adEnv, dt),ADFieldSilos.lastPlan -> plan|nil,ADFieldSilos.loanCount() -> n,ADFieldSilos.stopAll(adEnv),ADFieldSilos.save(),ADFieldSilos.load(),ADFieldSilos.releaseReason(loan, vehicleState, nowMs) -> nil|"done"|"player"|"stuck"(ren, testet),ADFieldWork.startAd(adEnv, vehicle, mode, firstId, secondId),ADFieldWork.enableParkAtJobFinished(vehicle). -
Step 1: Write the failing test —
tests/test_adFieldSilos.lua(tester den rene frigivelsesregel):
-- 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 -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 5000), "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)
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 —
luajit tests/test_adFieldSilos.lua→ FAIL (fil findes ikke). -
Step 3: Export helpers from adFieldWork — i
adFieldWork.luaefter den lokalestartAd-funktion (ogenableParkAtJobFinished), tilføj:
ADFieldWork.startAd = startAd
ADFieldWork.enableParkAtJobFinished = enableParkAtJobFinished
og i ADFieldWork.getRigs ændr busy-linjen til:
busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),
- Step 4: Write implementation —
FS25_ADSmartPickup/adFieldSilos.lua:
-- AD Field Silos
-- Marksilo-flowet i spillet: samler marksiloer, gårdlagre og ledige vogne, lader ADFieldSiloPlanner vælge,
-- 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.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}
function ADFieldSilos.releaseReason(loan, vehicleState, now)
if vehicleState.controlled then return "player" end
if not vehicleState.adActive then return "done" end
if vehicleState.moving then return nil end
if (vehicleState.fill or 0) > (loan.lastFill or 0) then return nil end
if now - (loan.lastProgressMs or loan.startedMs or 0) > 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 and ADFieldSilos.adEnv ~= nil 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
-- 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 = {}, {}
for _, building in ipairs(ADBuildings.list(adEnv, fid)) do
byId[building.id] = building
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
local silos, seen = {}, {}
for _, info in ipairs(ADFields.list()) do
for _, building in ipairs(ADFields.fieldSilos(adEnv, fid, info)) do
if not seen[building.id] and not ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then
seen[building.id] = true
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
table.insert(silos, {id = building.id, name = building.name, markerId = firstMarker(building, "load"),
x = x, z = z, stock = siloStock(building.placeable)})
end
end
end
return silos, stores
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) > 0 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 = {}, {}
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
if rig.role == "unloader" and rig.enabled and not rig.busy and not rig.controlled 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
return vehicles, byId
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)
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
ADFieldWork.enableParkAtJobFinished(vehicle)
module:raiseDirtyFlag()
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_PICKUPANDDELIVER, order.siloMarkerId, order.storeMarkerId)
local silo, store = silosById[order.siloId], storesById[order.storeId]
loans[vehicle] = {siloId = order.siloId, 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
local function release(adEnv, vehicle, reason)
local loan = loans[vehicle]
loans[vehicle] = nil
if loan == nil then return end
if reason == "stuck" then
local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType)
paused[k] = nowMs + ADFieldSilos.PAUSE_MS
pcall(vehicle.stopAutoDrive, vehicle)
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kom ikke i gang ved %s (%s) på 2 min — stoppet; siloen springes over i 10 min",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(loan.siloName), fillTypeName(loan.fillType))
elseif reason == "stopped" then
pcall(vehicle.stopAutoDrive, vehicle)
end
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' frigivet (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(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
local function checkLoans(adEnv)
local list = {}
for vehicle in pairs(loans) do table.insert(list, vehicle) end
for _, vehicle in ipairs(list) do
local loan = loans[vehicle]
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,
}
if vehicleState.moving or fill > (loan.lastFill or 0) then loan.lastProgressMs = nowMs end
loan.lastFill = math.max(loan.lastFill or 0, fill)
local reason = ADFieldSilos.releaseReason(loan, vehicleState, nowMs)
if reason ~= nil then release(adEnv, vehicle, reason) 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, fillType = loan.fillType, liters = loan.liters}) end
return list
end
function ADFieldSilos.planNow(adEnv)
local cfg = ADFieldSilos.config
local silos, stores = ADFieldSilos.collectBuildings(adEnv)
local vehicles, vehiclesById = ADFieldSilos.collectVehicles(adEnv)
local plan = ADFieldSiloPlanner.plan({silos = silos, stores = stores, loans = loanList(), vehicles = vehicles,
paused = currentPaused(), maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters})
ADFieldSilos.lastPlan = plan
if not cfg.enabled 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 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: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"),
})
xmlFile:delete()
end
- Step 5: Wire tick, save, load, modDesc
adSmartPickup.lua — i ADSmartPickup:update(dt), lige efter ADFieldJobs.tick-blokken:
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
adRunsController.lua — i ADRunsController.save() efter pcall(ADFieldJobs.save)-blokken: if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end; i ADRunsController.load(adEnv) efter pcall(ADRunsController.migrateBuildings, adEnv): if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end.
modDesc.xml — tilføj efter linjen med adFieldWork.lua i <extraSourceFiles>:
<sourceFile filename="adFieldSiloPlanner.lua"/>
<sourceFile filename="adFieldSiloConfig.lua"/>
<sourceFile filename="adFieldSilos.lua"/>
- Step 6: Run all tests + syntax
Run: for f in FS25_ADSmartPickup/*.lua; do luajit -bl "$f" >/dev/null || echo "SYNTAKS $f"; done; for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo "FEJLER $t"; done
Expected: ingen output.
- Step 7: Commit
git add FS25_ADSmartPickup/adFieldSilos.lua FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/adSmartPickup.lua FS25_ADSmartPickup/adRunsController.lua FS25_ADSmartPickup/modDesc.xml tests/test_adFieldSilos.lua
git commit -m "feat(marksilo): låner ledige tømmevogne hvert minut, AD hent-og-aflever til gårdlager, frigiver ved færdig/spiller/2 min stilstand; opsætning gemmes"
Task 4: Underfanen "Marksiloer"
Files:
- Create:
FS25_ADSmartPickup/gui/SmartPickupFieldSiloTab.lua - Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.lua(konstanter,rebuild,refreshButtons,refreshLive,onClickIngredient-routing) - Modify:
FS25_ADSmartPickup/adSmartPickup.lua(source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")efter Month-tab) - Modify:
FS25_ADSmartPickup/l10n/l10n_da.xml,l10n_en.xml - Modify:
FS25_ADSmartPickup/modDesc.xml(version1.36.0.0)
Interfaces:
-
Consumes:
ADFieldSilos.config/setConfig/lastPlan/loanCount/planNow/save,ADFieldSiloConfig.with/toggleStore/hasStore/MIN_LITERS_CHOICES/MAX_VEHICLES,SmartPickupFrame:addToggleRow(kind, title, texts, isOn),addTextRow,addCellRow(prefab, texts),self.fieldPrefab,ADBuildings.list. -
Produces:
SmartPickupFrame.TAB_FIELDSILOS = 5,SmartPickupFrame:fillFieldSiloRows(adEnv),SmartPickupFrame:onClickFieldSiloRow(state, data),SmartPickupFrame.FIELDSILO_ROW_KINDS. -
Step 1: Tab constants in SmartPickupFrame.lua — ret konstant-linjerne til:
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5
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"}
grep -n "TAB_KINDS\|\[4\]\|\[5\]\|TAB_MONTH" FS25_ADSmartPickup/gui/*.lua og ret enhver hårdkodet fane-indeks-antagelse (fx for tab = 4, 7) til de nye tal.
- Step 2: Rebuild/buttons/live — i
rebuild()efter Month-blokken:
if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
if adEnv ~= nil then
self:fillFieldSiloRows(adEnv)
end
self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
i refreshButtons(): elseif self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then wanted = self.monthButtons; i refreshLive() ret betingelsen for Month-genopbygning til if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then.
I onClickIngredient før FIELD_ROW_KINDS-grenen:
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
return self:onClickFieldSiloRow(state, data)
- Step 3: Tab file —
FS25_ADSmartPickup/gui/SmartPickupFieldSiloTab.lua:
-- Smart Pickup — fanen Marksiloer
-- Opsætning (til/fra, maks. vogne, mindste mængde, 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}
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 indexOf(list, value)
for index, entry in ipairs(list) do if entry == value then return index end end
return 1
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)
local maxOption = self:addToggleRow("fsMaxVehicles", i18n:getText("spu_fs_maxVehicles"), numberTexts(1, ADFieldSiloConfig.MAX_VEHICLES), false)
maxOption:setState(cfg.maxVehicles, false)
local minOption = self:addToggleRow("fsMinLiters", i18n:getText("spu_fs_minLiters"), literTexts(), false)
minOption:setState(indexOf(ADFieldSiloConfig.MIN_LITERS_CHOICES, cfg.minLiters), false)
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
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
self:addCellRow(self.fieldPrefab, {tostring(entry.siloName), title, string.format("%d", math.floor(entry.liters)),
string.format("%d", math.floor(entry.onWay)), 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 == "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
(addToggleRow gemmer {kind = kind} i rowData via addRow; for gårdlager-rækkerne overskrives den med buildingId som ovenfor.)
- Step 4: l10n — tilføj i
l10n_da.xml(og engelsk il10n_en.xml) inden</texts>:
<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_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)"/>
Engelsk: Field silos, Bring field silos home automatically, Max. vehicles out, Minimum amount per product, %d/%d vehicles out · %d silos waiting, Select at least one farm storage below — otherwise nothing is brought home, Field silos with products, No field silo has products above the minimum amount, Silo, Liters, On the way, Status, ok, missing marker, no farm storage takes %s, paused (did not get going), Farm storages (where products are brought home to).
- Step 5: Source the tab file — i
adSmartPickup.luaeftersource(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua"):
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
- Step 6: Version, syntax, tests, build
modDesc.xml: <version>1.36.0.0</version>.
Run: for f in FS25_ADSmartPickup/*.lua FS25_ADSmartPickup/gui/*.lua; do luajit -bl "$f" >/dev/null || echo "SYNTAKS $f"; done; for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo "FEJLER $t"; done; xmllint --noout FS25_ADSmartPickup/l10n/*.xml FS25_ADSmartPickup/modDesc.xml
Expected: ingen output. Byg: rm -f FS25_ADSmartPickup.zip && (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .) && cp FS25_ADSmartPickup.zip ~/FS25-data/mods/
- Step 7: Commit
git add FS25_ADSmartPickup tests
git commit -m "feat(gui): fanen Marksiloer — til/fra, maks. vogne, mindste mængde, gårdlagre, status pr. silo (v1.36.0.0)"
Self-Review (udført)
- Spec-dækning: marksilo-definition (Task 3
collectBuildings), gårdlagre ekskluderet (Task 3), kvalificering/tærskel/rækkefølge/x/lagervalg (Task 1), lån/afsendelse/parkering/frigivelse/vagt/pause (Task 3), ikke-persistente lån (Task 3), markflåde-busy (Task 3 step 3), GUI + status + tekster (Task 4), opsætning gemmes (Task 2+3). Ingen huller. - Navne:
ADFieldSiloPlanner.plan/key,ADFieldSiloConfig.new/with/toggleStore/hasStore/toRecord/fromRecord,ADFieldSilos.tick/isLoaned/setConfig/planNow/lastPlan/loanCount/save/load/releaseReason— brugt ens i alle tasks. - Review Focus-linjerne har tests i Task 1 (anden vare, reservation, flere varer) og Task 3 (spiller, frakobling via
setConfig→stopAll;releaseReason"player").