Files
fs25-adsmartpickup/FS25_ADSmartPickup/adRuns.lua
T

413 lines
16 KiB
Lua

-- 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 = 3
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),
sourceBuildingIds = copyList(run.sourceBuildingIds),
targetBuildingIds = copyList(run.targetBuildingIds),
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 = {},
sourceBuildingIds = {},
targetBuildingIds = {},
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.sourceBuildingIds ~= nil then run.sourceBuildingIds = copyList(changes.sourceBuildingIds) end
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) 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
-- ---- bygninger som kilder/mål (flow-tavlen) ----
local function toggleInList(list, value)
local result, found = {}, false
for _, item in ipairs(list or {}) do
if item == value then found = true else table.insert(result, item) end
end
if not found then table.insert(result, value) end
return result
end
function ADRuns.toggleSourceBuilding(store, runId, buildingId)
local run = ADRuns.getRun(store, runId)
if run == nil then return copyStore(store) end
return ADRuns.updateRun(store, runId, {sourceBuildingIds = toggleInList(run.sourceBuildingIds, buildingId)})
end
function ADRuns.toggleTargetBuilding(store, runId, buildingId)
local run = ADRuns.getRun(store, runId)
if run == nil then return copyStore(store) end
return ADRuns.updateRun(store, runId, {targetBuildingIds = toggleInList(run.targetBuildingIds, buildingId)})
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 {}, " "),
sourceBuildings = table.concat(run.sourceBuildingIds or {}, " "),
targetBuildings = table.concat(run.targetBuildingIds 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 function parseIds(text)
local ids = {}
for token in string.gmatch(tostring(text or ""), "%S+") do table.insert(ids, token) end
return ids
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,
sourceBuildingIds = parseIds(record.sourceBuildings),
targetBuildingIds = parseIds(record.targetBuildings),
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