-- AD Runs -- Rene funktioner (ingen spil-API) for "runs": navn, aflæsningsmål, varer, Wait-pulje, -- loop-tæller og tildelte traktorer. Alle funktioner returnerer en NY store — -- input røres aldrig, så GUI og controller kan holde referencer uden overraskelser. ADRuns = {} 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 = 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 = {} for index, value in ipairs(list or {}) do copy[index] = value end 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, ingredients = copyList(run.ingredients), waitPoolGroup = run.waitPoolGroup, loops = run.loops, returnBelowPercent = run.returnBelowPercent, vehicleIds = copyList(run.vehicleIds), } end local function copyStore(store) local runs = {} for index, run in ipairs(store.runs) do runs[index] = copyRun(run) end return {nextId = store.nextId, runs = runs} end local function clampPercent(percent) local value = tonumber(percent) or ADRuns.DEFAULT_RETURN_BELOW_PERCENT return math.max(ADRuns.MIN_RETURN_BELOW_PERCENT, math.min(ADRuns.MAX_RETURN_BELOW_PERCENT, math.floor(value))) end local function clampLoops(loops) local value = tonumber(loops) or 0 return math.max(0, math.min(ADRuns.MAX_LOOPS, math.floor(value))) end function ADRuns.newStore() return {nextId = 1, runs = {}} end 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", ingredients = {}, waitPoolGroup = "", loops = 0, returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT, vehicleIds = {}, } new.nextId = new.nextId + 1 table.insert(new.runs, run) return new, run end function ADRuns.getRun(store, runId) for _, run in ipairs(store.runs) do if run.id == runId then return run end end return nil end function ADRuns.deleteRun(store, runId) local new = copyStore(store) for index, run in ipairs(new.runs) do if run.id == runId then table.remove(new.runs, index) break end end return new end -- changes: delmængde af {name, targetWayPointId, ingredientMode, ingredients, waitPoolGroup, loops}. function ADRuns.updateRun(store, runId, changes) local new = copyStore(store) local run = ADRuns.getRun(new, runId) if run == nil then return new end if changes.name ~= nil then run.name = changes.name end -- Mål: liste (flere stalde pr. run); targetWayPointId = første = "primært" (bagudkompatibelt). -- false = ryd målene (nil i changes betyder "rør ikke") if changes.targetWayPointIds ~= nil then run.targetWayPointIds = copyList(changes.targetWayPointIds) elseif changes.targetWayPointId == false then run.targetWayPointIds = {} elseif changes.targetWayPointId ~= nil then run.targetWayPointIds = {changes.targetWayPointId} end run.targetWayPointId = run.targetWayPointIds[1] if ADRuns.INGREDIENT_MODES[changes.ingredientMode] then run.ingredientMode = changes.ingredientMode end if changes.ingredients ~= nil then run.ingredients = copyList(changes.ingredients) end 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) if run == nil then return copyStore(store) end local targets = {} local found = false for _, id in ipairs(run.targetWayPointIds) do if id == wayPointId then found = true else table.insert(targets, id) end end if not found then table.insert(targets, wayPointId) end return ADRuns.updateRun(store, runId, {targetWayPointIds = targets}) end function ADRuns.findRunByVehicle(store, vehicleId) for _, run in ipairs(store.runs) do for _, id in ipairs(run.vehicleIds) do if id == vehicleId then return run end end end return nil end function ADRuns.unassignVehicle(store, vehicleId) local new = copyStore(store) for _, run in ipairs(new.runs) do for index, id in ipairs(run.vehicleIds) do if id == vehicleId then table.remove(run.vehicleIds, index) break end end end return new end -- En traktor er kun i ét run. Returnerer ny store + id på det run den blev flyttet FRA (nil = ingen). function ADRuns.assignVehicle(store, runId, vehicleId) local previous = ADRuns.findRunByVehicle(store, vehicleId) if previous ~= nil and previous.id == runId then return copyStore(store), nil end local new = ADRuns.unassignVehicle(store, vehicleId) local run = ADRuns.getRun(new, runId) if run == nil then return new, nil end table.insert(run.vehicleIds, vehicleId) return new, previous ~= nil and previous.id or nil end -- Varer gemmes som mellemrumsadskilte fillType-NAVNE ("STRAW FORAGE") — navne er -- stabile på tværs af mods/kort, fillType-indeks er det ikke. function ADRuns.serializeIngredients(list) return table.concat(list or {}, " ") end function ADRuns.parseIngredients(text) local list = {} for name in string.gmatch(tostring(text or ""), "%S+") do table.insert(list, name) end return list end -- Flade poster (kun strenge/tal) til XML-laget, i store'ens rækkefølge. function ADRuns.toRecords(store) local records = {} for index, run in ipairs(store.runs) do records[index] = { id = run.id, name = run.name, target = run.targetWayPointId or 0, targets = table.concat(run.targetWayPointIds, " "), ingredientMode = run.ingredientMode, ingredients = ADRuns.serializeIngredients(run.ingredients), waitPool = run.waitPoolGroup, 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 end -- Poster uden gyldigt id/navn eller med dublet-id springes over; nextId = højeste id + 1. function ADRuns.fromRecords(records) local store = ADRuns.newStore() for _, record in ipairs(records or {}) do local id = tonumber(record.id) local isValid = id ~= nil and id >= 1 and record.name ~= nil and record.name ~= "" if isValid and ADRuns.getRun(store, id) == nil then local target = tonumber(record.target) local targets = {} for token in string.gmatch(tostring(record.targets or ""), "%S+") do local wayPointId = tonumber(token) if wayPointId ~= nil and wayPointId > 0 then table.insert(targets, wayPointId) end end 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", ingredients = ADRuns.parseIngredients(record.ingredients), waitPoolGroup = record.waitPool or "", loops = clampLoops(record.loops), returnBelowPercent = clampPercent(record.returnBelow), vehicleIds = copyList(record.vehicleIds), }) store.nextId = math.max(store.nextId, id + 1) end end return store end