feat: run-model (rene funktioner) til runs-UI

This commit is contained in:
2026-09-22 22:20:50 +02:00
parent 85ae4436f3
commit e4d131b827
2 changed files with 301 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
-- AD Runs
-- Rene funktioner (ingen spil-API) for "runs": navn, aflæsningsmål, varer, Wait-pulje,
-- loop-tæller og tildelte traktorer. Alle funktioner returnerer en NY store —
-- input røres aldrig, så GUI og controller kan holde referencer uden overraskelser.
ADRuns = {}
ADRuns.MAX_LOOPS = 99
ADRuns.XML_VERSION = 1
ADRuns.INGREDIENT_MODES = {auto = true, manual = true}
local function copyList(list)
local copy = {}
for index, value in ipairs(list or {}) do
copy[index] = value
end
return copy
end
local function copyRun(run)
return {
id = run.id,
name = run.name,
targetWayPointId = run.targetWayPointId,
ingredientMode = run.ingredientMode,
ingredients = copyList(run.ingredients),
waitPoolGroup = run.waitPoolGroup,
loops = run.loops,
vehicleIds = copyList(run.vehicleIds),
}
end
local function copyStore(store)
local runs = {}
for index, run in ipairs(store.runs) do
runs[index] = copyRun(run)
end
return {nextId = store.nextId, runs = runs}
end
local function clampLoops(loops)
local value = tonumber(loops) or 0
return math.max(0, math.min(ADRuns.MAX_LOOPS, math.floor(value)))
end
function ADRuns.newStore()
return {nextId = 1, runs = {}}
end
function ADRuns.createRun(store, name)
local new = copyStore(store)
local run = {
id = new.nextId,
name = name,
targetWayPointId = nil,
ingredientMode = "auto",
ingredients = {},
waitPoolGroup = "",
loops = 0,
vehicleIds = {},
}
new.nextId = new.nextId + 1
table.insert(new.runs, run)
return new, run
end
function ADRuns.getRun(store, runId)
for _, run in ipairs(store.runs) do
if run.id == runId then
return run
end
end
return nil
end
function ADRuns.deleteRun(store, runId)
local new = copyStore(store)
for index, run in ipairs(new.runs) do
if run.id == runId then
table.remove(new.runs, index)
break
end
end
return new
end
-- changes: delmængde af {name, targetWayPointId, ingredientMode, ingredients, waitPoolGroup, loops}.
function ADRuns.updateRun(store, runId, changes)
local new = copyStore(store)
local run = ADRuns.getRun(new, runId)
if run == nil then
return new
end
if changes.name ~= nil then run.name = changes.name end
-- false = ryd målet (nil i changes betyder "rør ikke")
if changes.targetWayPointId == false then
run.targetWayPointId = nil
elseif changes.targetWayPointId ~= nil then
run.targetWayPointId = changes.targetWayPointId
end
if ADRuns.INGREDIENT_MODES[changes.ingredientMode] then run.ingredientMode = changes.ingredientMode end
if changes.ingredients ~= nil then run.ingredients = copyList(changes.ingredients) end
if changes.waitPoolGroup ~= nil then run.waitPoolGroup = changes.waitPoolGroup end
if changes.loops ~= nil then run.loops = clampLoops(changes.loops) end
return new
end
function ADRuns.findRunByVehicle(store, vehicleId)
for _, run in ipairs(store.runs) do
for _, id in ipairs(run.vehicleIds) do
if id == vehicleId then
return run
end
end
end
return nil
end
function ADRuns.unassignVehicle(store, vehicleId)
local new = copyStore(store)
for _, run in ipairs(new.runs) do
for index, id in ipairs(run.vehicleIds) do
if id == vehicleId then
table.remove(run.vehicleIds, index)
break
end
end
end
return new
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)
if previous ~= nil and previous.id == runId then
return copyStore(store), nil
end
local new = ADRuns.unassignVehicle(store, vehicleId)
local run = ADRuns.getRun(new, runId)
if run == nil then
return new, nil
end
table.insert(run.vehicleIds, vehicleId)
return new, previous ~= nil and previous.id or nil
end
-- Varer gemmes som mellemrumsadskilte fillType-NAVNE ("STRAW FORAGE") — navne er
-- stabile på tværs af mods/kort, fillType-indeks er det ikke.
function ADRuns.serializeIngredients(list)
return table.concat(list or {}, " ")
end
function ADRuns.parseIngredients(text)
local list = {}
for name in string.gmatch(tostring(text or ""), "%S+") do
table.insert(list, name)
end
return list
end
-- Flade poster (kun strenge/tal) til XML-laget, i store'ens rækkefølge.
function ADRuns.toRecords(store)
local records = {}
for index, run in ipairs(store.runs) do
records[index] = {
id = run.id,
name = run.name,
target = run.targetWayPointId or 0,
ingredientMode = run.ingredientMode,
ingredients = ADRuns.serializeIngredients(run.ingredients),
waitPool = run.waitPoolGroup,
loops = run.loops,
vehicleIds = copyList(run.vehicleIds),
}
end
return records
end
-- Poster uden gyldigt id/navn eller med dublet-id springes over; nextId = højeste id + 1.
function ADRuns.fromRecords(records)
local store = ADRuns.newStore()
for _, record in ipairs(records or {}) do
local id = tonumber(record.id)
local isValid = id ~= nil and id >= 1 and record.name ~= nil and record.name ~= ""
if isValid and ADRuns.getRun(store, id) == nil then
local target = tonumber(record.target)
table.insert(store.runs, {
id = id,
name = record.name,
targetWayPointId = target ~= nil and target > 0 and target or nil,
ingredientMode = ADRuns.INGREDIENT_MODES[record.ingredientMode] and record.ingredientMode or "auto",
ingredients = ADRuns.parseIngredients(record.ingredients),
waitPoolGroup = record.waitPool or "",
loops = clampLoops(record.loops),
vehicleIds = copyList(record.vehicleIds),
})
store.nextId = math.max(store.nextId, id + 1)
end
end
return store
end
+100
View File
@@ -0,0 +1,100 @@
-- Kør: luajit tests/test_adRuns.lua (fra repo-roden)
-- Tester run-modellen (rene funktioner) og XML-laget mod en XMLFile-mock.
dofile("FS25_ADSmartPickup/adRuns.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
-- R1: opret run -> løbenummer, standardværdier; input-store røres ikke
local empty = ADRuns.newStore()
local s1, cow = ADRuns.createRun(empty, "Cow Feed")
check("R1 id 1", cow.id, 1)
check("R1 navn", cow.name, "Cow Feed")
check("R1 auto-varer som standard", cow.ingredientMode, "auto")
check("R1 loops 0", cow.loops, 0)
check("R1 tom Wait-pulje", cow.waitPoolGroup, "")
check("R1 input-store urørt", #empty.runs, 0)
check("R1 nextId tæller op", s1.nextId, 2)
local s2, pig = ADRuns.createRun(s1, "Pig Feed")
check("R1 andet run får id 2", pig.id, 2)
check("R1 to runs", #s2.runs, 2)
-- R2: opdatér felter; ukendt mode ignoreres; loops klippes
local s3 = ADRuns.updateRun(s2, 1, {name = "Cow 1", targetWayPointId = 94, ingredientMode = "manual", ingredients = {"STRAW", "FORAGE"}, waitPoolGroup = "Cow Wait", loops = 150})
local run1 = ADRuns.getRun(s3, 1)
check("R2 navn", run1.name, "Cow 1")
check("R2 mål", run1.targetWayPointId, 94)
check("R2 manual", run1.ingredientMode, "manual")
check("R2 varer", table.concat(run1.ingredients, ","), "STRAW,FORAGE")
check("R2 pulje", run1.waitPoolGroup, "Cow Wait")
check("R2 loops klippet til 99", run1.loops, 99)
check("R2 gammel store urørt", ADRuns.getRun(s2, 1).name, "Cow Feed")
local s3b = ADRuns.updateRun(s3, 1, {ingredientMode = "bogus", loops = -5})
check("R2 ukendt mode ignoreres", ADRuns.getRun(s3b, 1).ingredientMode, "manual")
check("R2 loops under 0 -> 0", ADRuns.getRun(s3b, 1).loops, 0)
check("R2 ukendt run -> uændret", #ADRuns.updateRun(s3, 99, {name = "x"}).runs, 2)
check("R2 targetWayPointId=false rydder målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {targetWayPointId = false}), 1).targetWayPointId, nil)
check("R2 nil rører ikke målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {name = "y"}), 1).targetWayPointId, 94)
-- R3: tildel traktor; samme traktor flyttes mellem runs
local s4, prev = ADRuns.assignVehicle(s3, 1, "vehicleA")
check("R3 tildelt", ADRuns.getRun(s4, 1).vehicleIds[1], "vehicleA")
check("R3 ingen tidligere", prev, nil)
check("R3 opslag", ADRuns.findRunByVehicle(s4, "vehicleA").id, 1)
local s5, prev2 = ADRuns.assignVehicle(s4, 2, "vehicleA")
check("R3 flyttet fra run 1", prev2, 1)
check("R3 væk fra run 1", #ADRuns.getRun(s5, 1).vehicleIds, 0)
check("R3 i run 2", ADRuns.getRun(s5, 2).vehicleIds[1], "vehicleA")
local s5b, prev3 = ADRuns.assignVehicle(s5, 2, "vehicleA")
check("R3 gen-tildeling samme run -> ingen dublet", #ADRuns.getRun(s5b, 2).vehicleIds, 1)
check("R3 gen-tildeling -> ingen 'flyttet fra'", prev3, nil)
local s6 = ADRuns.unassignVehicle(s5, "vehicleA")
check("R3 fjernet", ADRuns.findRunByVehicle(s6, "vehicleA"), nil)
check("R3 ukendt run ved tildeling -> uændret", ADRuns.findRunByVehicle(ADRuns.assignVehicle(s6, 42, "vehicleB"), "vehicleB"), nil)
-- R4: slet run
local s7 = ADRuns.deleteRun(s5, 1)
check("R4 ét run tilbage", #s7.runs, 1)
check("R4 det rigtige", s7.runs[1].id, 2)
check("R4 nextId bevares", s7.nextId, 3)
-- R5: records frem og tilbage
local records = ADRuns.toRecords(s5)
check("R5 target som tal", records[1].target, 94)
check("R5 varer som streng", records[1].ingredients, "STRAW FORAGE")
check("R5 køretøjer", records[2].vehicleIds[1], "vehicleA")
local back = ADRuns.fromRecords(records)
check("R5 to runs tilbage", #back.runs, 2)
check("R5 nextId = højeste + 1", back.nextId, 3)
check("R5 varer parset", ADRuns.getRun(back, 1).ingredients[2], "FORAGE")
check("R5 pulje", ADRuns.getRun(back, 1).waitPoolGroup, "Cow Wait")
check("R5 target 0 -> nil", ADRuns.getRun(back, 2).targetWayPointId, nil)
check("R5 køretøj", ADRuns.findRunByVehicle(back, "vehicleA").id, 2)
-- R6: defekte poster springes over; dubletter af id ignoreres
local dirty = ADRuns.fromRecords({
{id = "x", name = "Broken"},
{id = 5, name = "", },
{id = 7, name = "Ok", target = "12", ingredientMode = "weird", ingredients = "", waitPool = nil, loops = "3", vehicleIds = {}},
{id = 7, name = "Dublet"},
})
check("R6 kun den gyldige", #dirty.runs, 1)
check("R6 target parses fra streng", dirty.runs[1].targetWayPointId, 12)
check("R6 ukendt mode -> auto", dirty.runs[1].ingredientMode, "auto")
check("R6 tom varestreng -> tom liste", #dirty.runs[1].ingredients, 0)
check("R6 manglende pulje -> tom streng", dirty.runs[1].waitPoolGroup, "")
check("R6 loops fra streng", dirty.runs[1].loops, 3)
check("R6 nextId", dirty.nextId, 8)
check("R6 tom liste", #ADRuns.fromRecords(nil).runs, 0)
check("R6 parseIngredients trimmer", #ADRuns.parseIngredients(" STRAW FORAGE "), 2)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)