diff --git a/FS25_ADSmartPickup/adCourseCache.lua b/FS25_ADSmartPickup/adCourseCache.lua new file mode 100644 index 0000000..0230b06 --- /dev/null +++ b/FS25_ADSmartPickup/adCourseCache.lua @@ -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 diff --git a/FS25_ADSmartPickup/adCpBridge.lua b/FS25_ADSmartPickup/adCpBridge.lua index aa61780..2c0e142 100644 --- a/FS25_ADSmartPickup/adCpBridge.lua +++ b/FS25_ADSmartPickup/adCpBridge.lua @@ -470,7 +470,8 @@ 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). @@ -525,10 +526,15 @@ 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 + if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil 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 diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua index 48cd63d..b3dd037 100644 --- a/FS25_ADSmartPickup/adFieldJobs.lua +++ b/FS25_ADSmartPickup/adFieldJobs.lua @@ -15,7 +15,8 @@ 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 -- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben -ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all"} +-- courseRecords: vehicleId -> post i kursus-registret (ADCourseCache) +ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}} -- mark-id -> {mower = kursus, rake = kursus, = 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 = {} @@ -140,6 +141,7 @@ function ADFieldJobs.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 {} else Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded)) end diff --git a/FS25_ADSmartPickup/adFieldStorage.lua b/FS25_ADSmartPickup/adFieldStorage.lua index d6cd50d..9555a0b 100644 --- a/FS25_ADSmartPickup/adFieldStorage.lua +++ b/FS25_ADSmartPickup/adFieldStorage.lua @@ -16,6 +16,11 @@ ADFieldStorage.FIELD_TYPES = { 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() local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil if runsPath == nil then return nil end @@ -37,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 @@ -53,13 +73,18 @@ function ADFieldStorage.save(state) xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId) vehicleIndex = vehicleIndex + 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 = {}, fleetMode = "all"} + local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}} local path = ADFieldStorage.getPath() if path == nil or not fileExists(path) then return result end local xmlFile = XMLFile.load("ADSmartPickupFields", path) @@ -73,6 +98,10 @@ 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 .. ".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 diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index 033aeb4..9ecfaad 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -722,12 +722,16 @@ local function beginWork(adEnv, field, info, assignment) end end local point = info.points[1] or {x = info.x, z = info.z} + -- genbrug af Courseplay-kurser: maskinens eget / en andens matchende kursus frem for ny beregning + local cacheContext + opts, cacheContext = ADCourseCache.prepare(adEnv, field, info, assignment, opts) assignment.phase = "starting" assignment.since = now() local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason) if ok then assignment.phase = "working" assignment.since = now() + ADCourseCache.afterStart(vehicle, assignment.vehicleId, cacheContext) if assignment.role == "mower" then note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId}) rememberCourse(field, "mower", vehicle) @@ -742,7 +746,7 @@ local function beginWork(adEnv, field, info, assignment) rememberCourse(field, field.stage, vehicle) end log(field.id, "Courseplay startet (%s%s%s)", assignment.role, - reason == "copied" and (", " .. tostring(source) .. "-kursus kopieret") or "", + reason == "copied" and source ~= nil and (", " .. tostring(source) .. "-kursus kopieret") or "", assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "") else ADFieldWork.release(assignment) diff --git a/FS25_ADSmartPickup/adFields.lua b/FS25_ADSmartPickup/adFields.lua index 0134983..c651d8e 100644 --- a/FS25_ADSmartPickup/adFields.lua +++ b/FS25_ADSmartPickup/adFields.lua @@ -194,6 +194,33 @@ function ADFields.readPolygon(field) return verts 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 @@ -393,6 +420,9 @@ local function finishScan(adEnv, scan) info.markerDistance = distance info.markerCheckedAt = ADFields.now end + -- 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 diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua index ee99086..859d836 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua @@ -189,6 +189,9 @@ function SmartPickupFrame:saveFieldGuide(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 diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 35ca31a..34577b1 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.33.4.0 + 1.34.0.0 <en>AD Smart Pickup</en> @@ -35,6 +35,7 @@ + diff --git a/docs/TESTPLAN-markarbejde.md b/docs/TESTPLAN-markarbejde.md index 5a3d6a3..894b12f 100644 --- a/docs/TESTPLAN-markarbejde.md +++ b/docs/TESTPLAN-markarbejde.md @@ -205,6 +205,34 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja. --- +## 12. Genbrug af Courseplay-kurser (v1.34) + +Log-linjer at kigge efter (`ADSmartPickup: F …`): `nyt kursus beregnes til '' ()`, +`kursus genbrugt ('', m) — ingen ny beregning`, `kursus kopieret fra '' til ''`, +`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under ``. + +- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)` + → CP starter; `` 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) | Område | Hvad der er gættet | Tegn på fejl | @@ -219,6 +247,7 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja. | 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 diff --git a/tests/test_adCourseCache.lua b/tests/test_adCourseCache.lua new file mode 100644 index 0000000..252fa12 --- /dev/null +++ b/tests/test_adCourseCache.lua @@ -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) diff --git a/tests/test_adCpBridge.lua b/tests/test_adCpBridge.lua index 509ad31..7cfbd7e 100644 --- a/tests/test_adCpBridge.lua +++ b/tests/test_adCpBridge.lua @@ -115,5 +115,38 @@ 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 + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adFieldStorage.lua b/tests/test_adFieldStorage.lua index af4aa95..9183c6f 100644 --- a/tests/test_adFieldStorage.lua +++ b/tests/test_adFieldStorage.lua @@ -38,5 +38,59 @@ 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, +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 -> tomt register (bagudkompatibelt) +disk = {} +ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"}) +local old = ADFieldStorage.load() +check("ST7 gammel fil: tomt register", next(old.courseRecords), nil) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adFieldsPolygon.lua b/tests/test_adFieldsPolygon.lua index 05bbf58..ef98979 100644 --- a/tests/test_adFieldsPolygon.lua +++ b/tests/test_adFieldsPolygon.lua @@ -56,6 +56,7 @@ check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, 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. diff --git a/tests/test_adFieldsShape.lua b/tests/test_adFieldsShape.lua new file mode 100644 index 0000000..f0722ba --- /dev/null +++ b/tests/test_adFieldsShape.lua @@ -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)