perf(marker): hak hvert sekund — ingen kopier når intet ændrer sig, passive marker i tiendedele, scan hvert 2. s, cachet markliste/markører, tick-tidsmåling i loggen (v1.27.1.0)

In-game 26/9: lille hak ca. hvert sekund. Markarbejdets tick kopierede alle markers data hvert sekund
(affald -> GC-pause), hentede alle kortets marker og alle AD-markører.
This commit is contained in:
2026-09-26 02:03:31 +02:00
parent 7a073ae047
commit c34a472b1c
8 changed files with 101 additions and 24 deletions
+43 -8
View File
@@ -7,6 +7,12 @@ ADFieldJobs = {}
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: " ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
ADFieldJobs.TICK_MS = 1000 ADFieldJobs.TICK_MS = 1000
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
ADFieldJobs.SCAN_EVERY_TICKS = 2
ADFieldJobs.PASSIVE_SLICES = 10
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
ADFieldJobs.SLOW_TICK_MS = 3
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
@@ -226,12 +232,13 @@ end
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde). -- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
ADFieldJobs.lastDecision = {} ADFieldJobs.lastDecision = {}
local function stepField(adEnv, info) local function stepField(adEnv, info, env)
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0) local current = ADFieldJobs.getField(info.id)
ADFieldJobs.setField(field) local field = ADFieldModel.onReadiness(current, info.readyRatio or 0)
if field ~= current then ADFieldJobs.setField(field) end
local assignments = ADFieldJobs.assignmentsFor(info.id) local assignments = ADFieldJobs.assignmentsFor(info.id)
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment()) local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
ADFieldJobs.lastDecision[info.id] = decision ADFieldJobs.lastDecision[info.id] = decision
if decision.action == "start" then if decision.action == "start" then
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName) local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
@@ -262,14 +269,32 @@ local function stepField(adEnv, info)
end end
end end
local function clockMs()
local ok, seconds = pcall(getTimeSec)
return ok and type(seconds) == "number" and seconds * 1000 or nil
end
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
local function reportSlow(total, scan, follow, steps, stepped)
if total == nil or total <= ADFieldJobs.SLOW_TICK_MS then return end
if ADFieldJobs.lastSlowReport ~= nil and ADFieldJobs.now - ADFieldJobs.lastSlowReport < 60000 then return end
ADFieldJobs.lastSlowReport = ADFieldJobs.now
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde-tick %.1f ms (scan %.1f, sæt %.1f, marker %.1f for %d marker)",
total, scan or 0, follow or 0, steps or 0, stepped)
end
function ADFieldJobs.tick(adEnv, dt) function ADFieldJobs.tick(adEnv, dt)
ADFieldJobs.now = ADFieldJobs.now + dt ADFieldJobs.now = ADFieldJobs.now + dt
ADFieldJobs.timer = ADFieldJobs.timer + dt ADFieldJobs.timer = ADFieldJobs.timer + dt
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
ADFieldJobs.timer = 0 ADFieldJobs.timer = 0
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
if not ADFieldJobs.loaded then ADFieldJobs.load() end if not ADFieldJobs.loaded then ADFieldJobs.load() end
local farmId = g_currentMission:getFarmId() local t0 = clockMs()
ADFields.scanNext(adEnv, farmId) if ADFieldJobs.tickCount % ADFieldJobs.SCAN_EVERY_TICKS == 0 then
ADFields.scanNext(adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
end
local t1 = clockMs()
for _, assignment in pairs(ADFieldJobs.state.assignments) do for _, assignment in pairs(ADFieldJobs.state.assignments) do
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment) local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
if not ok then if not ok then
@@ -277,8 +302,18 @@ function ADFieldJobs.tick(adEnv, dt)
ADFieldWork.release(assignment) ADFieldWork.release(assignment)
end end
end end
for _, info in ipairs(ADFields.list()) do local t2 = clockMs()
local ok, err = pcall(stepField, adEnv, info) local env = ADFieldJobs.environment()
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
local stepped = 0
for index, info in ipairs(ADFields.list()) do
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
if not ADFieldModel.isPassive(ADFieldJobs.getField(info.id)) or index % ADFieldJobs.PASSIVE_SLICES == slice then
stepped = stepped + 1
local ok, err = pcall(stepField, adEnv, info, env)
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
end end
end end
local t3 = clockMs()
if t0 ~= nil and t3 ~= nil then reportSlow(t3 - t0, t1 - t0, t2 - t1, t3 - t2, stepped) end
end
+13 -2
View File
@@ -223,17 +223,28 @@ function ADFieldModel.lanePositions(n)
end end
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os. -- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os.
-- Uændret -> samme tabel (kaldes for hver mark hvert sekund; nye kopier giver affald og GC-hak).
function ADFieldModel.onReadiness(field, ratio) function ADFieldModel.onReadiness(field, ratio)
local copy = copyField(field)
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
local copy = copyField(field)
copy.stage = "ready" copy.stage = "ready"
return copy
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0) -- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
local copy = copyField(field)
copy.stage = "idle" copy.stage = "idle"
copy.startRequested = false copy.startRequested = false
end
return copy return copy
end end
return field
end
-- Passiv mark: manuel, uden mål, venter/færdig, ingen Start nu — der sker intet uden brugeren.
function ADFieldModel.isPassive(field)
local plan = field.plan
return plan.mode ~= "auto" and (plan.targetFruit == nil or plan.targetFruit == "") and not field.startRequested
and (field.stage == "idle" or field.stage == "done")
end
function ADFieldModel.shouldStart(field) function ADFieldModel.shouldStart(field)
return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true) return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true)
+21 -2
View File
@@ -11,6 +11,11 @@ ADFields.cache = {}
ADFields.order = {} ADFields.order = {}
ADFields.scanIndex = 0 ADFields.scanIndex = 0
ADFields.mowable = nil ADFields.mowable = nil
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
ADFields.OWNED_REFRESH_MS = 30000
ADFields.MARKER_REFRESH_MS = 30000
ADFields.ownedAt = nil
ADFields.now = 0
function ADFields.parseMarkerName(name) function ADFields.parseMarkerName(name)
if type(name) ~= "string" then return nil end if type(name) ~= "string" then return nil end
@@ -287,11 +292,15 @@ local function scanField(adEnv, field, fieldId)
info.kind = "bare" info.kind = "bare"
end end
readSoil(info) readSoil(info)
if info.x ~= nil then local previous = ADFields.cache[fieldId]
if previous ~= nil and previous.markerCheckedAt ~= nil and ADFields.now - previous.markerCheckedAt < ADFields.MARKER_REFRESH_MS then
info.markerId, info.markerName, info.markerDistance, info.markerCheckedAt = previous.markerId, previous.markerName, previous.markerDistance, previous.markerCheckedAt
elseif info.x ~= nil then
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z) local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
info.markerId = marker ~= nil and marker.id or nil info.markerId = marker ~= nil and marker.id or nil
info.markerName = marker ~= nil and marker.name or nil info.markerName = marker ~= nil and marker.name or nil
info.markerDistance = distance info.markerDistance = distance
info.markerCheckedAt = ADFields.now
end end
return info return info
end end
@@ -310,7 +319,10 @@ local function ownedFields(farmId)
end end
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken. -- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
function ADFields.scanNext(adEnv, farmId) function ADFields.scanNext(adEnv, farmId, now)
ADFields.now = now or ADFields.now
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
ADFields.ownedAt = ADFields.now
local owned = ownedFields(farmId) local owned = ownedFields(farmId)
local ids = {} local ids = {}
for _, entry in ipairs(owned) do ids[entry.id] = true end for _, entry in ipairs(owned) do ids[entry.id] = true end
@@ -318,10 +330,14 @@ function ADFields.scanNext(adEnv, farmId)
if not ids[id] then ADFields.cache[id] = nil end if not ids[id] then ADFields.cache[id] = nil end
end end
ADFields.order = owned ADFields.order = owned
ADFields.listCache = nil
end
local owned = ADFields.order
if #owned == 0 then return end if #owned == 0 then return end
ADFields.scanIndex = ADFields.scanIndex % #owned + 1 ADFields.scanIndex = ADFields.scanIndex % #owned + 1
local entry = owned[ADFields.scanIndex] local entry = owned[ADFields.scanIndex]
local ok, info = pcall(scanField, adEnv, entry.field, entry.id) local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
ADFields.listCache = nil
if ok then if ok then
ADFields.cache[entry.id] = info ADFields.cache[entry.id] = info
else else
@@ -329,12 +345,15 @@ function ADFields.scanNext(adEnv, farmId)
end end
end end
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
function ADFields.list() function ADFields.list()
if ADFields.listCache ~= nil then return ADFields.listCache end
local infos = {} local infos = {}
for _, entry in ipairs(ADFields.order) do for _, entry in ipairs(ADFields.order) do
local info = ADFields.cache[entry.id] local info = ADFields.cache[entry.id]
if info ~= nil then table.insert(infos, info) end if info ~= nil then table.insert(infos, info) end
end end
ADFields.listCache = infos
return infos return infos
end end
+1 -1
View File
@@ -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.27.0.0</version> <version>1.27.1.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+1 -1
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — brugermanual # AD Smart Pickup — brugermanual
Version 1.27.0 · Farming Simulator 25 · tillæg til AutoDrive Version 1.27.1 · Farming Simulator 25 · tillæg til AutoDrive
*English version: [USER_MANUAL.md](USER_MANUAL.md)* *English version: [USER_MANUAL.md](USER_MANUAL.md)*
+2 -2
View File
@@ -1,4 +1,4 @@
# Testplan — Markarbejde (fanen Marker), version 1.27.0.0 # Testplan — Markarbejde (fanen Marker), version 1.27.1.0
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den. Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
@@ -11,7 +11,7 @@ samme rækkefølge.
## 0. Forberedelse ## 0. Forberedelse
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.0.0) FS25_ADSmartPickup`. - [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.1.0) FS25_ADSmartPickup`.
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …" - [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs). i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
Afgrøde-kolonnen viser afgrøden eller "bar jord". Afgrøde-kolonnen viser afgrøden eller "bar jord".
+1 -1
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — user manual # AD Smart Pickup — user manual
Version 1.27.0 · Farming Simulator 25 · add-on for AutoDrive Version 1.27.1 · Farming Simulator 25 · add-on for AutoDrive
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
+12
View File
@@ -289,5 +289,17 @@ check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting"
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale") check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop") check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
local calm = M.newField(95)
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
check("M24 manuel venter = passiv", M.isPassive(calm), true)
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
check("M24 i gang = ikke passiv", M.isPassive(started), false)
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
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)