363 lines
18 KiB
Lua
363 lines
18 KiB
Lua
-- AD Chain Run (kædens handlinger i spillet)
|
|
-- Udfører ADChain's plan: skifter frie marker (kun "auto", højst MAX_FIELD_SWITCHES pr. tick), tænder/slukker
|
|
-- mellemled, opretter manglende forsyningsflows (højst MAX_NEW_FLOWS pr. tick) og stopper flows for rødder der
|
|
-- er slået fra. Hver del kører i pcall, så en fejl ikke stopper resten. Spilkald kun inde i funktioner.
|
|
|
|
ADChainRun = {}
|
|
|
|
local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
|
local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end
|
|
|
|
local UNLOADER = "unloader"
|
|
|
|
-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool).
|
|
local function isUnloader(implement)
|
|
return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true)
|
|
end
|
|
|
|
-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle
|
|
-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig.
|
|
local function belongsToFlow(tractor)
|
|
return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil
|
|
end
|
|
|
|
-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og
|
|
-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu).
|
|
function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked)
|
|
if fillTypeIndex == nil then return nil end
|
|
used, blocked = used or {}, blocked or {}
|
|
local free = {}
|
|
for _, tractor in ipairs(tractors or {}) do
|
|
if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then
|
|
table.insert(free, tractor)
|
|
end
|
|
end
|
|
for _, implement in ipairs(implements or {}) do
|
|
if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then
|
|
local tractor = ADPool.bestFit(free, implement, "unloader")
|
|
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- ---- marker ----
|
|
|
|
local function fieldById(fields, id)
|
|
for _, field in ipairs(fields or {}) do
|
|
if field.id == id then return field end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ADChainRun.switchFields(data, actions)
|
|
local switched = 0
|
|
for _, action in ipairs(actions or {}) do
|
|
if switched >= ADChain.MAX_FIELD_SWITCHES then break end
|
|
local field = fieldById(data.fields, action.fieldId)
|
|
-- brugerens egen målafgrøde røres aldrig (dobbelttjek oven i ADCropPlan)
|
|
if action.mode == "auto" and field ~= nil and field.targetSource ~= "user" then
|
|
local ok, err = pcall(ADFieldJobs.setTarget, action.fieldId, action.to, "chain")
|
|
if ok then
|
|
switched = switched + 1
|
|
ADChain.switchCount = ADChain.switchCount + 1
|
|
local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from),
|
|
tostring(action.to), action.hectares or 0, tostring(action.reason))
|
|
log("%s", text)
|
|
pcall(ADFieldJobs.notify, text, false)
|
|
else
|
|
warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---- mellemled ----
|
|
|
|
local function setProduction(data, key, isOn)
|
|
local entry = data.points[key]
|
|
if entry == nil or entry.point == nil or entry.point.setProductionState == nil then
|
|
warn("produktion %s findes ikke (længere)", tostring(key))
|
|
return false
|
|
end
|
|
local ok, err = pcall(entry.point.setProductionState, entry.point, entry.productionId, isOn)
|
|
if not ok then warn("produktion %s kunne ikke %s: %s", tostring(key), isOn and "tændes" or "slukkes", tostring(err)) end
|
|
return ok
|
|
end
|
|
|
|
-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen.
|
|
-- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra).
|
|
function ADChainRun.toggleProductions(data, prod)
|
|
local state = ADChain.state
|
|
local byChain = state.byChain
|
|
for _, key in ipairs(prod.enable or {}) do
|
|
if not state.userOff[key] and setProduction(data, key, true) then
|
|
byChain[key] = true
|
|
log("tænder %s (mellemled i kæden)", key)
|
|
end
|
|
end
|
|
if data.partial then return end
|
|
for _, key in ipairs(prod.disable or {}) do
|
|
if setProduction(data, key, false) then
|
|
byChain[key] = nil
|
|
log("slukker %s (kæden bruger den ikke længere)", key)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---- flows ----
|
|
|
|
local function collectPool(adEnv)
|
|
local ok, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
|
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
|
end)
|
|
if not ok then
|
|
warn("maskinpuljen kunne ikke læses: %s", tostring(implements))
|
|
return {}, {}
|
|
end
|
|
return implements or {}, tractors or {}
|
|
end
|
|
|
|
-- kind = "noVehicle"|"pallet": vises som status på kanten i fanen Kæder
|
|
local function createStopped(run, kind, reason, ...)
|
|
run.stopped = true
|
|
ADChain.noVehicle[run.id] = kind
|
|
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
|
|
end
|
|
|
|
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
|
|
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
|
|
local function flowTitle(data, fillTypes)
|
|
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
|
|
if #fillTypes == 1 then return first end
|
|
return string.format("%s +%d", tostring(first), #fillTypes - 1)
|
|
end
|
|
|
|
local function startWithPair(adEnv, run, runId, pair, used)
|
|
used[pair.tractor.id], used[pair.implement.id] = true, true
|
|
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
|
|
run = ADRuns.getRun(ADRunsController.store, runId)
|
|
local wagons = ADRuns.copyWagons(run.wagons)
|
|
wagons[pair.tractor.id] = pair.implement.id
|
|
run.wagons = wagons
|
|
ADRunsController.resolveMarkers(adEnv, run)
|
|
local results = ADRunsController.startRun(adEnv, runId)
|
|
local started = 0
|
|
for _, result in ipairs(results or {}) do if result.ok then started = started + 1 end end
|
|
log("flow '%s' oprettet med '%s' + vogn '%s' (%d startet)", tostring(run.name), tostring(pair.tractor.name),
|
|
tostring(pair.implement.name), started)
|
|
end
|
|
|
|
local function createGroupFlow(adEnv, data, group, fillTypes, pool, used)
|
|
local title = flowTitle(data, fillTypes)
|
|
local created = ADRunsController.createRun(ADChain.flowName(title, group.consumerName or group.consumer), "supply")
|
|
local runId = created.id
|
|
ADRunsController.updateRun(runId, {targetBuildingIds = {group.consumer}, ingredientMode = "manual", ingredients = fillTypes,
|
|
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, group.consumer), chain = group.rootKey})
|
|
-- updateRun erstatter store'en: slå altid run'et op igen
|
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
local fillIndex = data.fillIndex[fillTypes[1]]
|
|
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
|
|
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
|
|
return
|
|
end
|
|
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
|
|
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
|
|
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
|
|
if pair == nil then
|
|
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
|
|
return
|
|
end
|
|
startWithPair(adEnv, run, runId, pair, used)
|
|
end
|
|
|
|
-- Nye varer til en bygning, der allerede har et kædeflow: lægges i det flow.
|
|
local function extendFlow(runId, fillTypes)
|
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
if run == nil then return false end
|
|
local ingredients = {}
|
|
for _, name in ipairs(run.ingredients or {}) do table.insert(ingredients, name) end
|
|
local added = {}
|
|
for _, name in ipairs(fillTypes) do
|
|
local present = false
|
|
for _, have in ipairs(ingredients) do if have == name then present = true end end
|
|
if not present then table.insert(ingredients, name); table.insert(added, name) end
|
|
end
|
|
if #added == 0 then return false end
|
|
ADRunsController.updateRun(runId, {ingredients = ingredients})
|
|
log("flow '%s' får også %s", tostring(run.name), table.concat(added, ", "))
|
|
return true
|
|
end
|
|
|
|
-- Tidligere kædeflows (ét pr. vare) til samme bygning uden traktorer lægges sammen i ét.
|
|
function ADChainRun.mergeIdleFlows(adEnv)
|
|
local merged = 0
|
|
for _, merge in ipairs(ADChainActions.mergeIdle(ADChain.runsView(ADRunsController.store.runs))) do
|
|
if #merge.add > 0 then extendFlow(merge.keepId, merge.add) end
|
|
for _, id in ipairs(merge.deleteIds) do
|
|
local ok, err = pcall(ADRunsController.deleteRun, id, adEnv)
|
|
if ok then
|
|
ADChain.noVehicle[id] = nil
|
|
merged = merged + 1
|
|
else
|
|
warn("kædeflow %s kunne ikke lægges sammen: %s", tostring(id), tostring(err))
|
|
end
|
|
end
|
|
end
|
|
if merged > 0 then log("%d tomme kædeflows lagt sammen (ét flow pr. bygning)", merged) end
|
|
return merged
|
|
end
|
|
|
|
function ADChainRun.createFlows(adEnv, data, edges)
|
|
local runs = ADChain.runsView(ADRunsController.store.runs)
|
|
local missing = ADChainActions.missingFlows(edges, runs)
|
|
local pool, used, count = {}, {}, 0
|
|
for _, group in ipairs(ADChainActions.groupMissing(missing)) do
|
|
if count >= ADChain.MAX_NEW_FLOWS then break end
|
|
local bulk, pallets = {}, {}
|
|
for _, name in ipairs(group.fillTypes) do
|
|
local index = data.fillIndex[name]
|
|
table.insert((index ~= nil and ADSources.isPalletFillType(index)) and pallets or bulk, name)
|
|
end
|
|
local ok, err = pcall(function()
|
|
if #bulk > 0 then
|
|
local existing = ADChainActions.chainRunFor(runs, group.consumer)
|
|
if existing ~= nil then
|
|
if extendFlow(existing, bulk) then count = count + 1 end
|
|
else
|
|
createGroupFlow(adEnv, data, group, bulk, pool, used)
|
|
count = count + 1
|
|
end
|
|
end
|
|
for _, name in ipairs(pallets) do
|
|
createGroupFlow(adEnv, data, group, {name}, pool, used)
|
|
count = count + 1
|
|
end
|
|
end)
|
|
if not ok then warn("flow til %s kunne ikke oprettes: %s", tostring(group.consumerName), tostring(err)) end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- Slutprodukter: sælg straks (fabrikkens udgang -> "Sælg direkte"), medmindre kæden står på "Behold". Varer kæden
|
|
-- selv satte til salg, sættes tilbage til "Behold", når spilleren vælger det.
|
|
local function outputMode(point, fillIndex)
|
|
if point.getOutputDistributionMode == nil then return nil end
|
|
local ok, mode = pcall(point.getOutputDistributionMode, point, fillIndex)
|
|
return ok and mode or nil
|
|
end
|
|
|
|
function ADChainRun.applyFinalSales(data, chain, edges)
|
|
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
|
local state, changed = ADChain.state, 0
|
|
local wanted = {}
|
|
local uses = ADChainActions.farmUses(data, ADChain.runsView(ADRunsController.store.runs))
|
|
for _, sale in ipairs(ADChainActions.finalSales(chain, data.recipes, edges, state.finalKeep, uses)) do
|
|
local id = sale.key .. "|" .. sale.fillType
|
|
wanted[id] = true
|
|
local entry, fillIndex = (data.points or {})[sale.key], data.fillIndex[sale.fillType]
|
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
|
and outputMode(entry.point, fillIndex) ~= ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.DIRECT_SELL)
|
|
state.soldByChain[id] = true
|
|
changed = changed + 1
|
|
log("slutprodukt %s i %s sælges straks", tostring(sale.fillType), tostring(entry.buildingName or sale.key))
|
|
end
|
|
end
|
|
for id in pairs(state.soldByChain) do
|
|
local key, fillType = id:match("^(.*)|([^|]+)$")
|
|
-- sat til salg af kæden, men ikke længere ønsket (Behold, gården bruger varen, kæden væk): tilbage til Behold
|
|
if key ~= nil and not wanted[id] then
|
|
local entry, fillIndex = (data.points or {})[key], data.fillIndex[fillType]
|
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil
|
|
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
|
changed = changed + 1
|
|
log("slutprodukt %s beholdes igen", tostring(fillType))
|
|
end
|
|
state.soldByChain[id] = nil
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
|
|
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
|
|
function ADChainRun.keepIntermediates(data, edges)
|
|
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
|
local changed = 0
|
|
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
|
|
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
|
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
|
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
|
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
|
|
changed = changed + 1
|
|
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
|
|
tostring(entry.buildingName or item.key))
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
|
|
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
|
|
function ADChainRun.stopFlows(adEnv, chain, edges)
|
|
local changed = 0
|
|
local active = ADChain.activeRoots(chain, ADChain.state.paused)
|
|
for _, run in ipairs(ADChainActions.flowsToStop(ADChain.runsView(ADRunsController.store.runs), active)) do
|
|
local rootKey = ADChain.stillNeeded(run, edges)
|
|
if rootKey ~= nil then
|
|
ADRunsController.updateRun(run.id, {chain = rootKey})
|
|
log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey))
|
|
changed = changed + 1
|
|
elseif ADChainActions.releaseAction(run) == "delete" then
|
|
local ok, err = pcall(ADRunsController.deleteRun, run.id, adEnv)
|
|
if ok then
|
|
ADChain.noVehicle[run.id] = nil
|
|
log("tomt kædeflow '%s' slettet: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
|
changed = changed + 1
|
|
else
|
|
warn("flow '%s' kunne ikke slettes: %s", tostring(run.name), tostring(err))
|
|
end
|
|
else
|
|
local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id)
|
|
if ok then
|
|
ADRunsController.updateRun(run.id, {chain = ""})
|
|
log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
|
changed = changed + 1
|
|
else
|
|
warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err))
|
|
end
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
-- Hele planen; hver del for sig i pcall.
|
|
function ADChainRun.apply(adEnv, data, result)
|
|
local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions)
|
|
if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end
|
|
local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod)
|
|
if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end
|
|
local okMerge, mergedCount = pcall(ADChainRun.mergeIdleFlows, adEnv)
|
|
if not okMerge then warn("sammenlægning af kædeflows fejlede: %s", tostring(mergedCount)) end
|
|
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
|
|
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
|
|
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
|
|
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
|
|
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
|
|
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
|
|
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
|
|
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
|
|
local okStop, stopped = true, 0
|
|
if not data.partial then
|
|
okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges)
|
|
if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end
|
|
end
|
|
if (okNew and created > 0) or (okStop and stopped > 0) then
|
|
local okSave, errSave = pcall(ADRunsController.save)
|
|
if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end
|
|
end
|
|
end
|