Files
fs25-adsmartpickup/FS25_ADSmartPickup/adRuns.lua
T

534 lines
21 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
-- "Sælg altid over X %": kildens lager over grænsen sælges uanset pris (0 = fra). Så hober f.eks. gylle
-- ved BGA'en sig ikke op, når prisen er lav.
ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT = 0, 0, 100
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
-- Flow-vogne: {[traktorId] = vognId} (maskinpulje — flowet henter sin egen vogn).
function ADRuns.copyWagons(wagons)
local copy = {}
for tractorId, wagonId in pairs(wagons or {}) do copy[tractorId] = wagonId end
return copy
end
function ADRuns.serializeWagons(wagons)
local keys = {}
for tractorId in pairs(wagons or {}) do table.insert(keys, tractorId) end
table.sort(keys)
local parts = {}
for _, tractorId in ipairs(keys) do table.insert(parts, tractorId .. "=" .. wagons[tractorId]) end
return table.concat(parts, ";")
end
function ADRuns.parseWagons(text)
local wagons = {}
for tractorId, wagonId in string.gmatch(tostring(text or ""), "([^=;]+)=([^=;]+)") do wagons[tractorId] = wagonId end
return wagons
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),
storeBuildingIds = copyList(run.storeBuildingIds),
products = copyProducts(run.products),
sellAtPercent = run.sellAtPercent,
storeAbovePercent = run.storeAbovePercent,
sellAbovePercent = run.sellAbovePercent,
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),
stopped = run.stopped == true,
wagons = ADRuns.copyWagons(run.wagons),
}
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 = {},
storeBuildingIds = {},
products = {},
sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT,
storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT,
sellAbovePercent = ADRuns.DEFAULT_SELL_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.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) 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.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_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
-- Tvunget lagermål for udkørsel: lagerture kører KUN til disse bygninger (tom = moden vælger selv).
function ADRuns.toggleStoreBuilding(store, runId, buildingId)
local run = ADRuns.getRun(store, runId)
if run == nil then return copyStore(store) end
return ADRuns.updateRun(store, runId, {storeBuildingIds = toggleInList(run.storeBuildingIds, 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
-- traktoren forlader flowet: dens huskede vogn hører ikke længere til flowet
if run.wagons ~= nil then run.wagons[vehicleId] = nil end
end
return new
end
-- Traktorer der ikke findes længere (solgt/byttet) fjernes fra deres flow. exists(vehicleId) -> bool.
-- Mangler ALLE flowenes traktorer, er indlæsningen nok ikke færdig/gået galt: intet fjernes.
-- -> ny store, {{runName, vehicleId}} over de fjernede
function ADRuns.pruneMissingVehicles(store, exists)
local missing, total = {}, 0
for _, run in ipairs(store.runs) do
for _, id in ipairs(run.vehicleIds) do
total = total + 1
if not exists(id) then table.insert(missing, {runName = run.name, vehicleId = id}) end
end
end
if #missing == 0 or #missing == total then return store, {} end
local new = store
for _, entry in ipairs(missing) do new = ADRuns.unassignVehicle(new, entry.vehicleId) end
return new, missing
end
-- Én besked pr. berørt flow efter pruneMissingVehicles: {runName, removed, left, critical} (critical = flowet
-- står uden traktorer og virker ikke). I samme rækkefølge som flowene.
function ADRuns.pruneNotices(storeAfter, removed)
local byName = {}
for _, entry in ipairs(removed or {}) do byName[entry.runName] = (byName[entry.runName] or 0) + 1 end
local notices = {}
for _, run in ipairs(storeAfter.runs) do
if byName[run.name] ~= nil then
table.insert(notices, {runName = run.name, removed = byName[run.name], left = #run.vehicleIds, critical = #run.vehicleIds == 0})
end
end
return notices
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 {}, " "),
storeBuildings = table.concat(run.storeBuildingIds or {}, " "),
products = ADRuns.serializeProducts(run.products),
sellAt = run.sellAtPercent,
storeAbove = run.storeAbovePercent,
sellAbove = run.sellAbovePercent or 0,
collectRound = run.collectRound and 1 or 0,
stopped = run.stopped and 1 or 0,
wagons = ADRuns.serializeWagons(run.wagons),
}
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),
storeBuildingIds = parseIds(record.storeBuildings),
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),
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_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),
stopped = tonumber(record.stopped) == 1,
wagons = ADRuns.parseWagons(record.wagons),
})
store.nextId = math.max(store.nextId, id + 1)
end
end
return store
end
-- ---- køretøjsnavne ----
-- Navn pr. køretøj efter det forløb det kører: "Milk Collect 1", "Milk Collect 2" … i medlemsrækkefølge;
-- er køretøjet alene i forløbet, bare forløbets navn. {[vehicleId] = navn}.
function ADRuns.vehicleNames(store)
local names = {}
for _, run in ipairs(store ~= nil and store.runs or {}) do
local count = #(run.vehicleIds or {})
for index, vehicleId in ipairs(run.vehicleIds or {}) do
names[vehicleId] = count > 1 and string.format("%s %d", tostring(run.name), index) or tostring(run.name)
end
end
return names
end
-- true når navnet er et forløbsnavn vi selv har sat ("Navn" eller "Navn 3").
function ADRuns.isRunVehicleName(store, name)
if name == nil then return false end
for _, run in ipairs(store ~= nil and store.runs or {}) do
local runName = tostring(run.name)
if name == runName then return true end
local prefix, number = string.match(name, "^(.-) (%d+)$")
if prefix == runName and number ~= nil then return true end
end
return false
end
-- Navnet i log og menu: AutoDrives førernavn (sat efter forløbet), ellers køretøjets modelnavn.
function ADRuns.vehicleLabel(vehicle)
local stateModule = vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule ~= nil and stateModule.getName ~= nil then
local ok, name = pcall(stateModule.getName, stateModule)
if ok and name ~= nil and name ~= "" then return tostring(name) end
end
if vehicle ~= nil and vehicle.getName ~= nil then
return tostring(vehicle:getName())
end
return "?"
end