feat: flow-guide tildeler ledige vogne til solo-traktorer

Solo-traktorer (ingen vogn bagpå) er valgbare når en frakoblet vogn med
Connect-markør kan bære flowets varer. Guiden foreslår en vogn (navnematch,
så Disponibel) med vognvalg pr. traktor; flowet husker parret ved gem, så
vognens parkering ikke skal omdøbes.
This commit is contained in:
2026-09-29 20:48:04 +02:00
parent 87b030d492
commit 1739b01d92
8 changed files with 232 additions and 8 deletions
+78 -3
View File
@@ -7,7 +7,10 @@
-- 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é)
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked, solo}} -- label = kort navn (resumé)
-- wagons = {{id, label}}} -- ledige vogne til solo-traktorer (bedste først)
-- Solo-traktor (ingen vogn bagpå) får en ledig vogn fra context.wagons (draft.wagons = {[traktorId] = vognId}),
-- som den henter ved "Connect <vognens parkering>" — uden at vognens parkering skal omdøbes.
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
ADFlowGuide = {}
@@ -66,7 +69,7 @@ end
local RUN_FIELDS = {
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
"returnBelowPercent", "waitPoolGroup", "vehicleIds",
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
}
local function draftFrom(run, isNew)
@@ -83,6 +86,7 @@ local function draftFrom(run, isNew)
draft.returnBelowPercent = draft.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT
draft.loops = draft.loops or 0
draft.waitPoolGroup = draft.waitPoolGroup or ""
draft.wagons = draft.wagons or {}
draft.collectRound = draft.collectRound == true
draft.isNew = isNew == true
draft.showAllBuildings = false
@@ -124,6 +128,44 @@ function ADFlowGuide.previewRun(draft, markerIds)
return run
end
-- ---- vogne til solo-traktorer ----
local function soloIds(context)
local set = {}
for _, vehicle in ipairs((context or {}).vehicles or {}) do
if vehicle.solo then set[vehicle.id] = true end
end
return set
end
-- Ny vogn-tabel: fravalgte traktorer slipper deres vogn; valgte solo-traktorer uden (gyldig) vogn får den første
-- ledige i context.wagons, som ingen anden traktor i flowet har. Vogne til ikke-solo-traktorer bevares (husket).
function ADFlowGuide.assignWagons(draft, context)
local chosen, solo, offered = {}, soloIds(context), {}
for _, id in ipairs(draft.vehicleIds or {}) do chosen[id] = true end
for _, wagon in ipairs((context or {}).wagons or {}) do offered[wagon.id] = true end
local wagons, used = {}, {}
for tractorId, wagonId in pairs(draft.wagons or {}) do
local valid = chosen[tractorId] and (not solo[tractorId] or offered[wagonId])
if valid and not used[wagonId] then
wagons[tractorId] = wagonId
used[wagonId] = true
end
end
for _, tractorId in ipairs(draft.vehicleIds or {}) do
if solo[tractorId] and wagons[tractorId] == nil then
for _, wagon in ipairs((context or {}).wagons or {}) do
if not used[wagon.id] then
wagons[tractorId] = wagon.id
used[wagon.id] = true
break
end
end
end
end
return wagons
end
-- ---- valg ----
local SET_VALUES = {
@@ -157,6 +199,10 @@ function ADFlowGuide.choose(guide, id, value)
draft[id] = value == true
elseif LIST_VALUES[id] then
draft[id] = ADGuide.toggleItem(draft[id], value.item, value.on)
if id == "vehicleIds" then draft.wagons = ADFlowGuide.assignWagons(draft, guide.context) end
elseif id == "wagon" then
draft.wagons = ADGuide.deepCopy(draft.wagons or {})
draft.wagons[value.item] = value.value
elseif id == "buildings" then
local key = draft.kind == "outbound" and "sourceBuildingIds" or "targetBuildingIds"
draft[key] = ADGuide.toggleItem(draft[key], value.item, value.on)
@@ -307,6 +353,21 @@ local function rowsWait(guide)
return {choice("waitPoolGroup", L("spu_waitPool"), options, guide.draft.waitPoolGroup or "")}
end
-- Vognvalg under en valgt solo-traktor: dens egen vogn + ledige vogne ingen anden traktor i flowet har.
local function rowWagon(guide, tractorId)
local wagons = guide.draft.wagons or {}
local usedByOthers = {}
for otherId, wagonId in pairs(wagons) do
if otherId ~= tractorId then usedByOthers[wagonId] = true end
end
local options = {}
for _, wagon in ipairs(guide.context.wagons or {}) do
if not usedByOthers[wagon.id] then table.insert(options, {label = T(tostring(wagon.label)), value = wagon.id}) end
end
if #options == 0 or wagons[tractorId] == nil then return text("spu_guide_flow_noFreeWagon") end
return choice("wagon", L("spu_guide_flow_wagon"), options, wagons[tractorId], tractorId)
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)
@@ -329,8 +390,13 @@ local function rowsTractors(guide)
end
table.insert(entries, {id = vehicle.id, label = tostring(vehicle.text), state = state})
end
local solo = soloIds(guide.context)
for _, entry in ipairs(ADFlowView.vehicleRows(entries, false)) do
table.insert(rows, check("vehicleIds", entry.id, entry.label, entry.state == "in"))
if entry.state == "in" and solo[entry.id] then
local wagonRow = rowWagon(guide, entry.id)
if wagonRow ~= nil then table.insert(rows, wagonRow) end
end
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
@@ -468,7 +534,16 @@ function ADFlowGuide.result(guide)
}
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)}
vehicleIds = ADGuide.deepCopy(draft.vehicleIds), wagons = ADFlowGuide.resultWagons(draft)}
end
-- Vogne for de valgte traktorer (til run.wagons ved gem).
function ADFlowGuide.resultWagons(draft)
local chosen, wagons = chosenSet(draft.vehicleIds), {}
for tractorId, wagonId in pairs(draft.wagons or {}) do
if chosen[tractorId] then wagons[tractorId] = wagonId end
end
return wagons
end
-- Traktorer der skal tildeles/fjernes efter gem.
+20
View File
@@ -83,6 +83,26 @@ function ADFlowWagons.guessWagon(runName, tractorNumber, candidates, taken)
return matching[1] ~= nil and matching[1].id or nil
end
-- Ledige vogne til flow-guiden, bedste først: parkering med flowets navn, så "Disponibel", så resten (alfabetisk).
-- candidates = {{id, label}} -> ny sorteret liste.
function ADFlowWagons.rankWagons(runName, candidates)
local wanted = tostring(runName or ""):lower()
local function rank(wagon)
local label = tostring(wagon.label or ""):lower()
if wanted ~= "" and label:find(wanted, 1, true) ~= nil then return 1 end
if label:find("disponibel", 1, true) ~= nil then return 2 end
return 3
end
local ranked = {}
for _, wagon in ipairs(candidates or {}) do table.insert(ranked, wagon) end
table.sort(ranked, function(a, b)
local ra, rb = rank(a), rank(b)
if ra ~= rb then return ra < rb end
return tostring(a.label) < tostring(b.label)
end)
return ranked
end
function ADFlowWagons.ownerOf(runs, wagonId)
for _, run in ipairs(runs or {}) do
for _, id in pairs(run.wagons or {}) do
@@ -108,15 +108,76 @@ function SmartPickupFrame:flowStoreContext(adEnv, preview, farmId)
return result
end
function SmartPickupFrame:flowVehicleContext(adEnv, preview)
-- Varer (fillType-indeks) flowet kører med: udkørsel = valgte produkter, forsyning = valgte/målets varer.
-- Tom liste = ukendt endnu (ingen mål valgt) -> enhver vogn med last-enheder tæller.
local function flowFillTypes(adEnv, preview)
local names = {}
if preview.kind == "outbound" then
for _, product in ipairs(preview.products or {}) do table.insert(names, product.fillType) end
elseif preview.ingredientMode == "manual" then
names = preview.ingredients or {}
else
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do table.insert(names, fillType.name) end
end
local result = {}
for _, name in ipairs(names) do
local index = g_fillTypeManager:getFillTypeIndexByName(tostring(name))
if index ~= nil then table.insert(result, index) end
end
return result
end
local function carriesAny(carriable, wanted)
if #carriable == 0 then return false end
if #wanted == 0 then return true end
for _, a in ipairs(carriable) do
for _, b in ipairs(wanted) do if a == b then return true end end
end
return false
end
-- Ledige vogne til solo-traktorer: frakoblet, egen AD-parkering med "Connect"-markør, ikke holdt af et andet flow
-- og kan bære flowets varer. Bedste først (navnematch, så "Disponibel"). In-game 29/9: flowet kunne ikke oprettes
-- uden at omdøbe en vogns parkering til flowets navn.
function SmartPickupFrame:flowWagonContext(adEnv, preview)
local farmId = g_currentMission:getFarmId()
local markers = adEnv.ADGraphManager:getMapMarkers()
local wanted = flowFillTypes(adEnv, preview)
local candidates = {}
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
local ok, entry = pcall(function()
local index = any.advd ~= nil and any.advd.parkDestination or nil
if any.spec_motorized ~= nil or any.spec_attachable == nil or type(index) ~= "number" or index < 1 then return nil end
if any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil then return nil end
if any.getOwnerFarmId == nil or any:getOwnerFarmId() ~= farmId then return nil end
local id = ADRunsController.getVehicleId(any)
local park = adEnv.ADGraphManager:getMapMarkerById(index)
if id == nil or park == nil or ADPool.connectFor(park.name, markers) == nil then return nil end
local owner = ADFlowWagons.ownerOf(ADRunsController.store.runs, id)
if owner ~= nil and owner.id ~= preview.id then return nil end
if not carriesAny(ADRunsController.getCarriableFillTypes(adEnv, any), wanted) then return nil end
return {id = id, label = tostring(park.name)}
end)
if ok and entry ~= nil then table.insert(candidates, entry) end
end
return ADFlowWagons.rankWagons(preview.name, candidates)
end
function SmartPickupFrame:flowVehicleContext(adEnv, preview, wagons)
local result = {}
local hasWagons = wagons ~= nil and #wagons > 0
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local ok, entry = pcall(function()
local text = self:describeVehicle(adEnv, preview, vehicle, false)
local validation = ADRunsController.validateVehicle(adEnv, preview, vehicle)
local other = ADRunsController.getRunForVehicle(vehicle)
return {id = ADRunsController.getVehicleId(vehicle), text = text, label = ADRunsController.getVehicleLabel(adEnv, vehicle), otherRunId = other ~= nil and other.id or nil,
otherRunName = other ~= nil and other.name or nil, blocked = SmartPickupFrame.VEHICLE_BLOCKING_REASONS[validation.reason] == true}
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
-- solo-traktor (ingen vogn bagpå, kan trække): valgbar når en ledig vogn findes — den hentes ved Connect
local solo = validation.reason == "noTrailer" and ADPool.canPull(ADPool.ownRole(vehicle))
if solo and hasWagons then text = string.format(self.i18n:getText("spu_guide_flow_soloText"), tostring(label)) end
local blocked = SmartPickupFrame.VEHICLE_BLOCKING_REASONS[validation.reason] == true and not (solo and hasWagons)
return {id = ADRunsController.getVehicleId(vehicle), text = text, label = label, otherRunId = other ~= nil and other.id or nil,
otherRunName = other ~= nil and other.name or nil, blocked = blocked, solo = solo}
end)
if ok and entry.id ~= nil then
table.insert(result, entry)
@@ -140,7 +201,9 @@ function SmartPickupFrame:flowGuideContext(adEnv, draft)
return ids
end)
local context = {buildings = self:flowBuildingContext(adEnv, draft, farmId), fillTypes = {}, products = {}, storeBuildings = {},
waitPools = ADRunsController.getWaitPools(adEnv), vehicles = self:flowVehicleContext(adEnv, preview)}
waitPools = ADRunsController.getWaitPools(adEnv)}
context.wagons = self:flowWagonContext(adEnv, preview)
context.vehicles = self:flowVehicleContext(adEnv, preview, context.wagons)
if draft.kind == "outbound" then
for _, product in ipairs(ADRunsController.getRunProducts(adEnv, preview)) do
local title = tostring(product.title)
@@ -371,6 +434,13 @@ function SmartPickupFrame:saveFlowGuide(guide, start)
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
if vehicle ~= nil then ADRunsController.assignVehicle(runId, vehicle) end
end
-- solo-traktorernes vogne fra guiden: flowet husker dem straks og henter dem ved Connect ved start
local savedRun = ADRuns.getRun(ADRunsController.store, runId)
if savedRun ~= nil then
local wagons = ADRuns.copyWagons(savedRun.wagons)
for tractorId, wagonId in pairs(result.wagons or {}) do wagons[tractorId] = wagonId end
savedRun.wagons = wagons
end
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, runId))
self.selectedRunId = runId
local saved = string.format(self.i18n:getText("spu_guide_flow_saved"), tostring(result.name))
+3
View File
@@ -27,6 +27,9 @@
<text name="spu_status_started" text="Startet %d, sprunget over %d"/>
<text name="spu_status_noAd" text="AutoDrive-API ikke fundet"/>
<text name="spu_status_error" text="Fejl: %s"/>
<text name="spu_guide_flow_wagon" text=" ↳ Vogn"/>
<text name="spu_guide_flow_noFreeWagon" text=" ↳ ingen ledig vogn (frakoblet, med Connect-markør, kan bære varen)"/>
<text name="spu_guide_flow_soloText" text="%s — ingen vogn bagpå: henter en ledig vogn"/>
<text name="spu_reason_noTrailer" text="ingen vogn (kobl en på, eller giv vognens parkering flowets navn)"/>
<text name="spu_reason_cannotCarry" text="vognen kan ikke bære run'ets varer"/>
<text name="spu_reason_farFromNetwork" text="for langt fra vejnettet"/>
+3
View File
@@ -27,6 +27,9 @@
<text name="spu_status_started" text="Started %d, skipped %d"/>
<text name="spu_status_noAd" text="AutoDrive API not found"/>
<text name="spu_status_error" text="Error: %s"/>
<text name="spu_guide_flow_wagon" text=" ↳ Trailer"/>
<text name="spu_guide_flow_noFreeWagon" text=" ↳ no free trailer (detached, with a Connect marker, able to carry the goods)"/>
<text name="spu_guide_flow_soloText" text="%s — no trailer attached: fetches a free trailer"/>
<text name="spu_reason_noTrailer" text="no trailer (attach one, or name the trailer's parking after the flow)"/>
<text name="spu_reason_cannotCarry" text="trailer cannot carry the run's goods"/>
<text name="spu_reason_farFromNetwork" text="too far from the road network"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.60.9.0</version>
<version>1.60.10.0</version>
<title>
<en>AD Smart Pickup</en>
</title>