diff --git a/FS25_ADSmartPickup/adFlowGuide.lua b/FS25_ADSmartPickup/adFlowGuide.lua index f89d1c9..7f0f5f7 100644 --- a/FS25_ADSmartPickup/adFlowGuide.lua +++ b/FS25_ADSmartPickup/adFlowGuide.lua @@ -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 " — 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. diff --git a/FS25_ADSmartPickup/adFlowWagons.lua b/FS25_ADSmartPickup/adFlowWagons.lua index 65666fc..fa4e5dc 100644 --- a/FS25_ADSmartPickup/adFlowWagons.lua +++ b/FS25_ADSmartPickup/adFlowWagons.lua @@ -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 diff --git a/FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua b/FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua index e74d934..7dc9a96 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua @@ -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)) diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 6fb2657..7f0a983 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -27,6 +27,9 @@ + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 7295cd7..52baf98 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -27,6 +27,9 @@ + + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 2675ecb..349ec19 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.60.9.0 + 1.60.10.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adFlowGuide.lua b/tests/test_adFlowGuide.lua index 959399f..25decc4 100644 --- a/tests/test_adFlowGuide.lua +++ b/tests/test_adFlowGuide.lua @@ -251,5 +251,48 @@ check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), check("FL16 primært mål", previewIn.targetWayPointId, 101) check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true) + +-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ---- +local soloCtx = ctx({ + vehicles = { + {id = "s1", label = "Solo 1", text = "Solo 1", solo = true}, + {id = "s2", label = "Solo 2", text = "Solo 2", solo = true}, + {id = "s3", label = "Solo 3", text = "Solo 3", solo = true}, + {id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"}, + }, + wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}}, +}) +local soloGuide = FG.open(newRun("supply"), soloCtx) +local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true}) +check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1") +local two = FG.choose(one, "vehicleIds", {item = "s2", on = true}) +check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2") +check("WG2 første beholder sin", two.draft.wagons.s1, "w1") +local three = FG.choose(two, "vehicleIds", {item = "s3", on = true}) +check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil) +local soloRows = FG.rows(at(three, "tractors")) +local wagonRow = findRow(soloRows, "wagon", "s1") +check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice") +check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1") +check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1) +local noFree = false +for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end +check("WG5 solo uden ledig vogn får besked", noFree, true) +check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil) +local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"}) +check("WG6 skift vogn", swapped.draft.wagons.s1, "w2") +local off = FG.choose(two, "vehicleIds", {item = "s1", on = false}) +check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil) +local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true}) +check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1") +check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2") +check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil) +local remembered = newRun("supply", {vehicleIds = {"s2"}}) +remembered.wagons = {s2 = "w2"} +local reopened = FG.open(remembered, soloCtx) +check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2") +check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1") +check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2") + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adFlowWagons.lua b/tests/test_adFlowWagons.lua index 8362890..bdee32f 100644 --- a/tests/test_adFlowWagons.lua +++ b/tests/test_adFlowWagons.lua @@ -126,5 +126,15 @@ check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait") -- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad) check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none") + +-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk) +local ranked = W.rankWagons("Sugarbeet Cut", { + {id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"}, + {id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}}) +local order = {} +for _, w in ipairs(ranked) do table.insert(order, w.id) end +check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a") +check("rank: tom liste", #W.rankWagons("X", {}), 0) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)