feat(markarbejde): genbrug Courseplay-kurser — markform-signatur + kursus-register pr. maskine; genbrug/kopi i stedet for ny beregning, glemmes ved ændret mark (v1.34.0.0)

This commit is contained in:
2026-09-26 22:27:16 +02:00
parent 39fa72ba0d
commit 50301b61b9
14 changed files with 675 additions and 6 deletions
+195
View File
@@ -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)