diff --git a/FS25_ADSmartPickup/adBuildings.lua b/FS25_ADSmartPickup/adBuildings.lua index 8bc62c7..c6c9298 100644 --- a/FS25_ADSmartPickup/adBuildings.lua +++ b/FS25_ADSmartPickup/adBuildings.lua @@ -10,7 +10,8 @@ ADBuildings.LOG_PREFIX = "ADSmartPickup: " ADBuildings.MAX_MARKER_DISTANCE = 40 -- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren ADBuildings.MAX_DROP_DISTANCE = 25 -ADBuildings.CACHE_MS = 1000 +-- bygninger ændrer sig sjældent; genopbygningen (bygninger x markører) er dyr — 1 s gav et hak hvert sekund +ADBuildings.CACHE_MS = 10000 local cache = {list = nil, farmId = nil, time = -math.huge} @@ -363,6 +364,7 @@ local function build(adEnv, farmId) end function ADBuildings.invalidate() + if ADCache ~= nil then ADCache.clear() end cache.list, cache.time = nil, -math.huge end diff --git a/FS25_ADSmartPickup/adCache.lua b/FS25_ADSmartPickup/adCache.lua new file mode 100644 index 0000000..1e279c9 --- /dev/null +++ b/FS25_ADSmartPickup/adCache.lua @@ -0,0 +1,53 @@ +-- AD Cache +-- Fælles korttids-cache for dyre opslag (markør -> station, salgssteder, identify, Wait-punkter, lagre). +-- Holdende vogne genplanlægger hvert 5. s og slog de samme ting op igen og igen (markører x stationer x +-- triggere) -> hak (in-game 26/9). Verden ændrer sig ikke så hurtigt. Uden spillets tid (tests) caches +-- intet, så adfærden er som før. "Intet fundet" gemmes også. + +ADCache = {} + +ADCache.buckets = {} +-- tændes af ADSmartPickup når hooksene er installeret (i spillet); tests kører uden cache +ADCache.enabled = false +local NOTHING = {} + +-- Til moduler der indlæses uden adCache (tests): kald fn direkte. +function ADCacheGet(bucket, key, ttlMs, fn) + if ADCache == nil or not ADCache.enabled then return fn() end + return ADCache.get(bucket, key, ttlMs, fn) +end + +local function now() + local mission = g_currentMission + if mission ~= nil and type(mission.time) == "number" then return mission.time end + return nil +end + +-- Resultatet af fn() for (bucket, key), genbrugt i ttlMs. +function ADCache.get(bucket, key, ttlMs, fn) + local time = now() + if not ADCache.enabled or time == nil or key == nil then return fn() end + local store = ADCache.buckets[bucket] + if store == nil then + store = {size = 0, entries = {}} + ADCache.buckets[bucket] = store + end + local entry = store.entries[key] + if entry ~= nil and time - entry.at < ttlMs then + if entry.value == NOTHING then return nil end + return entry.value + end + local value = fn() + if entry == nil then + store.size = store.size + 1 + if store.size > 5000 then store.entries, store.size = {}, 1 end + end + -- spredt udløb (levetid +0-50 %): poster der blev lavet i samme kald, udløber ikke i samme frame + -- (simulering 26/9: 600 markør-opslag udløb samtidig -> hak hvert 10. s) + store.entries[key] = {value = value == nil and NOTHING or value, at = time + math.random() * ttlMs * 0.5} + return value +end + +function ADCache.clear(bucket) + if bucket == nil then ADCache.buckets = {} else ADCache.buckets[bucket] = nil end +end diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua index 7aea5da..24facca 100644 --- a/FS25_ADSmartPickup/adFieldJobs.lua +++ b/FS25_ADSmartPickup/adFieldJobs.lua @@ -20,7 +20,8 @@ ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all -- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på) ADFieldJobs.courses = {} ADFieldJobs.finished = {} -ADFieldJobs.timer = 0 +-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame +ADFieldJobs.timer = 500 ADFieldJobs.now = 0 ADFieldJobs.loaded = false diff --git a/FS25_ADSmartPickup/adLoadSwap.lua b/FS25_ADSmartPickup/adLoadSwap.lua index d53424c..f8f6799 100644 --- a/FS25_ADSmartPickup/adLoadSwap.lua +++ b/FS25_ADSmartPickup/adLoadSwap.lua @@ -166,7 +166,8 @@ end -- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne. -- true = traktoren er sendt til en silo (Wait-punktet er frigivet). function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable) - wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0) + -- tilfældig start (0-2 s): ventende vogne tjekker ikke alle i samme frame + wait.swapTimerMs = (wait.swapTimerMs or math.random(0, ADLoadSwap.CHECK_INTERVAL_MS)) + (dt or 0) if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then return false end diff --git a/FS25_ADSmartPickup/adOutbound.lua b/FS25_ADSmartPickup/adOutbound.lua index baf1232..bb3d72a 100644 --- a/FS25_ADSmartPickup/adOutbound.lua +++ b/FS25_ADSmartPickup/adOutbound.lua @@ -14,6 +14,8 @@ ADOutbound = {} ADOutbound.LOG_PREFIX = "ADSmartPickup: " -- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører). ADOutbound.CHECK_INTERVAL_MS = 5000 +-- spred over HELE intervallet, så genplanlægningerne (og cache-genberegninger) fordeles jævnt +ADOutbound.CHECK_JITTER_MS = 5000 -- "Kør uanset mængde": stiger vognens fyldning ikke så længe ved kilden, køres der med det der er. ADOutbound.NO_PROGRESS_MS = 15000 -- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl). @@ -498,7 +500,8 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById) warnOnce(adEnv, vehicle, "noWait", string.format("'%s' har intet ledigt Wait-punkt med vejnet (run '%s'); holder ved kilden %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(run.name), tostring(marker.name))) end local previous = holds[vehicle] - holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = 0} + -- tilfældig start (0-2 s) så holdende vogne ikke genplanlægger i samme frame (hak hvert 5. s, 26/9) + holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = math.random(0, ADOutbound.CHECK_JITTER_MS)} local message = string.format("'%s' holder ved %s: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker.name), table.concat(reasons, "; ")) if previous == nil or previous.lastLogged ~= message then Logging.info(ADOutbound.LOG_PREFIX .. "%s", message) diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 1a047bc..aff3a52 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -10,6 +10,13 @@ ADSmartPickup = {} +-- ADCache-opslag der også virker uden adCache (tests) +local function cacheGet(bucket, key, ttlMs, fn) + if ADCacheGet == nil then return fn() end + return ADCacheGet(bucket, key, ttlMs, fn) +end + + ADSmartPickup.dir = g_currentModDirectory if ADSmartPickup.dir ~= nil and source ~= nil then source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua") @@ -216,14 +223,21 @@ local function getUnloadStationAtMarker(adEnv, wayPoint, farmId) end -- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station. +-- Markør -> station er stabil (stationer og markører flytter sig sjældent): caches 10 s (ADCache). +ADSmartPickup.STATION_CACHE_MS = 30000 + function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId) - local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) - return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil + return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function() + local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) + return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil + end) end function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId) - local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) - return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil + return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function() + local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) + return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil + end) end -- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen". @@ -1194,6 +1208,7 @@ function ADSmartPickup:update(dt) installHook(adEnv) isHooked = true if ADProfiler ~= nil then pcall(ADProfiler.install) end + if ADCache ~= nil then ADCache.enabled = true end if ADRunsController ~= nil then pcall(ADRunsController.load, adEnv) pcall(ADRunsController.installSaveHook) diff --git a/FS25_ADSmartPickup/adSources.lua b/FS25_ADSmartPickup/adSources.lua index 0e5a4b6..0232712 100644 --- a/FS25_ADSmartPickup/adSources.lua +++ b/FS25_ADSmartPickup/adSources.lua @@ -5,6 +5,13 @@ ADSources = {} +-- ADCache-opslag der også virker uden adCache (tests) +local function cacheGet(bucket, key, ttlMs, fn) + if ADCacheGet == nil then return fn() end + return ADCacheGet(bucket, key, ttlMs, fn) +end + + ADSources.LOG_PREFIX = "ADSmartPickup: " -- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere). ADSources.MAX_DISTANCE = 40 @@ -136,6 +143,12 @@ end -- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil. function ADSources.identify(adEnv, wayPointId, farmId) + return cacheGet("identify", tostring(wayPointId) .. ":" .. tostring(farmId), 10000, function() + return ADSources.identifyUncached(adEnv, wayPointId, farmId) + end) +end + +function ADSources.identifyUncached(adEnv, wayPointId, farmId) local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) if wayPoint == nil then return nil @@ -552,6 +565,10 @@ end -- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }. function ADSources.getSellMarkers(adEnv, farmId) + return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end) +end + +function ADSources.findSellMarkers(adEnv, farmId) local result = {} for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId) @@ -999,6 +1016,10 @@ end -- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} } -- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn. function ADSources.getFarmInventory(adEnv, farmId) + return cacheGet("inventory", tostring(farmId), 2000, function() return ADSources.buildFarmInventory(adEnv, farmId) end) +end + +function ADSources.buildFarmInventory(adEnv, farmId) local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {} local result = {} for _, placeable in pairs(placeables) do diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua index 8483833..32dbd1b 100644 --- a/FS25_ADSmartPickup/adUnloadWait.lua +++ b/FS25_ADSmartPickup/adUnloadWait.lua @@ -309,7 +309,7 @@ end -- stående for evigt. Her genoptages ventningen for aktive traktorer i et forsynings-run hvis -- aflæsningsopgave peger på en Wait-markør: hjem = run'ets mål, og reservationen sættes igen. -- Kaldes fra ADSmartPickup:update (throttlet). -ADUnloadWait.ADOPT_INTERVAL_MS = 5000 +ADUnloadWait.ADOPT_INTERVAL_MS = 5300 local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS function ADUnloadWait.adoptOrphans(adEnv) diff --git a/FS25_ADSmartPickup/adWaitPool.lua b/FS25_ADSmartPickup/adWaitPool.lua index 02f5691..7b94ad7 100644 --- a/FS25_ADSmartPickup/adWaitPool.lua +++ b/FS25_ADSmartPickup/adWaitPool.lua @@ -5,6 +5,13 @@ ADWaitPool = {} +-- ADCache-opslag der også virker uden adCache (tests) +local function cacheGet(bucket, key, ttlMs, fn) + if ADCacheGet == nil then return fn() end + return ADCacheGet(bucket, key, ttlMs, fn) +end + + ADWaitPool.LOG_PREFIX = "ADSmartPickup: " -- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi ADWaitPool.STATE_DRIVING = 2 @@ -14,6 +21,10 @@ ADWaitPool.claims = setmetatable({}, {__mode = "v"}) -- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper. function ADWaitPool.getCandidates(adEnv, poolGroup) + return cacheGet("waitCandidates", tostring(poolGroup or ""), 10000, function() return ADWaitPool.findCandidates(adEnv, poolGroup) end) +end + +function ADWaitPool.findCandidates(adEnv, poolGroup) local candidates = {} for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup @@ -57,20 +68,31 @@ ADWaitPool.OCCUPIED_RADIUS = 6 -- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et -- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning). +-- Alle motoriserede køretøjers positioner, delt mellem opslag i 0,5 s (ikke én runde pr. vogn pr. opslag). +local function allVehiclePositions() + return cacheGet("vehiclePositions", "all", 500, function() + local list = {} + local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil + for _, other in pairs(system ~= nil and system.vehicles or {}) do + if other.spec_motorized ~= nil and other.rootNode ~= nil then + local ok, x, _, z = pcall(getWorldTranslation, other.rootNode) + if ok and x ~= nil then table.insert(list, {vehicle = other, x = x, z = z}) end + end + end + return list + end) +end + local function otherVehiclePositions(vehicle) local positions = {} - local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil local own = {} if vehicle.getChildVehicles ~= nil then local ok, children = pcall(vehicle.getChildVehicles, vehicle) for _, child in pairs(ok and children or {}) do own[child] = true end end own[vehicle] = true - for _, other in pairs(system ~= nil and system.vehicles or {}) do - if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then - local ok, x, _, z = pcall(getWorldTranslation, other.rootNode) - if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end - end + for _, entry in ipairs(allVehiclePositions()) do + if not own[entry.vehicle] then table.insert(positions, entry) end end return positions end diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index ef05a4d..23ac73c 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.27.3.0 + 1.28.0.0 <en>AD Smart Pickup</en> @@ -12,6 +12,7 @@ + diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..bba8ed4 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,10 @@ +# Simuleringer + +`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner +(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode. + + luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode + git worktree add /tmp/old ; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign + +Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning. +luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms. diff --git a/bench/bench_hak.lua b/bench/bench_hak.lua new file mode 100644 index 0000000..b6066b7 --- /dev/null +++ b/bench/bench_hak.lua @@ -0,0 +1,103 @@ +-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua ) +-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3 +-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode +-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame. +-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller. +local dir = arg[1] or "FS25_ADSmartPickup" +math.randomseed(42) +local function noop() end +Logging = {info = noop, warning = noop} +MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end} +function entityExists() return true end +local nodes = {} +function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end +local simTime = 0 +g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}} +function netGetTime() return os.clock() * 1000 end +addModEventListener = noop +Utils = {prependedFunction = function(a) return a end} + +-- vejnet og markører +local wayPoints, markers = {}, {} +for id = 1, 13550 do + local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600 + wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}} +end +for index = 1, 600 do + local id = index * 22 + table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"}) +end +-- stationer ved hver 5. markør, 3 triggere hver +local stations = {} +for index = 1, 120 do + local wp = wayPoints[markers[150 + index].id] + local triggers = {} + for t = 1, 3 do + local node = #nodes + 1 + nodes[node] = {wp.x + t, wp.z + t} + table.insert(triggers, {exactFillRootNode = node}) + end + table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index, + isa = function() return false end, getIsFillTypeAllowed = function() return true end}) +end +g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end} +g_currentMission.placeableSystem = {placeables = {}} +-- køretøjer +local vehicles = {} +for index = 1, 40 do + local node = #nodes + 1 + nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)} + table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end}) +end +g_currentMission.vehicleSystem = {vehicles = vehicles} +local adEnv = {ADGraphManager = { + getMapMarkers = function() return markers end, + getWayPointById = function(_, id) return wayPoints[id] end, + getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end, + getPathTo = function(_, _, id) return {{id = id}} end, +}} + +for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do + local chunk = loadfile(dir .. "/" .. file) + if chunk ~= nil then pcall(chunk) end +end +if ADCache ~= nil then ADCache.enabled = true end +ADPrices = ADPrices or {} +ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end + +-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager) +local function replan(vehicle) + ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end) + for _, marker in ipairs(markers) do + ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1) + end +end + +local t0 = os.clock() +replan(vehicles[1]) +local single = (os.clock() - t0) * 1000 + +-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det. +local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS) +local timers = {} +for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end +local worst, steady, frames = 0, 0, 0 +for frame = 1, 60 * 60 do + simTime = simTime + 16.7 + g_currentMission.time = simTime + local started = os.clock() + for index = 1, 15 do + timers[index] = timers[index] + 16.7 + if timers[index] >= 5000 then + timers[index] = 0 + replan(vehicles[index]) + end + end + local spent = (os.clock() - started) * 1000 + if spent > worst then worst = spent end + if simTime > 12000 and spent > steady then steady = spent end + if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end + frames = frames + 1 +end +print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " ")) +print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady)) diff --git a/docs/BRUGERMANUAL.md b/docs/BRUGERMANUAL.md index 7bc8e42..c05cf3d 100644 --- a/docs/BRUGERMANUAL.md +++ b/docs/BRUGERMANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — brugermanual -Version 1.27.3 · Farming Simulator 25 · tillæg til AutoDrive +Version 1.28.0 · Farming Simulator 25 · tillæg til AutoDrive *English version: [USER_MANUAL.md](USER_MANUAL.md)* diff --git a/docs/TESTPLAN-markarbejde.md b/docs/TESTPLAN-markarbejde.md index 95f87af..9ca9f9a 100644 --- a/docs/TESTPLAN-markarbejde.md +++ b/docs/TESTPLAN-markarbejde.md @@ -1,4 +1,4 @@ -# Testplan — Markarbejde (fanen Marker), version 1.27.3.0 +# Testplan — Markarbejde (fanen Marker), version 1.28.0.0 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 @@ -11,7 +11,7 @@ samme rækkefølge. ## 0. Forberedelse -- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.3.0) FS25_ADSmartPickup`. +- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.28.0.0) FS25_ADSmartPickup`. - [ ] **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). Afgrøde-kolonnen viser afgrøden eller "bar jord". diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index 1d65936..4bf80d1 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — user manual -Version 1.27.3 · Farming Simulator 25 · add-on for AutoDrive +Version 1.28.0 · Farming Simulator 25 · add-on for AutoDrive *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* diff --git a/tests/test_adCache.lua b/tests/test_adCache.lua new file mode 100644 index 0000000..5e425ec --- /dev/null +++ b/tests/test_adCache.lua @@ -0,0 +1,40 @@ +-- Kør: luajit tests/test_adCache.lua (fra repo-roden) +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 +g_currentMission = {time = 0} +dofile("FS25_ADSmartPickup/adCache.lua") +local calls = 0 +local function lookup() calls = calls + 1; return "station" end +check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation") +check("K1 to opslag", calls, 2) +ADCache.enabled = true +calls = 0 +ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup) +check("K2 genbrugt", calls, 1) +g_currentMission.time = 900 +ADCacheGet("b", "k", 1000, lookup) +check("K2 inden for levetid", calls, 1) +g_currentMission.time = 1600 +ADCacheGet("b", "k", 1000, lookup) +check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2) +local nothing = 0 +ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end) +check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil) +check("K4 kun ét opslag", nothing, 1) +-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig +g_currentMission.time = 10000 +for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end +local expired = 0 +g_currentMission.time = 22000 +for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end +check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true) +ADCache.clear() +check("K6 ryddet", next(ADCache.buckets), nil) + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)