Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFlowGuide.lua
T

459 lines
20 KiB
Lua

-- AD Flow Guide
-- "Opsæt flow": flowguidens trin oven på ADGuide (ren Lua, ingen spil-API) — samme trin-skærm og knapper
-- som markguiden. Kladden er en kopi af flowets felter, så intet ændres før [Gem]/[Start flow]. Det
-- spil-afhængige kommer i `context` (GUI'et henter det ud fra kladden, se previewRun):
-- {buildings = {{id, name, label, hasMarker, text}} -- kandidater til Fra (udkørsel) / Til (forsyning)
-- fillTypes = {{name, title}} -- varer målet tager (forsyning, Vælg selv)
-- products = {{name, title}} -- varer kilderne har (udkørsel)
-- storeBuildings = {{id, name, text}} -- lagre der kan tage flowets lagervarer
-- waitPools = {{group, count}}
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked}}} -- label = kort navn (resumé)
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
ADFlowGuide = {}
ADFlowGuide.ORDER = {"name", "direction", "buildings", "goods", "store", "rules", "wait", "tractors", "summary"}
ADFlowGuide.POLICIES = {"off", "store", "sell", "sellElseStore"}
ADFlowGuide.MAX_NAMED_TRACTORS = 3
ADFlowGuide.MAX_NAMED_GOODS = 3
local function L(key, args) return {key = key, args = args} end
local function T(text) return {text = text} end
local function isOut(guide) return guide.draft.kind == "outbound" end
local function hasStorePolicy(draft)
for _, product in ipairs(draft.products or {}) do
if product.policy == "store" or product.policy == "sellElseStore" then return true end
end
return false
end
local VISIBLE = {
name = function(guide) return guide.draft.isNew == true end,
store = function(guide) return isOut(guide) and hasStorePolicy(guide.draft) end,
}
function ADFlowGuide.isVisible(step, guide)
local rule = VISIBLE[step]
if rule ~= nil then return rule(guide) == true end
for _, id in ipairs(ADFlowGuide.ORDER) do
if id == step then return true end
end
return false
end
ADFlowGuide.DEF = {order = ADFlowGuide.ORDER, isVisible = ADFlowGuide.isVisible}
-- Overskrift pr. trin; "Fra"/"Til" følger retningen.
function ADFlowGuide.stepKey(step, guide)
if step == "buildings" then
return (guide ~= nil and isOut(guide)) and "spu_guide_flow_step_from" or "spu_guide_flow_step_to"
end
return "spu_guide_flow_step_" .. tostring(step)
end
local KIND_HINTS = {buildings = true, goods = true, rules = true}
function ADFlowGuide.hintKey(step, guide)
if KIND_HINTS[step] then
return "spu_guide_flow_hint_" .. step .. ((guide ~= nil and isOut(guide)) and "_out" or "_in")
end
return "spu_guide_flow_hint_" .. tostring(step)
end
-- ---- kladde ----
local RUN_FIELDS = {
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
"returnBelowPercent", "waitPoolGroup", "vehicleIds",
}
local function draftFrom(run, isNew)
local draft = {}
for _, key in ipairs(RUN_FIELDS) do draft[key] = ADGuide.deepCopy(run[key]) end
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products", "ingredients", "vehicleIds"}) do
draft[key] = draft[key] or {}
end
draft.kind = draft.kind == "outbound" and "outbound" or "supply"
draft.ingredientMode = draft.ingredientMode == "manual" and "manual" or "auto"
draft.sellAtPercent = draft.sellAtPercent or ADRuns.DEFAULT_SELL_AT_PERCENT
draft.storeAbovePercent = draft.storeAbovePercent or ADRuns.DEFAULT_STORE_ABOVE_PERCENT
draft.sellAbovePercent = draft.sellAbovePercent or ADRuns.DEFAULT_SELL_ABOVE_PERCENT
draft.returnBelowPercent = draft.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT
draft.loops = draft.loops or 0
draft.waitPoolGroup = draft.waitPoolGroup or ""
draft.collectRound = draft.collectRound == true
draft.isNew = isNew == true
draft.showAllBuildings = false
draft.storeAuto = #draft.storeBuildingIds == 0
return draft
end
function ADFlowGuide.open(run, context, options)
return ADGuide.new(ADFlowGuide.DEF, draftFrom(run, false), context, options)
end
-- Nyt flow: intet oprettes før [Gem]; kladden har standardværdierne fra ADRuns.createRun.
function ADFlowGuide.openNew(name, context)
local _, run = ADRuns.createRun(ADRuns.newStore(), name, "supply")
local draft = draftFrom(run, true)
draft.id = nil
return ADGuide.new(ADFlowGuide.DEF, draft, context)
end
-- Run-formet tabel til controllerens opslag (varer, vogne) før noget er gemt: markører fra de valgte
-- bygninger. markerIds(buildingId, role) -> {wayPointId, ...}.
function ADFlowGuide.previewRun(draft, markerIds)
local run = ADGuide.deepCopy(draft)
local function resolve(ids, role)
local result, seen = {}, {}
for _, buildingId in ipairs(ids or {}) do
for _, wayPointId in ipairs(markerIds(buildingId, role) or {}) do
if not seen[wayPointId] then
seen[wayPointId] = true
table.insert(result, wayPointId)
end
end
end
return result
end
run.sourceWayPointIds = resolve(draft.sourceBuildingIds, "load")
run.targetWayPointIds = resolve(draft.targetBuildingIds, "unload")
run.targetWayPointId = run.targetWayPointIds[1]
return run
end
-- ---- valg ----
local SET_VALUES = {
kind = true, ingredientMode = true, sellAtPercent = true, storeAbovePercent = true, sellAbovePercent = true, loops = true,
returnBelowPercent = true, waitPoolGroup = true,
}
local BOOL_VALUES = {showAllBuildings = true, storeAuto = true, collectRound = true}
local LIST_VALUES = {ingredients = true, storeBuildingIds = true, vehicleIds = true}
local function setPolicy(products, name, policy)
local result = {}
for _, product in ipairs(products or {}) do
if product.fillType ~= name then table.insert(result, {fillType = product.fillType, policy = product.policy}) end
end
if policy ~= "off" then table.insert(result, {fillType = name, policy = policy}) end
return result
end
function ADFlowGuide.choose(guide, id, value)
local draft = ADGuide.deepCopy(guide.draft)
if SET_VALUES[id] then
draft[id] = value
elseif BOOL_VALUES[id] then
draft[id] = value == true
elseif LIST_VALUES[id] then
draft[id] = ADGuide.toggleItem(draft[id], value.item, value.on)
elseif id == "buildings" then
local key = draft.kind == "outbound" and "sourceBuildingIds" or "targetBuildingIds"
draft[key] = ADGuide.toggleItem(draft[key], value.item, value.on)
elseif id == "policy" then
draft.products = setPolicy(draft.products, value.item, value.value)
elseif id == "name" then
if type(value) ~= "string" or not value:match("%S") then return guide end
draft.name = value
else
return guide
end
return ADGuide.withDraft(guide, draft)
end
-- ---- rækker ----
local function choice(id, title, options, value, item)
return {type = "choice", id = id, title = title, options = options, value = value, item = item}
end
local function toggle(id, title, value)
return {type = "toggle", id = id, title = title, off = L("ui_no"), on = L("ui_yes"), value = value == true}
end
local function check(id, item, text, value)
return {type = "check", id = id, item = item, title = T(text), value = value == true}
end
local function text(key, args) return {type = "text", label = L(key, args)} end
local function percentOptions(from, to, step)
local options = {}
for percent = from, to, step do table.insert(options, {label = T(string.format("%d %%", percent)), value = percent}) end
return options
end
-- "Sælg altid over": Fra, 50-100 %
local function sellAboveOptions()
local options = {{label = L("spu_sellAbove_off"), value = 0}}
for _, option in ipairs(percentOptions(50, ADRuns.MAX_SELL_ABOVE_PERCENT, 5)) do table.insert(options, option) end
return options
end
local function chosenSet(list)
local set = {}
for _, value in ipairs(list or {}) do set[value] = true end
return set
end
local function buildingIds(draft)
return draft.kind == "outbound" and draft.sourceBuildingIds or draft.targetBuildingIds
end
local function rowsName(guide)
return {
{type = "text", label = T(guide.draft.name or "")},
{type = "link", id = "rename", value = true, label = L("spu_guide_flow_rename")},
}
end
local function rowsDirection(guide)
return {choice("kind", L("spu_direction"), {
{label = L("spu_direction_in"), value = "supply"}, {label = L("spu_direction_out"), value = "outbound"}}, guide.draft.kind)}
end
-- Bygninger i fast rækkefølge (navn, markør); uden markør kun med "Vis alle" (eller når valgt).
local function rowsBuildings(guide)
local draft, rows = guide.draft, {}
local chosen = chosenSet(buildingIds(draft))
local candidates = guide.context.buildings or {}
local shown = ADFlowView.buildingRows(candidates, chosen, draft.showAllBuildings)
local hidden = ADFlowView.hiddenCount(candidates, shown)
if hidden > 0 or draft.showAllBuildings then
table.insert(rows, toggle("showAllBuildings", L("spu_showAllBuildings", {hidden}), draft.showAllBuildings))
end
for _, building in ipairs(shown) do
table.insert(rows, check("buildings", building.id, building.text or building.name, chosen[building.id]))
end
if #shown == 0 then table.insert(rows, text("spu_noBuildings")) end
return rows
end
local function rowsGoods(guide)
local draft, context, rows = guide.draft, guide.context, {}
if isOut(guide) then
local products = context.products or {}
if #products == 0 then return {text("spu_guide_flow_noProducts")} end
local options = {}
for _, policy in ipairs(ADFlowGuide.POLICIES) do table.insert(options, {label = L("spu_policy_" .. policy), value = policy}) end
for _, product in ipairs(products) do
table.insert(rows, choice("policy", T(product.text or product.title), options, ADRuns.getProductPolicy(draft, product.name), product.name))
end
return rows
end
table.insert(rows, choice("ingredientMode", L("spu_guide_flow_goods"), {
{label = L("spu_guide_flow_goodsAutoChoice"), value = "auto"}, {label = L("spu_guide_manual"), value = "manual"}}, draft.ingredientMode))
if draft.ingredientMode ~= "manual" then
table.insert(rows, text("spu_guide_flow_goodsAutoText"))
return rows
end
local chosen = chosenSet(draft.ingredients)
for _, fillType in ipairs(context.fillTypes or {}) do
table.insert(rows, check("ingredients", fillType.name, " " .. tostring(fillType.title), chosen[fillType.name]))
end
if #(context.fillTypes or {}) == 0 then table.insert(rows, text("spu_guide_flow_noGoodsTo")) end
return rows
end
local function rowsStore(guide)
local draft, rows = guide.draft, {}
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
{label = L("spu_guide_flow_storeAutoChoice"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeAuto))
if draft.storeAuto then
table.insert(rows, text("spu_guide_flow_storeAutoText"))
return rows
end
local chosen = chosenSet(draft.storeBuildingIds)
for _, building in ipairs(guide.context.storeBuildings or {}) do
table.insert(rows, check("storeBuildingIds", building.id, building.text, chosen[building.id]))
end
if #(guide.context.storeBuildings or {}) == 0 then table.insert(rows, text("spu_noBuildings")) end
return rows
end
local function rowsRules(guide)
local draft = guide.draft
if isOut(guide) then
return {
choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent),
choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent),
choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent),
toggle("collectRound", L("spu_collectRound"), draft.collectRound),
}
end
local loops = {{label = L("spu_loops_infinite"), value = 0}}
for count = 1, ADRuns.MAX_LOOPS do table.insert(loops, {label = T(tostring(count)), value = count}) end
return {
choice("loops", L("spu_loops"), loops, draft.loops),
choice("returnBelowPercent", L("spu_returnBelow"), percentOptions(ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10), draft.returnBelowPercent),
}
end
local function rowsWait(guide)
local options = {{label = L("spu_waitPool_all"), value = ""}}
for _, pool in ipairs(guide.context.waitPools or {}) do
table.insert(options, {label = T(string.format("%s (%d)", tostring(pool.group), pool.count or 0)), value = pool.group})
end
return {choice("waitPoolGroup", L("spu_waitPool"), options, guide.draft.waitPoolGroup or "")}
end
-- Traktorer: flowets egne først, så de ledige. Traktorer i andre flows og blokerede (vognen passer ikke)
-- skjules med en linje hver.
local function rowsTractors(guide)
local draft, rows = guide.draft, {}
local inRun = chosenSet(draft.vehicleIds)
local entries = {}
local other, blocked = 0, 0
for _, vehicle in ipairs(guide.context.vehicles or {}) do
local state = "free"
if inRun[vehicle.id] then
state = "in"
elseif vehicle.otherRunId ~= nil and vehicle.otherRunId ~= draft.id then
state = "other"
other = other + 1
elseif vehicle.blocked then
state = "blocked"
blocked = blocked + 1
end
table.insert(entries, {id = vehicle.id, label = tostring(vehicle.text), state = state})
end
for _, entry in ipairs(ADFlowView.vehicleRows(entries, false)) do
table.insert(rows, check("vehicleIds", entry.id, entry.label, entry.state == "in"))
end
if #rows == 0 then table.insert(rows, text("spu_guide_flow_noTractors")) end
if other > 0 then table.insert(rows, text("spu_guide_flow_otherFlows", {other})) end
if blocked > 0 then table.insert(rows, text("spu_guide_flow_blocked", {blocked})) end
return rows
end
local function rowsSummary(guide)
local rows = {}
for _, line in ipairs(ADFlowGuide.summary(guide)) do
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
end
return rows
end
local ROWS = {
name = rowsName, direction = rowsDirection, buildings = rowsBuildings, goods = rowsGoods, store = rowsStore,
rules = rowsRules, wait = rowsWait, tractors = rowsTractors, summary = rowsSummary,
}
function ADFlowGuide.rows(guide)
local builder = ROWS[guide.step]
return builder ~= nil and builder(guide) or {}
end
-- ---- resumé ----
-- Korte navne til resuméet (field, ellers navn, ellers rækkens tekst).
local function namesFor(ids, entries, field)
local byId = {}
for _, entry in ipairs(entries or {}) do byId[entry.id or entry.name] = entry[field] or entry.name or entry.text end
local names = {}
for _, id in ipairs(ids or {}) do table.insert(names, tostring(byId[id] or id)) end
return names
end
local SUMMARY = {
name = function(guide) return {T(guide.draft.name or "")} end,
direction = function(guide) return {L(isOut(guide) and "spu_direction_out" or "spu_direction_in")} end,
buildings = function(guide)
local names = namesFor(buildingIds(guide.draft), guide.context.buildings, "name")
if #names == 0 then return {L("spu_guide_flow_none")} end
return {T(ADFlowView.joinCounted(names, " + ", ""))}
end,
goods = function(guide)
local draft = guide.draft
if isOut(guide) then
local parts = {}
local titles = {}
for _, product in ipairs(guide.context.products or {}) do titles[product.name] = product.title end
for _, product in ipairs(draft.products or {}) do
table.insert(parts, {key = "spu_policy_" .. product.policy, prefix = tostring(titles[product.fillType] or product.fillType) .. ": "})
end
if #parts == 0 then return {L("spu_guide_flow_none")} end
return parts
end
if draft.ingredientMode ~= "manual" then return {L("spu_guide_flow_goodsAuto")} end
if #draft.ingredients == 0 then return {L("spu_guide_flow_none")} end
if #draft.ingredients > ADFlowGuide.MAX_NAMED_GOODS then return {L("spu_guide_flow_sumGoods", {#draft.ingredients})} end
local byName = {}
for _, fillType in ipairs(guide.context.fillTypes or {}) do byName[fillType.name] = fillType.title end
local names = {}
for _, name in ipairs(draft.ingredients) do table.insert(names, tostring(byName[name] or name)) end
return {T(table.concat(names, " + "))}
end,
store = function(guide)
local draft = guide.draft
if draft.storeAuto or #draft.storeBuildingIds == 0 then return {L("spu_guide_sum_auto")} end
if #draft.storeBuildingIds > 2 then return {L("spu_guide_sum_stores", {#draft.storeBuildingIds})} end
return {T(table.concat(namesFor(draft.storeBuildingIds, guide.context.storeBuildings, "name"), " + "))}
end,
rules = function(guide)
local draft = guide.draft
if isOut(guide) then
local parts = {L("spu_guide_flow_sumSellAt", {draft.sellAtPercent}), L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})}
if (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
return parts
end
local loops = (draft.loops or 0) > 0 and L("spu_guide_flow_sumLoops", {draft.loops}) or L("spu_guide_flow_sumLoopsInf")
return {loops, L("spu_guide_flow_sumReturn", {draft.returnBelowPercent})}
end,
wait = function(guide)
local group = guide.draft.waitPoolGroup or ""
return {group == "" and L("spu_waitPool_all") or T(group)}
end,
tractors = function(guide)
local ids = guide.draft.vehicleIds
if #ids == 0 then return {L("spu_guide_flow_sumNoTractors")} end
if #ids > ADFlowGuide.MAX_NAMED_TRACTORS then return {L("spu_guide_flow_sumTractors", {#ids})} end
return {T(table.concat(namesFor(ids, guide.context.vehicles, "label"), ", "))}
end,
}
-- Resumé-linjer {step, title, parts}: Navn (altid) + de synlige trin.
function ADFlowGuide.summary(guide)
local lines = {}
for _, step in ipairs(ADFlowGuide.ORDER) do
local build = SUMMARY[step]
if build ~= nil and (step == "name" or ADFlowGuide.isVisible(step, guide)) then
table.insert(lines, {step = step, title = L(ADFlowGuide.stepKey(step, guide)), parts = build(guide)})
end
end
return lines
end
-- ---- gem ----
function ADFlowGuide.result(guide)
local draft = guide.draft
local storeIds = ADGuide.deepCopy(draft.storeBuildingIds)
if ADFlowGuide.isVisible("store", guide) and draft.storeAuto then storeIds = {} end
local changes = {
kind = draft.kind, sourceBuildingIds = ADGuide.deepCopy(draft.sourceBuildingIds),
targetBuildingIds = ADGuide.deepCopy(draft.targetBuildingIds), storeBuildingIds = storeIds,
products = ADGuide.deepCopy(draft.products), ingredientMode = draft.ingredientMode,
ingredients = ADGuide.deepCopy(draft.ingredients), sellAtPercent = draft.sellAtPercent,
storeAbovePercent = draft.storeAbovePercent, sellAbovePercent = draft.sellAbovePercent or 0,
collectRound = draft.collectRound, loops = draft.loops,
returnBelowPercent = draft.returnBelowPercent, waitPoolGroup = draft.waitPoolGroup or "",
}
if draft.name ~= nil and draft.name:match("%S") then changes.name = draft.name end
return {isNew = draft.isNew == true, runId = draft.id, name = draft.name, kind = draft.kind, changes = changes,
vehicleIds = ADGuide.deepCopy(draft.vehicleIds)}
end
-- Traktorer der skal tildeles/fjernes efter gem.
function ADFlowGuide.vehicleChanges(oldIds, newIds)
local old, new = chosenSet(oldIds), chosenSet(newIds)
local added, removed = {}, {}
for _, id in ipairs(newIds or {}) do if not old[id] then table.insert(added, id) end end
for _, id in ipairs(oldIds or {}) do if not new[id] then table.insert(removed, id) end end
return added, removed
end