From 6afcae6fe165ae827c6cacc9d1bf6f595147f196 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Sun, 27 Sep 2026 14:05:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(markarbejde):=20manuel=20transport=20af=20?= =?UTF-8?q?brede=20h=C3=B8stere=20=E2=80=94=20flueben=20pr.=20h=C3=B8ster?= =?UTF-8?q?=20i=20markfl=C3=A5den;=20moden=20beder=20spilleren=20k=C3=B8re?= =?UTF-8?q?=20den=20til=20marken=20og=20montere=20sk=C3=A6rebordet=20(dial?= =?UTF-8?q?og=20+=20p=C3=A5mindelse=20hvert=205.=20min),=20overtager=20n?= =?UTF-8?q?=C3=A5r=20den=20st=C3=A5r=20p=C3=A5=20marken=20med=20passende?= =?UTF-8?q?=20sk=C3=A6rebord=20og=20spilleren=20er=20steget=20ud=20(eller?= =?UTF-8?q?=20ved=20Start=20nu),=20t=C3=B8mmevogne=20f=C3=B8rst=20n=C3=A5r?= =?UTF-8?q?=20h=C3=B8sten=20k=C3=B8rer,=20ingen=20AD-parkering=20bagefter?= =?UTF-8?q?=20(v1.38.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adFieldJobs.lua | 36 +++++- FS25_ADSmartPickup/adFieldStorage.lua | 11 +- FS25_ADSmartPickup/adFieldWork.lua | 110 ++++++++++++++---- FS25_ADSmartPickup/adManualTransport.lua | 32 +++++ .../gui/SmartPickupFieldTab.lua | 21 +++- FS25_ADSmartPickup/l10n/l10n_da.xml | 3 + FS25_ADSmartPickup/l10n/l10n_en.xml | 3 + FS25_ADSmartPickup/modDesc.xml | 3 +- tests/test_adFieldJobs.lua | 3 + tests/test_adFieldStorage.lua | 6 + tests/test_adManualTransport.lua | 37 ++++++ 11 files changed, 231 insertions(+), 34 deletions(-) create mode 100644 FS25_ADSmartPickup/adManualTransport.lua create mode 100644 tests/test_adManualTransport.lua diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua index a3d73fc..47db921 100644 --- a/FS25_ADSmartPickup/adFieldJobs.lua +++ b/FS25_ADSmartPickup/adFieldJobs.lua @@ -16,7 +16,8 @@ ADFieldJobs.tickCount = 0 ADFieldJobs.MAX_PASSES = 2 -- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben -- courseRecords: vehicleId -> post i kursus-registret (ADCourseCache) -ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}} +-- manual: {[vehicleId] = true} = høstere spilleren selv kører til marken (for brede til vejene) +ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}, manual = {}} -- mark-id -> {mower = kursus, rake = kursus, = fælles multi-tool-kursus}: kopier (kun i hukommelsen; -- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på) ADFieldJobs.courses = {} @@ -26,7 +27,8 @@ ADFieldJobs.timer = 500 ADFieldJobs.now = 0 ADFieldJobs.loaded = false -ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true} +-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang) +ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true} -- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente. ADFieldJobs.REGROW_RATIO = 0.1 @@ -45,7 +47,7 @@ local function cropGone(stage, info) end -- Ligger (x, z) inden for omridset? (lige-ulige-reglen; kanten tæller som inde) -local function insidePolygon(x, z, verts) +function ADFieldJobs.insidePolygon(x, z, verts) local inside, j = false, #verts for i = 1, #verts do local a, b = verts[i], verts[j] @@ -59,7 +61,7 @@ end -- fields = {{id, stage, verts = {{x, z}}}} function ADFieldJobs.resumeField(x, z, role, fields) for _, field in ipairs(fields or {}) do - if #(field.verts or {}) >= 3 and ADFieldModel.roleFor(field.stage) == role and insidePolygon(x, z, field.verts) then + if #(field.verts or {}) >= 3 and ADFieldModel.roleFor(field.stage) == role and ADFieldJobs.insidePolygon(x, z, field.verts) then return field.id end end @@ -104,7 +106,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env) if not hasMarker then return {action = "wait", care = care} end return {action = "care", stage = care} end - local active, finished, helpers = 0, 0, 0 + local active, finished, helpers, awaiting = 0, 0, 0, 0 for _, assignment in ipairs(assignments) do if assignment.stage == nil or assignment.stage == field.stage then local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase] @@ -112,6 +114,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env) if isActive then helpers = helpers + 1 end else if isActive then active = active + 1 end + if assignment.phase == "awaitingPlayer" then awaiting = awaiting + 1 end if assignment.phase == "finished" then finished = finished + 1 end end end @@ -138,7 +141,8 @@ function ADFieldJobs.decide(field, info, assignments, now, env) end -- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen local helperRole = ADFieldModel.helperFor(field.stage) - if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then + -- tømmevogne først når en høster faktisk er i gang (ikke mens spilleren kører den derud) + if helperRole ~= nil and active > awaiting and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then return {action = "dispatch", role = helperRole, count = 1, helper = true} end return {action = "none"} @@ -182,6 +186,7 @@ function ADFieldJobs.load() ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet ADFieldJobs.state.fleetMode = loaded.fleetMode or "all" ADFieldJobs.state.courseRecords = loaded.courseRecords or {} + ADFieldJobs.state.manual = loaded.manual or {} else Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded)) end @@ -193,6 +198,20 @@ function ADFieldJobs.setPlan(id, changes) ADFieldJobs.save() end +-- "Start nu" på en mark hvor moden venter på spilleren (manuel transport): overtag uden at vente +ADFieldJobs.takeoverRequested = {} +function ADFieldJobs.requestTakeover(id) + ADFieldJobs.takeoverRequested[id] = true +end + +-- Venter moden på at spilleren kører en høster til marken? (assignment eller nil) +function ADFieldJobs.awaitingPlayer(fieldId) + for _, assignment in pairs(ADFieldJobs.state.assignments) do + if assignment.fieldId == fieldId and assignment.phase == "awaitingPlayer" then return assignment end + end + return nil +end + function ADFieldJobs.requestStart(id) ADFieldJobs.note(id, {startRequested = true}) end @@ -212,6 +231,11 @@ function ADFieldJobs.setFleetMode(mode) ADFieldJobs.save() end +function ADFieldJobs.toggleManual(vehicleId) + ADFieldJobs.state.manual = ADFieldFleet.toggle(ADFieldJobs.state.manual or {}, vehicleId) + ADFieldJobs.save() +end + function ADFieldJobs.toggleFleet(vehicleId) ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId) ADFieldJobs.save() diff --git a/FS25_ADSmartPickup/adFieldStorage.lua b/FS25_ADSmartPickup/adFieldStorage.lua index 9555a0b..8df1e2c 100644 --- a/FS25_ADSmartPickup/adFieldStorage.lua +++ b/FS25_ADSmartPickup/adFieldStorage.lua @@ -73,6 +73,11 @@ function ADFieldStorage.save(state) xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId) vehicleIndex = vehicleIndex + 1 end + local manualIndex = 0 + for vehicleId in pairs(state.manual or {}) do + xmlFile:setString(string.format("%s.manual.vehicle(%d)#id", ADFieldStorage.ROOT, manualIndex), vehicleId) + manualIndex = manualIndex + 1 + end local courseIndex = 0 for _, record in pairs(state.courseRecords or {}) do ADFieldStorage.writeCourseRecord(xmlFile, string.format("%s.courses.course(%d)", ADFieldStorage.ROOT, courseIndex), record) @@ -84,7 +89,7 @@ function ADFieldStorage.save(state) end function ADFieldStorage.load() - local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}} + local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}, manual = {}} local path = ADFieldStorage.getPath() if path == nil or not fileExists(path) then return result end local xmlFile = XMLFile.load("ADSmartPickupFields", path) @@ -98,6 +103,10 @@ function ADFieldStorage.load() local id = xmlFile:getString(key .. "#id") if id ~= nil and id ~= "" then result.fleet[id] = true end end) + xmlFile:iterate(ADFieldStorage.ROOT .. ".manual.vehicle", function(_, key) + local id = xmlFile:getString(key .. "#id") + if id ~= nil and id ~= "" then result.manual[id] = true end + end) xmlFile:iterate(ADFieldStorage.ROOT .. ".courses.course", function(_, key) local record = ADCourseCache ~= nil and ADCourseCache.newRecord(ADFieldStorage.readCourseRecord(xmlFile, key)) or nil if record ~= nil then result.courseRecords[record.vehicle] = record end diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index 7ad70b0..079d394 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -138,6 +138,8 @@ function ADFieldWork.getRigs(adEnv) busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle) or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)), controlled = isControlledByPlayer(vehicle), + -- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord) + manual = (state.manual or {})[id] == true, width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z, -- tom spreder kan flere ting (gødning og kalk): alle dens roller roles = specs.sprayRoles, filledRole = specs.filledRole, dumpable = specs.dumpable, @@ -300,7 +302,7 @@ end local function acceptFor(field, info, role) if role == "harvester" then local fruit = info.fruitIndex - return function(candidate) return fruit ~= nil and candidate.fruits ~= nil and candidate.fruits[fruit] == true end + return function(candidate) return ADManualTransport.acceptsHarvester(candidate, fruit) end elseif role == "seeder" then local fruitIndex = ADFieldWork.sowFruitIndex(field) return function(candidate) return candidate.hasSeeds and fruitIndex ~= nil and candidate.seedFruits[fruitIndex] == true end @@ -351,6 +353,19 @@ local function refillPlan(adEnv, fieldId, vehicle, role, x, z) return {fillType = fillType, empty = empty, refillId = refillId} end +-- Manuel transport: bed spilleren køre høsteren til marken (dialog første gang, ellers besked i hjørnet). +local function askPlayer(field, info, assignment, withDialog) + local text = string.format("F%s skal høstes: kør '%s' til %s, montér skærebordet og stig ud — så overtager Smart Pickup", + tostring(field.id), ADRuns.vehicleLabel(assignment.vehicle), tostring(info.markerName or ("F" .. tostring(field.id)))) + assignment.remindedAt = now() + log(field.id, "venter på spilleren: %s", text) + if withDialog and InfoDialog ~= nil and InfoDialog.show ~= nil then + pcall(InfoDialog.show, "Smart Pickup: " .. text) + else + ADFieldJobs.notify(text, true) + end +end + function ADFieldWork.dispatch(adEnv, field, info, decision) local role = decision.role local ok, reason = preflight(field, role) @@ -409,26 +424,34 @@ function ADFieldWork.dispatch(adEnv, field, info, decision) groupSize = decision.helper and 1 or count, lane = count > 1 and lanes[index] or nil, leader = index == 1, fillType = decision.helper and info.harvestFillType or nil, } - local refill = refillPlan(adEnv, field.id, rig.vehicle, role, info.x, info.z) - if refill ~= nil and refill.empty and refill.refillId == nil then - -- tom og intet sted at fylde op: ud på marken ville den bare køre kurset uden at sprede - fail(field.id, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op", rig.label, fillTypeName(refill.fillType))) - return - end - assignment.startX, assignment.startZ = rig.x, rig.z - ADFieldJobs.state.assignments[rig.id] = assignment - if refill ~= nil and refill.empty then - -- AutoDrive LOAD: påfyldningen (2. markør), fyld op, videre til F-markøren (1.) — så Courseplay - startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType) - assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType} - local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refill.refillId) - log(field.id, "%s '%s' er tom — fyldes med %s ved %s og kører så til %s", role, rig.label, fillTypeName(refill.fillType), - tostring(marker ~= nil and marker.name or refill.refillId), tostring(info.markerName)) + if role == "harvester" and rig.manual and not assignment.helper then + -- for bred til vejene: spilleren kører den selv til marken og monterer skærebordet + assignment.phase, assignment.manual = "awaitingPlayer", true + assignment.startX, assignment.startZ = rig.x, rig.z + ADFieldJobs.state.assignments[rig.id] = assignment + askPlayer(field, info, assignment, true) else - startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil) - assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId} - log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0, - assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "") + local refill = refillPlan(adEnv, field.id, rig.vehicle, role, info.x, info.z) + if refill ~= nil and refill.empty and refill.refillId == nil then + -- tom og intet sted at fylde op: ud på marken ville den bare køre kurset uden at sprede + fail(field.id, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op", rig.label, fillTypeName(refill.fillType))) + return + end + assignment.startX, assignment.startZ = rig.x, rig.z + ADFieldJobs.state.assignments[rig.id] = assignment + if refill ~= nil and refill.empty then + -- AutoDrive LOAD: påfyldningen (2. markør), fyld op, videre til F-markøren (1.) — så Courseplay + startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType) + assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType} + local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refill.refillId) + log(field.id, "%s '%s' er tom — fyldes med %s ved %s og kører så til %s", role, rig.label, fillTypeName(refill.fillType), + tostring(marker ~= nil and marker.name or refill.refillId), tostring(info.markerName)) + else + startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil) + assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId} + log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0, + assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "") + end end end note(field.id, {reason = false}) @@ -790,14 +813,53 @@ local function finishAssignment(adEnv, assignment) end log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle)) -- læssevogn/påfyldte sæt parkeres af AutoDrive selv (onCpFinished med parkering sat) - local park = not assignment.adHandlesEnd and parkMarkerId(adEnv, vehicle) or nil + -- manuel transport: ALDRIG ud på vejen med AutoDrive (skærebordet sidder på) — spilleren henter den + local park = not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end + if assignment.manual then + ADFieldJobs.notify(string.format("F%s høstet — afmontér skærebordet og kør '%s' hjem", tostring(assignment.fieldId), ADRuns.vehicleLabel(vehicle)), true) + end restoreToolOffset(assignment) assignment.phase = "finished" table.insert(ADFieldJobs.finished, assignment) ADFieldJobs.state.assignments[assignment.vehicleId] = nil end +local MANUAL_REASON_TEXT = { + notThere = "kør den til marken", + noHeader = "montér skærebordet", + wrongHeader = "skærebordet kan ikke høste afgrøden — montér det rigtige", + playerInside = "stig ud, så overtager Smart Pickup", +} + +-- Manuel transport: overtag når høsteren står på marken med passende skærebord og spilleren er steget ud +-- (eller når spilleren trykker "Start nu"); påmind hvert 5. minut indtil da. +local function followManual(adEnv, field, info, assignment) + if field == nil or info == nil then return ADFieldWork.release(assignment) end + local vehicle = assignment.vehicle + local x, z = vehiclePosition(vehicle) + local atField = #(info.verts or {}) >= 3 and ADFieldJobs.insidePolygon(x, z, info.verts) + if not atField and info.markerId ~= nil then + local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId) + atField = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) <= ADFieldWork.ARRIVE_DISTANCE + end + local forced = ADFieldJobs.takeoverRequested[field.id] == true + ADFieldJobs.takeoverRequested[field.id] = nil + local ok, why = ADManualTransport.readiness({atField = atField, fruits = ADCpBridge.getHarvestFruits(vehicle) or {}, + fruit = info.fruitIndex, controlled = isControlledByPlayer(vehicle), forced = forced}) + if ok then + log(field.id, "overtager '%s'%s", ADRuns.vehicleLabel(vehicle), forced and " (Start nu)" or "") + return beginWork(adEnv, field, info, assignment) + end + local reason = MANUAL_REASON_TEXT[why] or tostring(why) + note(field.id, {reason = {text = string.format("venter på dig: '%s' — %s", ADRuns.vehicleLabel(vehicle), reason)}}) + if forced then + ADFieldJobs.notify(string.format("F%s: kan ikke starte '%s' endnu — %s", tostring(field.id), ADRuns.vehicleLabel(vehicle), reason), true) + elseif ADManualTransport.shouldRemind(assignment.remindedAt, now()) then + askPlayer(field, info, assignment, false) + end +end + local function followWorking(adEnv, field, assignment) local vehicle = assignment.vehicle if assignment.role == "rake" and field.windrowOffset == nil and now() - assignment.since >= ADFieldWork.WINDROW_MEASURE_MS @@ -859,7 +921,7 @@ function ADFieldWork.resumeAfterLoad(adEnv) if fieldId ~= nil then ADFieldJobs.state.assignments[rig.id] = {vehicleId = rig.id, vehicle = vehicle, fieldId = fieldId, role = rig.role, stage = stageById[fieldId], helper = false, phase = "working", since = now(), width = rig.width, - groupSize = 1, leader = true, resumed = true} + groupSize = 1, leader = true, resumed = true, manual = rig.manual == true} -- lager til læssevognens rest, som ved en normal start local assignment = ADFieldJobs.state.assignments[rig.id] local field, info = ADFieldJobs.getField(fieldId), ADFields.getInfo(fieldId) @@ -886,7 +948,7 @@ function ADFieldWork.follow(adEnv, assignment) -- førersædet og kigge må ikke slippe det (in-game 25/9). local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting" local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0 - if isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then + if assignment.phase ~= "awaitingPlayer" and isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle)) return ADFieldWork.release(assignment) end @@ -935,6 +997,8 @@ function ADFieldWork.follow(adEnv, assignment) end elseif assignment.phase == "working" then followWorking(adEnv, field, assignment) + elseif assignment.phase == "awaitingPlayer" then + followManual(adEnv, field, info, assignment) end end diff --git a/FS25_ADSmartPickup/adManualTransport.lua b/FS25_ADSmartPickup/adManualTransport.lua new file mode 100644 index 0000000..45d5da8 --- /dev/null +++ b/FS25_ADSmartPickup/adManualTransport.lua @@ -0,0 +1,32 @@ +-- AD Manual Transport +-- Brede høstere (mejetærsker med skærebord) kan ikke køre på vejene: spilleren kører dem til marken, monterer +-- skærebordet og stiger ud; moden overtager derfra. Rene regler (testbare uden spillet); spil-delen ligger i +-- adFieldWork. + +ADManualTransport = {} + +ADManualTransport.REMIND_MS = 300000 -- påmindelse hvert 5. minut, mens moden venter på spilleren + +-- Må høsteren vælges til marken? Manuel: ja — skærebordet monteres først på marken (tjekkes ved overtagelse). +-- Ellers skal det monterede skærebord kunne tage afgrøden. +function ADManualTransport.acceptsHarvester(candidate, fruit) + if fruit == nil then return false end + if candidate.manual then return true end + return candidate.fruits ~= nil and candidate.fruits[fruit] == true +end + +-- Klar til overtagelse? s = {atField, fruits = {[fruit] = true}, fruit, controlled, forced}. +-- Returnerer true, eller false + grund: "notThere" | "noHeader" | "wrongHeader" | "playerInside". +-- forced ("Start nu"): sted og spiller springes over, skærebordet tjekkes stadig. +function ADManualTransport.readiness(s) + if not s.forced and not s.atField then return false, "notThere" end + local fruits = s.fruits or {} + if next(fruits) == nil then return false, "noHeader" end + if s.fruit == nil or not fruits[s.fruit] then return false, "wrongHeader" end + if not s.forced and s.controlled then return false, "playerInside" end + return true +end + +function ADManualTransport.shouldRemind(lastAt, now) + return lastAt == nil or now - lastAt >= ADManualTransport.REMIND_MS +end diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua index 418d6cb..f2594fa 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua @@ -6,7 +6,7 @@ -- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua). -- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow. -SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true} +SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true, fleetManual = true} SmartPickupFrame.ROLE_KEYS = { mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake", collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader", @@ -238,6 +238,12 @@ function SmartPickupFrame:fillFleetRows(adEnv) local title = string.format(" %s — %s, %.1f m%s", tostring(rig.label), self:roleText(rig.role), rig.width or 0, self:rigDetails(rig)) row:getDescendantByName("title"):setText(title) row:getDescendantByName("option"):setState(rig.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false) + if rig.role == "harvester" then + -- for bred til vejene med skærebord: spilleren kører den til marken, moden overtager derfra + local option = self:addToggleRow("fleetManual", string.format(i18n:getText("spu_field_manualTransport"), tostring(rig.label)), + {i18n:getText("ui_no"), i18n:getText("ui_yes")}, rig.manual) + self.rowData[option] = {kind = "fleetManual", vehicleId = rig.id} + end end if #rigs == 0 then self:addTextRow(" " .. i18n:getText("spu_field_noRigs")) @@ -266,7 +272,9 @@ function SmartPickupFrame:refreshFieldRows(adEnv) local selected = self.selectedFieldId ~= nil and ADFieldJobs.getField(self.selectedFieldId) or nil local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil local isWorking = selected ~= nil and ADFieldModel.isWorking(selected) - self.fieldStartButtonInfo.text = self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start") + local awaiting = self.selectedFieldId ~= nil and ADFieldJobs.awaitingPlayer(self.selectedFieldId) ~= nil + self.fieldStartButtonInfo.text = self.i18n:getText(awaiting and "spu_field_button_takeover" + or (isWorking and "spu_field_button_stop" or "spu_field_button_start")) self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil) end @@ -279,6 +287,8 @@ function SmartPickupFrame:onClickFieldRow(state, data) ADFieldJobs.toggleFleet(data.vehicleId) elseif data.kind == "fleetMode" then ADFieldJobs.setFleetMode(isOn and "all" or "ticked") + elseif data.kind == "fleetManual" then + if ((ADFieldJobs.state.manual or {})[data.vehicleId] == true) ~= isOn then ADFieldJobs.toggleManual(data.vehicleId) end end frame:requestRebuild() end) @@ -290,7 +300,12 @@ function SmartPickupFrame:onClickFieldStartStop() local id = frame.selectedFieldId if id == nil then return end local field = ADFieldJobs.getField(id) - if ADFieldModel.isWorking(field) then + if ADFieldJobs.awaitingPlayer(id) ~= nil then + -- manuel transport: overtag nu (skærebordet tjekkes stadig) + ADFieldJobs.requestTakeover(id) + frame.statusOverride = string.format(frame.i18n:getText("spu_field_takeoverRequested"), id) + frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS + elseif ADFieldModel.isWorking(field) then ADFieldJobs.stopField(frame.getAdEnv(), id) else local decision = ADFieldJobs.lastDecision[id] diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index c2c708b..22c9b87 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -59,6 +59,9 @@ + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 44b8af1..178dc45 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -59,6 +59,9 @@ + + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index f689958..900ae9b 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.37.2.0 + 1.38.0.0 <en>AD Smart Pickup</en> @@ -38,6 +38,7 @@ + diff --git a/tests/test_adFieldJobs.lua b/tests/test_adFieldJobs.lua index d9ab0f0..acb3613 100644 --- a/tests/test_adFieldJobs.lua +++ b/tests/test_adFieldJobs.lua @@ -52,6 +52,9 @@ local withHarvester = {{phase = "working", stage = "harvesting", helper = false} local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0) check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true) check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none") +-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn +local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}} +check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none") -- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0} check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance") diff --git a/tests/test_adFieldStorage.lua b/tests/test_adFieldStorage.lua index 9183c6f..4910b33 100644 --- a/tests/test_adFieldStorage.lua +++ b/tests/test_adFieldStorage.lua @@ -92,5 +92,11 @@ ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"}) local old = ADFieldStorage.load() check("ST7 gammel fil: tomt register", next(old.courseRecords), nil) +-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom +disk = {} +ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}}) +check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true) +check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adManualTransport.lua b/tests/test_adManualTransport.lua new file mode 100644 index 0000000..256ba8f --- /dev/null +++ b/tests/test_adManualTransport.lua @@ -0,0 +1,37 @@ +-- Kør: luajit tests/test_adManualTransport.lua (fra repo-roden) +-- Manuel transport af brede høstere (in-game 27/9: mejetærskere med skærebord er for brede til vejene). +dofile("FS25_ADSmartPickup/adManualTransport.lua") +local M = ADManualTransport +local failures = 0 +local function check(name, actual, expected) + if actual == expected then print("OK " .. name) else + failures = failures + 1 + print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) + end +end +local WHEAT, BARLEY = 1, 2 + +-- valg af høster +check("manuel høster uden skærebord kan vælges", M.acceptsHarvester({manual = true, fruits = {}}, WHEAT), true) +check("automatisk høster uden skærebord kan ikke", M.acceptsHarvester({fruits = {}}, WHEAT), false) +check("automatisk høster med passende skærebord", M.acceptsHarvester({fruits = {[WHEAT] = true}}, WHEAT), true) +check("ukendt afgrøde -> ingen", M.acceptsHarvester({manual = true}, nil), false) + +-- klar til overtagelse +local header = {[WHEAT] = true} +local function ready(s) local ok, why = M.readiness(s); return ok and "ok" or why end +check("hjemme -> notThere", ready({atField = false, fruits = {}, fruit = WHEAT}), "notThere") +check("på marken uden skærebord -> noHeader", ready({atField = true, fruits = {}, fruit = WHEAT}), "noHeader") +check("forkert skærebord -> wrongHeader", ready({atField = true, fruits = {[BARLEY] = true}, fruit = WHEAT}), "wrongHeader") +check("spilleren sidder i -> playerInside", ready({atField = true, fruits = header, fruit = WHEAT, controlled = true}), "playerInside") +check("klar", ready({atField = true, fruits = header, fruit = WHEAT}), "ok") +check("Start nu: spiller i og ikke på marken -> ok", ready({atField = false, fruits = header, fruit = WHEAT, controlled = true, forced = true}), "ok") +check("Start nu tjekker stadig skærebordet", ready({atField = false, fruits = {}, fruit = WHEAT, forced = true}), "noHeader") + +-- påmindelser +check("første gang -> påmind", M.shouldRemind(nil, 1000), true) +check("for tidligt", M.shouldRemind(1000, 1000 + M.REMIND_MS - 1), false) +check("efter 5 min", M.shouldRemind(1000, 1000 + M.REMIND_MS), true) + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)