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
|
||||
Reference in New Issue
Block a user