From e4d131b827fe5931105c4a505eaffaee67d8702a Mon Sep 17 00:00:00 2001 From: masterdraco Date: Tue, 22 Sep 2026 22:20:50 +0200 Subject: [PATCH] feat: run-model (rene funktioner) til runs-UI --- FS25_ADSmartPickup/adRuns.lua | 201 ++++++++++++++++++++++++++++++++++ tests/test_adRuns.lua | 100 +++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 FS25_ADSmartPickup/adRuns.lua create mode 100644 tests/test_adRuns.lua diff --git a/FS25_ADSmartPickup/adRuns.lua b/FS25_ADSmartPickup/adRuns.lua new file mode 100644 index 0000000..d1a3a1e --- /dev/null +++ b/FS25_ADSmartPickup/adRuns.lua @@ -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 diff --git a/tests/test_adRuns.lua b/tests/test_adRuns.lua new file mode 100644 index 0000000..62fa254 --- /dev/null +++ b/tests/test_adRuns.lua @@ -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)