From 68ad63ce1fe441497fea6e62ce7d81b206058b56 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Wed, 23 Sep 2026 09:46:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20run-model=20v2=20=E2=80=94=20retning,?= =?UTF-8?q?=20kilder,=20varepolitik,=20t=C3=A6rskler,=20runde=20(XML=20v2?= =?UTF-8?q?=20+=20v1-migration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adRuns.lua | 127 ++++++++++++++++++++++++++- FS25_ADSmartPickup/adRunsStorage.lua | 13 +++ tests/test_adRuns.lua | 62 ++++++++++++- 3 files changed, 199 insertions(+), 3 deletions(-) diff --git a/FS25_ADSmartPickup/adRuns.lua b/FS25_ADSmartPickup/adRuns.lua index 7794b3a..7d62cc7 100644 --- a/FS25_ADSmartPickup/adRuns.lua +++ b/FS25_ADSmartPickup/adRuns.lua @@ -9,8 +9,14 @@ ADRuns.MAX_LOOPS = 99 -- "Kør hjem fra Wait når målet er under X %": 10..90, standard 50 (= den gamle 50 %-regel). ADRuns.DEFAULT_RETURN_BELOW_PERCENT = 50 ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT = 10, 90 -ADRuns.XML_VERSION = 1 +ADRuns.XML_VERSION = 2 ADRuns.INGREDIENT_MODES = {auto = true, manual = true} +-- Retning: Forsyning (ind til stald/fabrik) eller Udkørsel (fabrik/lager -> silo/salg). +ADRuns.KINDS = {supply = true, outbound = true} +-- Politik pr. vare i et udkørsels-run; "off" = varen er ikke med (fjernes fra listen). +ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true} +ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100 +ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95 local function copyList(list) local copy = {} @@ -20,10 +26,29 @@ local function copyList(list) return copy end +local function copyProducts(products) + local copy = {} + for index, product in ipairs(products or {}) do + copy[index] = {fillType = product.fillType, policy = product.policy} + end + return copy +end + +local function clampRange(value, default, min, max) + local number = tonumber(value) or default + return math.max(min, math.min(max, math.floor(number))) +end + local function copyRun(run) return { id = run.id, name = run.name, + kind = run.kind, + sourceWayPointIds = copyList(run.sourceWayPointIds), + products = copyProducts(run.products), + sellAtPercent = run.sellAtPercent, + storeAbovePercent = run.storeAbovePercent, + collectRound = run.collectRound, targetWayPointId = run.targetWayPointId, targetWayPointIds = copyList(run.targetWayPointIds), ingredientMode = run.ingredientMode, @@ -57,11 +82,17 @@ function ADRuns.newStore() return {nextId = 1, runs = {}} end -function ADRuns.createRun(store, name) +function ADRuns.createRun(store, name, kind) local new = copyStore(store) local run = { id = new.nextId, name = name, + kind = ADRuns.KINDS[kind] and kind or "supply", + sourceWayPointIds = {}, + products = {}, + sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT, + storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT, + collectRound = false, targetWayPointId = nil, targetWayPointIds = {}, ingredientMode = "auto", @@ -119,9 +150,84 @@ function ADRuns.updateRun(store, runId, changes) if changes.waitPoolGroup ~= nil then run.waitPoolGroup = changes.waitPoolGroup end if changes.loops ~= nil then run.loops = clampLoops(changes.loops) end if changes.returnBelowPercent ~= nil then run.returnBelowPercent = clampPercent(changes.returnBelowPercent) end + if ADRuns.KINDS[changes.kind] then run.kind = changes.kind end + if changes.sourceWayPointIds ~= nil then run.sourceWayPointIds = copyList(changes.sourceWayPointIds) end + if changes.products ~= nil then run.products = copyProducts(changes.products) end + if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end + if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end + if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end return new end +-- ---- udkørsel: kilder og varer ---- + +-- Tilføj/fjern en kilde-markør (afkrydsning i fanen Udkørsel). +function ADRuns.toggleSource(store, runId, wayPointId) + local run = ADRuns.getRun(store, runId) + if run == nil then + return copyStore(store) + end + local sources, found = {}, false + for _, id in ipairs(run.sourceWayPointIds) do + if id == wayPointId then + found = true + else + table.insert(sources, id) + end + end + if not found then + table.insert(sources, wayPointId) + end + return ADRuns.updateRun(store, runId, {sourceWayPointIds = sources}) +end + +function ADRuns.getProductPolicy(run, fillTypeName) + for _, product in ipairs(run.products or {}) do + if product.fillType == fillTypeName then + return product.policy + end + end + return "off" +end + +-- policy "off" fjerner varen; ukendt politik ignoreres (store uændret). +function ADRuns.setProductPolicy(store, runId, fillTypeName, policy) + local run = ADRuns.getRun(store, runId) + if run == nil or (policy ~= "off" and not ADRuns.POLICIES[policy]) then + return copyStore(store) + end + local products = {} + for _, product in ipairs(run.products) do + if product.fillType ~= fillTypeName then + table.insert(products, product) + end + end + if policy ~= "off" then + table.insert(products, {fillType = fillTypeName, policy = policy}) + end + return ADRuns.updateRun(store, runId, {products = products}) +end + +-- "SUGAR:sell FORAGE:store" — navne er stabile på tværs af mods, fillType-indeks er det ikke. +function ADRuns.serializeProducts(products) + local parts = {} + for _, product in ipairs(products or {}) do + table.insert(parts, product.fillType .. ":" .. product.policy) + end + return table.concat(parts, " ") +end + +function ADRuns.parseProducts(text) + local products = {} + for token in string.gmatch(tostring(text or ""), "%S+") do + local name, policy = string.match(token, "^([^:]+):(%a+)$") + if name ~= nil and ADRuns.POLICIES[policy] then + table.insert(products, {fillType = name, policy = policy}) + end + end + return products +end + -- Tilføj/fjern et mål (afkrydsning i fanen). function ADRuns.toggleTarget(store, runId, wayPointId) local run = ADRuns.getRun(store, runId) @@ -211,6 +317,12 @@ function ADRuns.toRecords(store) loops = run.loops, returnBelow = run.returnBelowPercent, vehicleIds = copyList(run.vehicleIds), + kind = run.kind, + sources = table.concat(run.sourceWayPointIds or {}, " "), + products = ADRuns.serializeProducts(run.products), + sellAt = run.sellAtPercent, + storeAbove = run.storeAbovePercent, + collectRound = run.collectRound and 1 or 0, } end return records @@ -232,9 +344,20 @@ function ADRuns.fromRecords(records) if #targets == 0 and target ~= nil and target > 0 then targets = {target} end + local sources = {} + for token in string.gmatch(tostring(record.sources or ""), "%S+") do + local wayPointId = tonumber(token) + if wayPointId ~= nil and wayPointId > 0 then table.insert(sources, wayPointId) end + end table.insert(store.runs, { id = id, name = record.name, + kind = ADRuns.KINDS[record.kind] and record.kind or "supply", + sourceWayPointIds = sources, + products = ADRuns.parseProducts(record.products), + sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT), + storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT), + collectRound = tonumber(record.collectRound) == 1, targetWayPointId = targets[1], targetWayPointIds = targets, ingredientMode = ADRuns.INGREDIENT_MODES[record.ingredientMode] and record.ingredientMode or "auto", diff --git a/FS25_ADSmartPickup/adRunsStorage.lua b/FS25_ADSmartPickup/adRunsStorage.lua index 7e167a2..8007ad5 100644 --- a/FS25_ADSmartPickup/adRunsStorage.lua +++ b/FS25_ADSmartPickup/adRunsStorage.lua @@ -36,6 +36,13 @@ function ADRunsStorage.writeRecords(xmlFile, records) xmlFile:setString(key .. "#waitPool", record.waitPool) xmlFile:setInt(key .. "#loops", record.loops) xmlFile:setInt(key .. "#returnBelow", record.returnBelow or ADRuns.DEFAULT_RETURN_BELOW_PERCENT) + -- v2: udkørsel + xmlFile:setString(key .. "#kind", record.kind or "supply") + xmlFile:setString(key .. "#sources", record.sources or "") + xmlFile:setString(key .. "#products", record.products or "") + xmlFile:setInt(key .. "#sellAt", record.sellAt or ADRuns.DEFAULT_SELL_AT_PERCENT) + xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT) + xmlFile:setInt(key .. "#collectRound", record.collectRound or 0) for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId) end @@ -55,6 +62,12 @@ function ADRunsStorage.readRecords(xmlFile) waitPool = xmlFile:getString(key .. "#waitPool"), loops = xmlFile:getInt(key .. "#loops"), returnBelow = xmlFile:getInt(key .. "#returnBelow"), + kind = xmlFile:getString(key .. "#kind"), + sources = xmlFile:getString(key .. "#sources"), + products = xmlFile:getString(key .. "#products"), + sellAt = xmlFile:getInt(key .. "#sellAt"), + storeAbove = xmlFile:getInt(key .. "#storeAbove"), + collectRound = xmlFile:getInt(key .. "#collectRound"), vehicleIds = {}, } xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey) diff --git a/tests/test_adRuns.lua b/tests/test_adRuns.lua index c029923..b55591b 100644 --- a/tests/test_adRuns.lua +++ b/tests/test_adRuns.lua @@ -157,7 +157,7 @@ end -- X1: records skrives som attributter + vehicle-underelementer local out = makeXmlMock() ADRunsStorage.writeRecords(out, ADRuns.toRecords(s5)) -check("X1 version", out.values["ADSmartPickupRuns#version"], 1) +check("X1 version", out.values["ADSmartPickupRuns#version"], 2) check("X1 run 0 navn", out.values["ADSmartPickupRuns.run(0)#name"], "Cow 1") check("X1 run 0 target", out.values["ADSmartPickupRuns.run(0)#target"], 94) check("X1 run 0 varer", out.values["ADSmartPickupRuns.run(0)#ingredients"], "STRAW FORAGE") @@ -193,5 +193,65 @@ check("X3 uden savegame -> tom store", #ADRunsStorage.load().runs, 0) g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg-ny"}} check("X3 fil mangler -> tom store", #ADRunsStorage.load().runs, 0) +-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration +local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound") +check("Rv2 kind", out.kind, "outbound") +local _, plain = ADRuns.createRun(ADRuns.newStore(), "x") +check("Rv2 supply som standard", plain.kind, "supply") +check("Rv2 sellAt 90", out.sellAtPercent, 90) +check("Rv2 storeAbove 80", out.storeAbovePercent, 80) +check("Rv2 runde fra", out.collectRound, false) +local o2 = ADRuns.toggleSource(o1, out.id, 12694) +o2 = ADRuns.toggleSource(o2, out.id, 12386) +check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386") +check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1) +local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell") +o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store") +check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell") +check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off") +check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell") +check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off") +check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0) +local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true}) +check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100) +check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50) +check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true) +check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store") +local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus") +check("Rv2 parse antal", #parsed, 2) +check("Rv2 parse politik", parsed[2].policy, "store") +local rt = ADRuns.fromRecords(ADRuns.toRecords(o4)) +local rtRun = ADRuns.getRun(rt, out.id) +check("Rv2 roundtrip kind", rtRun.kind, "outbound") +check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386") +check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store") +check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100) +check("Rv2 roundtrip runde", rtRun.collectRound, true) +local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}}) +check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply") +check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0) +check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90) +-- XML-laget skriver/læser v2-felterne +do + local written = {} + local xml = { + setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end, + getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end, + iterate = function(_, prefix, fn) + local i = 0 + while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end + end, + } + ADRunsStorage.writeRecords(xml, ADRuns.toRecords(o4)) + check("Rv2 xml version 2", written["ADSmartPickupRuns#version"], 2) + check("Rv2 xml kind", written["ADSmartPickupRuns.run(0)#kind"], "outbound") + check("Rv2 xml sources", written["ADSmartPickupRuns.run(0)#sources"], "12694 12386") + check("Rv2 xml products", written["ADSmartPickupRuns.run(0)#products"], "SUGAR:sell FORAGE:store") + check("Rv2 xml collectRound", written["ADSmartPickupRuns.run(0)#collectRound"], 1) + local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml)) + check("Rv2 xml læs sellAt", ADRuns.getRun(back, out.id).sellAtPercent, 100) + check("Rv2 xml læs runde", ADRuns.getRun(back, out.id).collectRound, true) +end + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)