From 6eff1dcae8cae1f2c61e70f8b37a2c28171f250d Mon Sep 17 00:00:00 2001 From: masterdraco Date: Mon, 28 Sep 2026 18:52:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(pulje):=20fl=C3=A5defanen=20viser=20puljet?= =?UTF-8?q?raktorer=20og=20-redskaber=20med=20status=20og=20hvorfor=20de?= =?UTF-8?q?=20ikke=20er=20med;=20puljetraktorer=20omd=C3=B8bes=20ikke=20ef?= =?UTF-8?q?ter=20l=C3=A5nte=20redskaber=20(v1.48.3.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adFieldWork.lua | 6 +- FS25_ADSmartPickup/adHitch.lua | 8 +- FS25_ADSmartPickup/adPool.lua | 100 +++++++++++++++++- .../gui/SmartPickupFieldTab.lua | 29 +++++ FS25_ADSmartPickup/l10n/l10n_da.xml | 19 ++++ FS25_ADSmartPickup/l10n/l10n_en.xml | 19 ++++ FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adPool.lua | 28 +++++ 8 files changed, 207 insertions(+), 4 deletions(-) diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index e8cf0d0..67e3137 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -136,6 +136,8 @@ local function isBusyVehicle(vehicle, id) or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle)) end +ADFieldWork.isBusyVehicle = isBusyVehicle + function ADFieldWork.getRigs(adEnv) local rigs = {} local state = ADFieldJobs.state @@ -1112,7 +1114,9 @@ function ADFieldWork.autoNameFleet(adEnv) local vehicle = rig.vehicle local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local model = vehicle.getName ~= nil and tostring(vehicle:getName()) or nil - if stateModule ~= nil and stateModule.setName ~= nil and model ~= nil and ADRuns.vehicleLabel(vehicle) == model and not rig.controlled then + -- puljetraktor med et lånt redskab: ikke opkaldt efter redskabet (in-game 28/9: Challenger blev "Kultivator 1") + local pooled = ADHitch ~= nil and ADPool ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0 + if stateModule ~= nil and stateModule.setName ~= nil and model ~= nil and ADRuns.vehicleLabel(vehicle) == model and not rig.controlled and not pooled then local name = ADNaming.fleetName(rig.role, rig.role == "harvester" and harvesterSubtype(rig) or nil, names) if name ~= nil then stateModule:setName(name) diff --git a/FS25_ADSmartPickup/adHitch.lua b/FS25_ADSmartPickup/adHitch.lua index 4bef086..77fa89b 100644 --- a/FS25_ADSmartPickup/adHitch.lua +++ b/FS25_ADSmartPickup/adHitch.lua @@ -299,7 +299,8 @@ end -- Puljeredskaber bag traktoren sættes af på deres parkering, og traktoren kører hjem til sin egen. -- Kun redskaber med en Connect-markør til deres parkering er puljeredskaber; andre bliver siddende. -function ADHitch.returnHome(adEnv, tractor, onDone) +-- Parkeringer (waypoint-id) for puljeredskaberne bag traktoren (tom = ingen puljeredskaber bag). +function ADHitch.poolParkIds(adEnv, tractor) local markers = adEnv.ADGraphManager:getMapMarkers() local parkIds = {} for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do @@ -307,6 +308,11 @@ function ADHitch.returnHome(adEnv, tractor, onDone) local park = parkId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(parkId) or nil if park ~= nil and ADPool.connectFor(park.name, markers) ~= nil then table.insert(parkIds, parkId) end end + return parkIds +end + +function ADHitch.returnHome(adEnv, tractor, onDone) + local parkIds = ADHitch.poolParkIds(adEnv, tractor) if #parkIds == 0 then return false end local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor)) ADPool.setReturning(tractor, true) diff --git a/FS25_ADSmartPickup/adPool.lua b/FS25_ADSmartPickup/adPool.lua index 0d45732..b38dc2f 100644 --- a/FS25_ADSmartPickup/adPool.lua +++ b/FS25_ADSmartPickup/adPool.lua @@ -95,6 +95,29 @@ function ADPool.mayUsePool(decision, hasOwnForRole) return not decision.helper and (decision.count or 1) == 1 and not hasOwnForRole end +-- Status til flådefanen. s = {parkName, hasConnect, attachedTo, quarantineMin, reserved} -> nøgle, argument +function ADPool.implementStatus(s) + if s.parkName == nil then return "noPark" end + if not s.hasConnect then return "noConnect", s.parkName end + if s.attachedTo ~= nil then return "attached", s.attachedTo end + if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end + if s.reserved then return "reserved" end + return "parked", s.parkName +end + +-- s = {hasPark, enabled, controlled, returning, fieldId, busy, offNetwork, hitched} -> nøgle, argument +function ADPool.tractorStatus(s) + if not s.hasPark then return "noPark" end + if s.controlled then return "player" end + if s.returning then return "returning" end + if s.fieldId ~= nil then return "job", s.fieldId end + if s.busy then return "busy" end + if not s.enabled then return "notInFleet" end + if s.offNetwork then return "offNetwork" end + if s.hitched then return "hitched" end + return "free" +end + -- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet. -- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool. function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) @@ -153,6 +176,19 @@ function ADPool.quarantine(id, nowMs) quarantined[id] = nowMs + ADPool.QUARANTINE_MS end +function ADPool.quarantineMinutes(id, nowMs) + local untilMs = quarantined[id] + if untilMs == nil or nowMs >= untilMs then return 0 end + return math.ceil((untilMs - nowMs) / 60000) +end + +function ADPool.isReserved(id) + for _, ids in pairs(reservations) do + for _, reserved in ipairs(ids) do if reserved == id then return true end end + end + return false +end + function ADPool.blocked(nowMs) local result = {} for _, ids in pairs(reservations) do @@ -213,6 +249,15 @@ local function isAttached(vehicle) return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil end +-- Traktorens egen rolle uden de redskaber der hænger på (mejetærsker/selvkørende har en, en traktor ikke). +function ADPool.ownRole(vehicle) + local specs = {} + for _, name in ipairs(ADCpBridge.SPECS) do + if vehicle[name] ~= nil then specs[name] = true end + end + return ADFieldFleet.roleFromSpecs(specs) +end + -- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer. -- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur). function ADPool.collect(adEnv, isBusy) @@ -245,7 +290,7 @@ function ADPool.collect(adEnv, isBusy) if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0 and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle) and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) - and ADPool.canPull(ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle))) then + and ADPool.canPull(ADPool.ownRole(vehicle)) then local x, z = positionOf(vehicle) -- uden for AD-vejnettet: den kan ikke køre ud efter redskabet (ville sende redskaber i karantæne) local networkDistance = ADFieldWork.distanceToNetwork(adEnv, x, z) @@ -256,3 +301,56 @@ function ADPool.collect(adEnv, isBusy) end return implements, tractors end + +-- Oversigt til flådefanen: alle redskaber med en markrolle (også dem der mangler parkering/Connect, så +-- spilleren kan se hvorfor) og alle traktorer uden egen arbejdsrolle, der ikke hører til et flow. +function ADPool.overview(adEnv) + local markers = adEnv.ADGraphManager:getMapMarkers() + local farmId = g_currentMission:getFarmId() + local nowMs = ADFieldJobs.now or 0 + local implements, tractors = {}, {} + for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do + local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId + if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil then + local role = ADPool.roleOf(ADCpBridge.getSpecs(vehicle)) + if role ~= nil then + local id = ADRunsController.getVehicleId(vehicle) + local park = parkMarker(adEnv, vehicle) + local attacher = isAttached(vehicle) and vehicle:getAttacherVehicle() or nil + local key, arg = ADPool.implementStatus({ + parkName = park ~= nil and park.name or nil, + hasConnect = park ~= nil and ADPool.connectFor(park.name, markers) ~= nil, + attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil, + quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0, + reserved = id ~= nil and ADPool.isReserved(id), + }) + table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg}) + end + end + end + local state = ADFieldJobs.state + for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do + local id = ADRunsController.getVehicleId(vehicle) + if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil + and ADPool.canPull(ADPool.ownRole(vehicle)) then + local x, z = positionOf(vehicle) + local distance = ADFieldWork.distanceToNetwork(adEnv, x, z) + local assignment = state.assignments[id] + local rear = ADHitch.rearImplementsOf(vehicle) + local key, arg = ADPool.tractorStatus({ + hasPark = parkMarker(adEnv, vehicle) ~= nil, + enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id), + controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled(), + returning = ADPool.isReturning(vehicle), + fieldId = assignment ~= nil and assignment.fieldId or nil, + busy = ADFieldWork.isBusyVehicle(vehicle, id), + offNetwork = distance ~= nil and distance > ADFieldWork.MAX_OFF_NETWORK, + hitched = #rear > 0, + }) + table.insert(tractors, {id = id, label = ADRuns.vehicleLabel(vehicle), hp = tractorHpOf(vehicle), status = key, arg = arg, + enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)}) + end + end + return implements, tractors +end + diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua index e534ea9..1b48f09 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua @@ -249,6 +249,35 @@ function SmartPickupFrame:fillFleetRows(adEnv) if #rigs == 0 then self:addTextRow(" " .. i18n:getText("spu_field_noRigs")) end + if ADPool ~= nil and ADPool.overview ~= nil then self:fillPoolRows(adEnv) end +end + +-- Maskinpuljen: solo-traktorer (flueben som resten af flåden) og redskaber med status og hvorfor de ikke er med. +function SmartPickupFrame:fillPoolRows(adEnv) + local i18n = self.i18n + local ok, implements, tractors = pcall(ADPool.overview, adEnv) + if not ok then return end + local byLabel = function(a, b) return ADFlowView.naturalLess(a.label, b.label) end + table.sort(tractors, byLabel) + table.sort(implements, function(a, b) + if a.role ~= b.role then return a.role < b.role end + return byLabel(a, b) + end) + self:addTextRow(i18n:getText("spu_pool_section_tractors")) + for _, tractor in ipairs(tractors) do + local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id}) + local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg)) + local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or "" + row:getDescendantByName("title"):setText(string.format(" %s%s — %s", tostring(tractor.label), hp, status)) + row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false) + end + if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end + self:addTextRow(i18n:getText("spu_pool_section_implements")) + for _, implement in ipairs(implements) do + local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg)) + self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status)) + end + if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end end -- ---- live ---- diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 3f8635e..763069d 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -85,6 +85,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 9ce1e9c..955ec79 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -85,6 +85,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 9127422..e229f90 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.48.2.0 + 1.48.3.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adPool.lua b/tests/test_adPool.lua index 1a7703b..c1d9f44 100644 --- a/tests/test_adPool.lua +++ b/tests/test_adPool.lua @@ -102,5 +102,33 @@ check("I4: almindelig", P.mayUsePool({count = 1}, false), true) check("I4: tømmevogn", P.mayUsePool({count = 1, helper = true}, false), false) check("I4: multi-tool", P.mayUsePool({count = 2}, false), false) check("I4: marken har faste", P.mayUsePool({count = 1}, true), false) +-- status til flådefanen +local function st(key, arg) return tostring(key) .. "|" .. tostring(arg) end +local function ist(s) return st(P.implementStatus(s)) end +check("redskab: uden parkering", ist({}), "noPark|nil") +check("redskab: uden Connect", ist({parkName = "Plov"}), "noConnect|Plov") +check("redskab: på traktor", ist({parkName = "Plov", hasConnect = true, attachedTo = "MT635"}), "attached|MT635") +check("redskab: karantæne", ist({parkName = "Plov", hasConnect = true, quarantineMin = 8}), "quarantine|8") +check("redskab: reserveret", ist({parkName = "Plov", hasConnect = true, reserved = true}), "reserved|nil") +check("redskab: parkeret", ist({parkName = "Plov", hasConnect = true}), "parked|Plov") +local function tst(s) return st(P.tractorStatus(s)) end +local base = {hasPark = true, enabled = true} +check("traktor: uden parkering", tst({enabled = true}), "noPark|nil") +check("traktor: spiller", tst({hasPark = true, enabled = true, controlled = true}), "player|nil") +check("traktor: hjemtur", tst({hasPark = true, enabled = true, returning = true}), "returning|nil") +check("traktor: opgave", tst({hasPark = true, enabled = true, fieldId = 52}), "job|52") +check("traktor: optaget", tst({hasPark = true, enabled = true, busy = true}), "busy|nil") +check("traktor: ikke i flåden", tst({hasPark = true}), "notInFleet|nil") +check("traktor: uden for vejnet", tst({hasPark = true, enabled = true, offNetwork = true}), "offNetwork|nil") +check("traktor: har andet redskab på", tst({hasPark = true, enabled = true, hitched = true}), "hitched|nil") +check("traktor: ledig", tst(base), "free|nil") +-- karantæne-rest i hele minutter (rundet op) og reservation +P.quarantine("q1", 0) +check("karantæne: minutter tilbage", P.quarantineMinutes("q1", 60000), 9) +check("karantæne: udløbet", P.quarantineMinutes("q1", P.QUARANTINE_MS), 0) +P.reserve("owner", {"r1"}) +check("reserveret: ja", P.isReserved("r1"), true) +P.release("owner") +check("reserveret: nej", P.isReserved("r1"), false) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)