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:
@@ -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
|
||||||
@@ -470,7 +470,8 @@ end
|
|||||||
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
|
-- 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)
|
-- 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)
|
-- 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
|
-- 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
|
-- 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).
|
-- 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)
|
local ok, err = job:validate(farmId)
|
||||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||||
g_currentMission.aiSystem:startJob(job, farmId)
|
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
|
end
|
||||||
local function generateAndStart()
|
local function generateAndStart()
|
||||||
if handle.cancelled then return end
|
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
|
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
||||||
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
||||||
if copied then
|
if copied then
|
||||||
|
|||||||
@@ -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.
|
-- 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
|
ADFieldJobs.MAX_PASSES = 2
|
||||||
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
-- 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, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
||||||
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||||
ADFieldJobs.courses = {}
|
ADFieldJobs.courses = {}
|
||||||
@@ -140,6 +141,7 @@ function ADFieldJobs.load()
|
|||||||
if ok then
|
if ok then
|
||||||
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
||||||
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
||||||
|
ADFieldJobs.state.courseRecords = loaded.courseRecords or {}
|
||||||
else
|
else
|
||||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ ADFieldStorage.FIELD_TYPES = {
|
|||||||
dedicated = "String",
|
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()
|
function ADFieldStorage.getPath()
|
||||||
local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||||
if runsPath == nil then return nil end
|
if runsPath == nil then return nil end
|
||||||
@@ -37,6 +42,21 @@ function ADFieldStorage.readRecord(xmlFile, key)
|
|||||||
return record
|
return record
|
||||||
end
|
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)
|
function ADFieldStorage.save(state)
|
||||||
local path = ADFieldStorage.getPath()
|
local path = ADFieldStorage.getPath()
|
||||||
if path == nil then return false end
|
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)
|
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
|
||||||
vehicleIndex = vehicleIndex + 1
|
vehicleIndex = vehicleIndex + 1
|
||||||
end
|
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:save()
|
||||||
xmlFile:delete()
|
xmlFile:delete()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldStorage.load()
|
function ADFieldStorage.load()
|
||||||
local result = {fields = {}, fleet = {}, fleetMode = "all"}
|
local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}}
|
||||||
local path = ADFieldStorage.getPath()
|
local path = ADFieldStorage.getPath()
|
||||||
if path == nil or not fileExists(path) then return result end
|
if path == nil or not fileExists(path) then return result end
|
||||||
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
||||||
@@ -73,6 +98,10 @@ function ADFieldStorage.load()
|
|||||||
local id = xmlFile:getString(key .. "#id")
|
local id = xmlFile:getString(key .. "#id")
|
||||||
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
||||||
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()
|
xmlFile:delete()
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -722,12 +722,16 @@ local function beginWork(adEnv, field, info, assignment)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
local point = info.points[1] or {x = info.x, z = info.z}
|
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.phase = "starting"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
||||||
if ok then
|
if ok then
|
||||||
assignment.phase = "working"
|
assignment.phase = "working"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
|
ADCourseCache.afterStart(vehicle, assignment.vehicleId, cacheContext)
|
||||||
if assignment.role == "mower" then
|
if assignment.role == "mower" then
|
||||||
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
||||||
rememberCourse(field, "mower", vehicle)
|
rememberCourse(field, "mower", vehicle)
|
||||||
@@ -742,7 +746,7 @@ local function beginWork(adEnv, field, info, assignment)
|
|||||||
rememberCourse(field, field.stage, vehicle)
|
rememberCourse(field, field.stage, vehicle)
|
||||||
end
|
end
|
||||||
log(field.id, "Courseplay startet (%s%s%s)", assignment.role,
|
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 "")
|
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||||
else
|
else
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
|||||||
@@ -194,6 +194,33 @@ function ADFields.readPolygon(field)
|
|||||||
return verts
|
return verts
|
||||||
end
|
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).
|
-- Hjørnernes gennemsnit (nil uden hjørner).
|
||||||
function ADFields.polygonCenter(verts)
|
function ADFields.polygonCenter(verts)
|
||||||
if verts == nil or #verts == 0 then return nil end
|
if verts == nil or #verts == 0 then return nil end
|
||||||
@@ -393,6 +420,9 @@ local function finishScan(adEnv, scan)
|
|||||||
info.markerDistance = distance
|
info.markerDistance = distance
|
||||||
info.markerCheckedAt = ADFields.now
|
info.markerCheckedAt = ADFields.now
|
||||||
end
|
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.cache[info.id] = info
|
||||||
ADFields.listCache = nil
|
ADFields.listCache = nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ function SmartPickupFrame:saveFieldGuide(guide, start)
|
|||||||
if result.fieldId == nil then return end
|
if result.fieldId == nil then return end
|
||||||
if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
|
if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
|
||||||
ADFieldJobs.setPlan(result.fieldId, result.plan)
|
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()
|
local byField = SmartPickupFrame.rigIdsByField()
|
||||||
for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
|
for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
|
||||||
if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
|
if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.33.4.0</version>
|
<version>1.34.0.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
<sourceFile filename="adFieldGuide.lua"/>
|
<sourceFile filename="adFieldGuide.lua"/>
|
||||||
<sourceFile filename="adFields.lua"/>
|
<sourceFile filename="adFields.lua"/>
|
||||||
<sourceFile filename="adCpBridge.lua"/>
|
<sourceFile filename="adCpBridge.lua"/>
|
||||||
|
<sourceFile filename="adCourseCache.lua"/>
|
||||||
<sourceFile filename="adFieldStorage.lua"/>
|
<sourceFile filename="adFieldStorage.lua"/>
|
||||||
<sourceFile filename="adFieldWork.lua"/>
|
<sourceFile filename="adFieldWork.lua"/>
|
||||||
<sourceFile filename="adFieldJobs.lua"/>
|
<sourceFile filename="adFieldJobs.lua"/>
|
||||||
|
|||||||
@@ -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<nr> …`): `nyt kursus beregnes til '<maskine>' (<grund>)`,
|
||||||
|
`kursus genbrugt ('<maskine>', <bredde> m) — ingen ny beregning`, `kursus kopieret fra '<A>' til '<B>'`,
|
||||||
|
`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under `<courses>`.
|
||||||
|
|
||||||
|
- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)`
|
||||||
|
→ CP starter; `<courses><course vehicle=… field=… width=… sig=… settings=… waypoints=…/>` 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)
|
## Kendte usikkerheder (første gang i spillet)
|
||||||
|
|
||||||
| Område | Hvad der er gættet | Tegn på fejl |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 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
|
- [ ] **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
|
vognene; høsteren med korn i tanken → log `'Mejetærsker' kører rest-læs (N l) til lager` → AutoDrive
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -115,5 +115,38 @@ check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
|||||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
||||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
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"))
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
os.exit(failures == 0 and 0 or 1)
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
|||||||
@@ -38,5 +38,59 @@ for key, value in pairs(record) do
|
|||||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
|
||||||
|
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 <courses> -> 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"))
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
os.exit(failures == 0 and 0 or 1)
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
|||||||
@@ -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("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("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("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
|
-- 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.
|
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user