Files

549 lines
22 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).
-- Start alle pr. flow: isRunning (traktorer kører AD), stopped (pause fra Stop alt / Stop).
-- -> "queue" (start traktorerne), "resume" (kører allerede: kun pausen ophæves), "skip".
-- In-game 29/9: flows på pause hvis traktorer kørte videre på en gemt AD-tur blev talt som 'kørte allerede',
-- forblev stoppede, og traktoren kørte vognen hjem lige efter at have hentet den.
function ADRuns.startAllAction(isRunning, stopped)
if not isRunning then return "queue" end
return stopped and "resume" or "skip"
end
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),
chain = run.chain or "",
}
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 = {},
chain = "",
}
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
-- 0 = "Altid" (sælg uanset pris); ellers 50-100 %
if changes.sellAtPercent ~= nil then run.sellAtPercent = tonumber(changes.sellAtPercent) == 0 and 0 or 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
if changes.chain ~= nil then run.chain = tostring(changes.chain) 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),
chain = run.chain or "",
}
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 = tonumber(record.sellAt) == 0 and 0 or 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),
chain = record.chain or "",
})
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