chore: interne design-/plannoter og bench ud af repoet (kun lokalt)
This commit is contained in:
@@ -1 +1,4 @@
|
||||
*.zip
|
||||
docs/superpowers/
|
||||
bench/
|
||||
.superpowers/
|
||||
|
||||
@@ -57,7 +57,6 @@ Fejl og idéer: opret et issue her i repoet. Vedlæg gerne `log.txt` fra
|
||||
```
|
||||
|
||||
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||
- Design og planer: `docs/superpowers/`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Simuleringer
|
||||
|
||||
`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner
|
||||
(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode.
|
||||
|
||||
luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode
|
||||
git worktree add /tmp/old <commit>; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign
|
||||
|
||||
Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning.
|
||||
luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms.
|
||||
@@ -1,103 +0,0 @@
|
||||
-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua <mappe med modulerne>)
|
||||
-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3
|
||||
-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode
|
||||
-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame.
|
||||
-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller.
|
||||
local dir = arg[1] or "FS25_ADSmartPickup"
|
||||
math.randomseed(42)
|
||||
local function noop() end
|
||||
Logging = {info = noop, warning = noop}
|
||||
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
|
||||
function entityExists() return true end
|
||||
local nodes = {}
|
||||
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
||||
local simTime = 0
|
||||
g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}}
|
||||
function netGetTime() return os.clock() * 1000 end
|
||||
addModEventListener = noop
|
||||
Utils = {prependedFunction = function(a) return a end}
|
||||
|
||||
-- vejnet og markører
|
||||
local wayPoints, markers = {}, {}
|
||||
for id = 1, 13550 do
|
||||
local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600
|
||||
wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}}
|
||||
end
|
||||
for index = 1, 600 do
|
||||
local id = index * 22
|
||||
table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"})
|
||||
end
|
||||
-- stationer ved hver 5. markør, 3 triggere hver
|
||||
local stations = {}
|
||||
for index = 1, 120 do
|
||||
local wp = wayPoints[markers[150 + index].id]
|
||||
local triggers = {}
|
||||
for t = 1, 3 do
|
||||
local node = #nodes + 1
|
||||
nodes[node] = {wp.x + t, wp.z + t}
|
||||
table.insert(triggers, {exactFillRootNode = node})
|
||||
end
|
||||
table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index,
|
||||
isa = function() return false end, getIsFillTypeAllowed = function() return true end})
|
||||
end
|
||||
g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end}
|
||||
g_currentMission.placeableSystem = {placeables = {}}
|
||||
-- køretøjer
|
||||
local vehicles = {}
|
||||
for index = 1, 40 do
|
||||
local node = #nodes + 1
|
||||
nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)}
|
||||
table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end})
|
||||
end
|
||||
g_currentMission.vehicleSystem = {vehicles = vehicles}
|
||||
local adEnv = {ADGraphManager = {
|
||||
getMapMarkers = function() return markers end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
getPathTo = function(_, _, id) return {{id = id}} end,
|
||||
}}
|
||||
|
||||
for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do
|
||||
local chunk = loadfile(dir .. "/" .. file)
|
||||
if chunk ~= nil then pcall(chunk) end
|
||||
end
|
||||
if ADCache ~= nil then ADCache.enabled = true end
|
||||
ADPrices = ADPrices or {}
|
||||
ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end
|
||||
|
||||
-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager)
|
||||
local function replan(vehicle)
|
||||
ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end)
|
||||
for _, marker in ipairs(markers) do
|
||||
ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1)
|
||||
end
|
||||
end
|
||||
|
||||
local t0 = os.clock()
|
||||
replan(vehicles[1])
|
||||
local single = (os.clock() - t0) * 1000
|
||||
|
||||
-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det.
|
||||
local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS)
|
||||
local timers = {}
|
||||
for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end
|
||||
local worst, steady, frames = 0, 0, 0
|
||||
for frame = 1, 60 * 60 do
|
||||
simTime = simTime + 16.7
|
||||
g_currentMission.time = simTime
|
||||
local started = os.clock()
|
||||
for index = 1, 15 do
|
||||
timers[index] = timers[index] + 16.7
|
||||
if timers[index] >= 5000 then
|
||||
timers[index] = 0
|
||||
replan(vehicles[index])
|
||||
end
|
||||
end
|
||||
local spent = (os.clock() - started) * 1000
|
||||
if spent > worst then worst = spent end
|
||||
if simTime > 12000 and spent > steady then steady = spent end
|
||||
if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end
|
||||
frames = frames + 1
|
||||
end
|
||||
print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " "))
|
||||
print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady))
|
||||
@@ -1,221 +0,0 @@
|
||||
# Behovsstyret forsyning Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Én AutoDrive-traktor holder alle input i en produktion (fx TMR-blander) fyldt ved selv at vælge vare + kilde-silo pr. tur.
|
||||
|
||||
**Architecture:** Den eksisterende wrapper om `ADMultipleTargetsManager.getNextPickup` udvides: ved >1 valgt vare i AD læses aflæsningsmålets lager, ingredienserne rangeres efter behov (rene funktioner i ny fil), og en kilde findes blandt alle AD-markører. AD's varevalg indsnævres til én vare pr. tur og gendannes når AD stopper.
|
||||
|
||||
**Tech Stack:** Lua 5.1 (FS25 mod-sandbox), luajit til tests med håndskrevne mocks, AutoDrive 3.0.1.2 via global `FS25_AutoDrive`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-21-behovsstyret-forsyning-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Rør aldrig `FS25_AutoDrive.zip`; al adgang via den globale `FS25_AutoDrive`-tabel.
|
||||
- Enhver fejl i spil-/AD-API → `Logging.warning` + AutoDrives originale `getNextPickup`. Aldrig crash.
|
||||
- Præcis én valgt vare → opførsel uændret; de 11 eksisterende tests forbliver grønne.
|
||||
- Markør↔station-tærskel: 40 m (`MAX_MARKER_TO_TRIGGER_DISTANCE`). `MIN_FREE_LITERS = 1000`.
|
||||
- Rene funktioner muterer ikke input (returnér nye tabeller).
|
||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`. Commits uden Co-Authored-By.
|
||||
- Verificerede AD-fakta: `selectedFillTypes` er et array-felt på stateModule og gemmes i savegame; `setFillType(ft)` bevarer valget hvis `ft` er i det; `isActive()`, `getSecondMarker()`, `raiseDirtyFlag()` findes; `ADGraphManager:getMapMarkers()` returnerer array indekseret på `markerIndex`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `adSupplyPlanner.lua` — rene funktioner
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adSupplyPlanner.lua`
|
||||
- Test: `tests/test_adSupplyPlanner.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (global tabel `ADSupplyPlanner`):
|
||||
- `MIN_FREE_LITERS` (number)
|
||||
- `scoreNeed(level, capacity, freeSpace) -> number` (højere = mere trængende)
|
||||
- `rankNeeds(needs) -> ranked` hvor `needs = {{fillType, level, capacity}, …}` og `ranked = {{fillType, level, capacity, freeSpace, score}, …}` sorteret mest trængende først, uden dem med `freeSpace < MIN_FREE_LITERS`
|
||||
- `resolveIngredients(state, selected) -> ingredients|nil` hvor `state = nil | {ingredients = {ft…}, narrowedTo = ft}`
|
||||
- `copyList(list) -> ny liste`
|
||||
|
||||
- [ ] **Step 1: Skriv fejlende test** — `tests/test_adSupplyPlanner.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||
|
||||
local HAY, GRASS, SILAGE = 20, 21, 22
|
||||
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
|
||||
|
||||
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
|
||||
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
|
||||
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
|
||||
|
||||
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
|
||||
local needs = {
|
||||
{fillType = HAY, level = 80000, capacity = 100000},
|
||||
{fillType = GRASS, level = 20000, capacity = 100000},
|
||||
{fillType = SILAGE, level = 99500, capacity = 100000},
|
||||
}
|
||||
local ranked = ADSupplyPlanner.rankNeeds(needs)
|
||||
check("fuld ingrediens springes over", #ranked, 2)
|
||||
check("mest trængende først", ranked[1].fillType, GRASS)
|
||||
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
|
||||
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
|
||||
check("input muteres ikke", needs[1].freeSpace, nil)
|
||||
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
|
||||
|
||||
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
|
||||
local tie = ADSupplyPlanner.rankNeeds({
|
||||
{fillType = SILAGE, level = 0, capacity = 5000},
|
||||
{fillType = HAY, level = 0, capacity = 5000},
|
||||
})
|
||||
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
|
||||
|
||||
-- resolveIngredients
|
||||
local multi = {HAY, GRASS}
|
||||
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
|
||||
check("multi-valg bliver ingrediensliste", #resolved, 2)
|
||||
check("listen er en kopi", resolved ~= multi, true)
|
||||
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
|
||||
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
|
||||
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
|
||||
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
|
||||
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
|
||||
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Kør og se den fejle** — `luajit tests/test_adSupplyPlanner.lua` → fejler med "cannot open FS25_ADSmartPickup/adSupplyPlanner.lua".
|
||||
|
||||
- [ ] **Step 3: Implementér** — `FS25_ADSmartPickup/adSupplyPlanner.lua`:
|
||||
|
||||
```lua
|
||||
-- AD Supply Planner
|
||||
-- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens
|
||||
-- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet.
|
||||
|
||||
ADSupplyPlanner = {}
|
||||
|
||||
-- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af).
|
||||
ADSupplyPlanner.MIN_FREE_LITERS = 1000
|
||||
|
||||
function ADSupplyPlanner.copyList(list)
|
||||
local copy = {}
|
||||
for index, value in ipairs(list) do
|
||||
copy[index] = value
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Hvor meget trænger målet til denne ingrediens? Højere = hentes først.
|
||||
-- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1),
|
||||
-- så en lille næsten-tom tank vinder over en stor halvfuld.
|
||||
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)
|
||||
if capacity <= 0 then
|
||||
return 0
|
||||
end
|
||||
return freeSpace / capacity
|
||||
end
|
||||
|
||||
-- needs: {{fillType, level, capacity}, ...}. Returnerer NY liste, mest trængende først;
|
||||
-- ingredienser uden plads i målet er sorteret fra.
|
||||
function ADSupplyPlanner.rankNeeds(needs)
|
||||
local ranked = {}
|
||||
for _, need in ipairs(needs) do
|
||||
local freeSpace = need.capacity - need.level
|
||||
if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then
|
||||
table.insert(ranked, {
|
||||
fillType = need.fillType,
|
||||
level = need.level,
|
||||
capacity = need.capacity,
|
||||
freeSpace = freeSpace,
|
||||
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace),
|
||||
})
|
||||
end
|
||||
end
|
||||
table.sort(ranked, function(a, b)
|
||||
if a.score ~= b.score then
|
||||
return a.score > b.score
|
||||
end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
return ranked
|
||||
end
|
||||
|
||||
-- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg.
|
||||
-- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste.
|
||||
-- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra).
|
||||
function ADSupplyPlanner.resolveIngredients(state, selected)
|
||||
if type(selected) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
if #selected > 1 then
|
||||
return ADSupplyPlanner.copyList(selected)
|
||||
end
|
||||
if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then
|
||||
return ADSupplyPlanner.copyList(state.ingredients)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Kør tests** — `luajit tests/test_adSupplyPlanner.lua` → `ALLE TESTS OK`.
|
||||
|
||||
- [ ] **Step 5: Commit** — `git add FS25_ADSmartPickup/adSupplyPlanner.lua tests/test_adSupplyPlanner.lua && git commit -m "feat: ADSupplyPlanner — behovsrangering og ingrediensliste-tilstand"`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Integration i `adSmartPickup.lua`
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (hele filen omstruktureres let; se kode)
|
||||
- Test: `tests/test_adSmartPickup.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADSupplyPlanner.rankNeeds`, `.resolveIngredients`, `.copyList` (Task 1).
|
||||
- Produces: `ADSmartPickup.choosePickup(adEnv, driver) -> choice|nil` med `choice = {markerIndex, markerName, group, level, fillType?, need?}`; `ADSmartPickup.findLeastFilledPickup` returnerer nu samme `choice`-tabel (uden `fillType`).
|
||||
|
||||
- [ ] **Step 1: Udvid test-mocks og skriv fejlende tests** i `tests/test_adSmartPickup.lua`:
|
||||
- `dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")` før `adSmartPickup.lua`.
|
||||
- `unloadStations`-liste + `getUnloadingStations` i `g_currentMission.storageSystem`; nulstilles i `resetWorld()`.
|
||||
- `getMapMarkers = function() return markers end` i `ADGraphManager`-mock.
|
||||
- `getSecondMarker = function() return nil end` i `makeDriver` (rigtig AD har den altid).
|
||||
- Nye helpers `makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken)` og `makeSupplyDriver(firstMarker, secondMarker, fillTypes)` (muterbar stateModule-mock med `setFillType`, `raiseDirtyFlag`, `isActive`).
|
||||
- Tests S1–S9 (se Step 1-koden i implementeringen; dækker spec'ens testliste).
|
||||
|
||||
- [ ] **Step 2: Kør og se de nye fejle** — `luajit tests/test_adSmartPickup.lua` → S-tests FAIL, de 11 gamle OK.
|
||||
|
||||
- [ ] **Step 3: Implementér** — i `adSmartPickup.lua`:
|
||||
- `supplyState = setmetatable({}, {__mode = "k"})` (køretøj → `{ingredients, narrowedTo}`).
|
||||
- `getDistanceToTriggers(triggers, nodeFields, x, z)` + `getClosestStation(stations, triggersField, nodeFields, wayPoint, farmId)`; `getLoadStationAtMarker` (felt `loadTriggers`, node `triggerNode`) og `getUnloadStationAtMarker` (felt `unloadTriggers`, noder `exactFillRootNode`, `triggerNode`, `aiNode`).
|
||||
- `collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)` → `{{marker, station}, …}`; `findLeastFilled(markerStations, fillTypes, farmId)` → `choice|nil` (eksisterende eget-lager-først-rangering).
|
||||
- `getTargetNeeds(station, ingredients, farmId)`: pr. understøttet ingrediens summeres `storage:getFillLevel(ft)` og `storage:getFreeCapacity(ft)` over `station.targetStorages`; `capacity = level + free`. Ingen storages → `station:getFreeCapacity(ft, farmId)` som `capacity`, `level = 0`.
|
||||
- `ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)`, `applySupplyChoice(driver, ingredients, choice)` (`setFillType`, `selectedFillTypes = {ft}`, `raiseDirtyFlag`, gem tilstand), `ADSmartPickup.choosePickup` (supply → ellers mappe-logik → ellers nil).
|
||||
- Hook bruger `choice.group` til AD's rotations-bogføring og logger vare + behov ved supply-valg.
|
||||
- `restoreStoppedVehicles()` kaldt fra `update` (pcall): når `not stateModule:isActive()` og valget stadig er `{narrowedTo}` → `selectedFillTypes = kopi af ingredients` + dirty-flag; tilstand slettes.
|
||||
|
||||
- [ ] **Step 4: Kør alle tests** — `luajit tests/test_adSupplyPlanner.lua && luajit tests/test_adSmartPickup.lua` → begge `ALLE TESTS OK`.
|
||||
|
||||
- [ ] **Step 5: Commit** — `git add -A && git commit -m "feat: behovsstyret forsyning — én traktor holder alle input fyldt"`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Pakning, docs, deploy
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (version `1.1.0.0`, `<sourceFile filename="adSupplyPlanner.lua"/>` FØR `adSmartPickup.lua`, beskrivelse), `README.md`
|
||||
|
||||
- [ ] **Step 1:** Opdatér modDesc + README (betjening: multi-vælg varer; begrænsninger fra spec).
|
||||
- [ ] **Step 2:** Byg: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cd ..`; verificér med `unzip -l` at begge .lua + modDesc + icon er med.
|
||||
- [ ] **Step 3:** Deploy atomisk (spillet kan køre): `cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp && mv ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp ~/FS25-data/mods/FS25_ADSmartPickup.zip`.
|
||||
- [ ] **Step 4:** Code-review (ecc:code-reviewer) af diff; ret CRITICAL/HIGH.
|
||||
- [ ] **Step 5:** Commit + `git push gitea`. Opdatér memory + Obsidian-noten `projekter/FS25 silo-patches + AD Smart Pickup.md`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
||||
# Flow-tavle — implementeringsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Én tavle med ét flow pr. række (Vare · Fra · Til · Regel · Traktorer · Status); Fra/Til vælges som bygninger, markører findes automatisk. Faner: Flows · Lagre.
|
||||
|
||||
**Architecture:** Ny spil-adapter `adBuildings.lua` (bygninger + markør-opslag + cache). Modellen får bygnings-id-lister; controlleren udleder waypoint-lister fra dem (`resolveMarkers`) så planlægger/hooks er uændrede. GUI omskrives: `SmartPickupFrame.lua` (faner, tavle, fælles), `SmartPickupFlowTab.lua` (detail-blok), `SmartPickupStorageTab.lua` (uændret).
|
||||
|
||||
**Tech Stack:** FS25 Lua, AutoDrive-globaler via `FS25_AutoDrive`, luajit-tests.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-23-flow-tavle-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Motoren (adOutbound, adOutboundPlanner, adSupplyPlanner, adUnloadWait, adSmartPickup-hooks) røres ikke.
|
||||
- Byg kun når alle tests er grønne (`for t in tests/test_*.lua; do luajit $t|tail -1; done`), derefter zip → `~/FS25-data/mods/` → commit → push gitea main.
|
||||
- Version 1.13.0.0. XML-version 3; v1/v2 læses.
|
||||
- Wait-markører er aldrig bygningsmarkører. Siloers læssemarkør = strengt opslag; alt andet = nærmeste ≤ 40 m.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Run med bygning der er solgt siden sidst: rækken viser "(mangler)", Start afviser med noMarker, intet crash. (Task 3 test)
|
||||
2. v2-fil med markør der ikke kan oversættes til en bygning: markøren beholdes rå og virker som før. (Task 2 test)
|
||||
3. To bygninger med markører inden for 40 m af hinanden (silo-rækker): hver silo får sin egen markør (strengt opslag). (Task 1 test)
|
||||
4. Forsynings-run: Fra vises som "alle siloer" og sources-listen røres ikke. (Task 3 test)
|
||||
5. Fanen rebuild'es mens et run kører: Start/Stop-knappen og status følger `isRunning`. (in-game)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: adBuildings
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adBuildings.lua`; Test `tests/test_adBuildings.lua`.
|
||||
|
||||
**Produces:**
|
||||
```lua
|
||||
ADBuildings.MAX_MARKER_DISTANCE = 40; ADBuildings.CACHE_MS = 1000
|
||||
ADBuildings.list(adEnv, farmId) -- {building...} sorteret kind, navn (cachet)
|
||||
ADBuildings.find(adEnv, farmId, id) -- building | nil
|
||||
ADBuildings.byMarker(adEnv, farmId, wayPointId, role) -- role "load"|"unload": building hvis markøren er dens markør, ellers nil
|
||||
ADBuildings.getId(placeable) -- uniqueId eller "pos:x:z"
|
||||
ADBuildings.invalidate()
|
||||
building = {id, name, kind, placeable, loadMarker={id,name,distance}|nil, unloadMarker=…|nil, canBeSource, canBeTarget, canSell}
|
||||
```
|
||||
Kilder til bygninger: `g_currentMission.placeableSystem.placeables` (ejet af farm eller sell) — kind: factory (spec_productionPoint), silo (spec_silo), husbandry (getHusbandryFillLevel), objectStorage (spec_objectStorage), sell (ADPrices.isSellingStation på placeable.spec_sellingPoint / unloadingStation via storageSystem). Læssetriggere: `productionPoint.loadingStation.loadTriggers`, `spec_silo.loadingStation.loadTriggers`, husbandry: alle loadingStations i storageSystem med owningPlaceable == placeable. Aflæsningstriggere: unloadingStations med owningPlaceable == placeable. Objektlager/pallespawner: rootNode/spawnPlaces[1].startNode.
|
||||
Markørvalg: nærmeste markør (ikke Wait) til en trigger-node ≤ 40 m; for silo-load: kun markører hvor `ADSmartPickup.getLoadStationAtWayPointId(marker.id)` == siloens station.
|
||||
|
||||
- [ ] Tests B1–B7 (kind, markører, silo strengt, Wait udelukket, byMarker, id-fallback, cache-invalidering) → RED → implementér → GREEN → commit `feat: adBuildings`.
|
||||
|
||||
### Task 2: Model v3
|
||||
|
||||
**Files:** Modify `adRuns.lua`, `adRunsStorage.lua`; Test `tests/test_adRuns.lua`.
|
||||
|
||||
**Produces:** run-felter `sourceBuildingIds`, `targetBuildingIds` (lister af strenge); `ADRuns.toggleSourceBuilding/toggleTargetBuilding(store, runId, id)`; XML v3 `#sourceBuildings`/`#targetBuildings` (mellemrumsadskilte). `ADRuns.XML_VERSION = 3`. Migration sker i controlleren (Task 3), ikke i modellen.
|
||||
|
||||
- [ ] Tests R-v3 → RED → implementér → GREEN → commit.
|
||||
|
||||
### Task 3: Controller — resolve + migration + validering
|
||||
|
||||
**Files:** Modify `adRunsController.lua`; Test `tests/test_adRunsController.lua`.
|
||||
|
||||
**Produces:**
|
||||
```lua
|
||||
ADRunsController.resolveMarkers(adEnv, run) -- udfylder run.sourceWayPointIds (outbound) / targetWayPointIds (supply) fra bygninger; rå markører (uden bygning) bevares; returnerer {missing = {buildingName...}}
|
||||
ADRunsController.migrateBuildings(adEnv) -- ved load: markører → bygnings-id via byMarker; logger
|
||||
ADRunsController.getFlowRow(adEnv, run) -- {product, from, to, rule, vehicles, status, warning} tekster til tavlen
|
||||
validateVehicle: ny grund "noMarker" når en valgt bygning mangler markør
|
||||
```
|
||||
Start/plan: `startRun` og `ADOutbound.buildPlanInput` kalder resolveMarkers først (buildPlanInput via ADRunsController.getRunForVehicle → resolve i getRunForVehicle? nej: i `ADSmartPickup.choosePickup`/`ADOutbound.choosePickup` kaldes `ADRunsController.resolveMarkers(adEnv, run)` én gang pr. plan).
|
||||
|
||||
- [ ] Tests C-flow → RED → implementér → GREEN → commit.
|
||||
|
||||
### Task 4: GUI — Flows-fane
|
||||
|
||||
**Files:** Rewrite `gui/SmartPickupFrame.lua` (faner Flows/Lagre, tavle, fælles rækker, knapper), Create `gui/SmartPickupFlowTab.lua` (detail-blok, callbacks), delete `SmartPickupSupplyTab.lua`/`SmartPickupOutboundTab.lua` (indhold flyttes), Modify `gui/SmartPickupFrame.xml` (statiske rækker: runOption "Flow", waitPool, loops, returnBelow, sellAt, storeAbove; prefabs som i dag), l10n da/en (`spu_tab_flows`, `spu_flow_*`, `spu_direction_in/out`, `spu_from_allSilos`, `spu_noMarker`, `spu_reason_noMarker`, `spu_section_*`), `adSmartPickup.lua` (source()).
|
||||
|
||||
- [ ] Implementér; syntaks- og definitions-tjek; in-game.
|
||||
|
||||
### Task 5: README, version, byg, deploy, push, memory.
|
||||
@@ -1,569 +0,0 @@
|
||||
# Udkørsel af produkter — implementeringsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Runs med retning "Udkørsel": traktorer henter fabrikkers output og lagervarer (bulk og paller, også fra objektlagre) og kører dem til silo eller salgssted efter politik pr. vare, pristærskel og lagertærskel.
|
||||
|
||||
**Architecture:** Ren planlægger (`adOutboundPlanner`) + rene prisregler (`adPrices`) uden spil-API; spiladaptere i `adSources`/`adPrices`; AutoDrive-integration i `adOutbound` via to hooks (næste pålæsning + læsseopgaven), samme mønster som `adUnloadWait`. Wait-puljen udskilles i `adWaitPool` og deles. GUI får tre faner (Oversigt · Forsyning · Udkørsel) med fælles byggeklodser.
|
||||
|
||||
**Tech Stack:** FS25 Lua (GIANTS-sandbox), AutoDrive 3.x via `FS25_AutoDrive`-globalen, Universal Autoload via AD's AL-lag, luajit-tests uden spil.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-23-udkoersel-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Alle spil-API-kald i hooks i `pcall`; fejl → hold ved Wait + WARN én gang, aldrig AutoDrive-stop.
|
||||
- Modellen (`adRuns`) muterer aldrig: ny store ved hver ændring.
|
||||
- Log-præfiks `ADSmartPickup: ` i alle moduler; alle run-ændringer logges.
|
||||
- XML-version 2; version 1-filer læses som `kind="supply"`.
|
||||
- Standarder: `sellAtPercent` 90 (50–100), `storeAbovePercent` 80 (50–95), `collectRound` false.
|
||||
- Politikker: `store` · `sell` · `sellElseStore` (+ "off" = varen udelades af `products`).
|
||||
- Kildetyper: fabrik (produktionspunkt), silo (`spec_silo`), objektlager (`spec_objectStorage`), genkendt inden for 40 m af markøren.
|
||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`; alle eksisterende tests skal blive grønne.
|
||||
- Version bump til 1.12.0.0 i `modDesc.xml`; byg `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy til `~/FS25-data/mods/`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Traktor med UAL-vogn står ved et objektlager hvis spawn sidder fast (`objectSpawn.isActive` for evigt): forventet: 30 s uden fremdrift → læsningen afsluttes med det der er, ingen palle efterlades ud over den ene der evt. er spawnet. (Task 6 test O5)
|
||||
2. Bedst betalende salgssted er uopnåeligt, næstbedste kan nås: forventet: næstbedste vælges uden advarsel; kun når intet kan nås, vises rød advarsel én gang. (Task 4 test P6 + Task 6 test O7)
|
||||
3. Vare hvis salgsstation ikke findes på kortet, politik `sell`: forventet: varen står som "intet salgssted" i UI og planlæggeren springer den over uden fejl. (Task 4 test P7)
|
||||
4. Version 1-runs-fil (uden `kind`) indlæses: forventet: alle runs bliver Forsyning, intet går tabt, næste gem skriver version 2. (Task 1 test R-v2)
|
||||
5. Bulk-vogn med opsamlingsrunde og to kilder hvor den første giver fuldt læs: forventet: der køres direkte til målet, kilde 2 besøges ikke. (Task 6 test O6)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Model + lagring v2
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adRuns.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adRunsStorage.lua`
|
||||
- Test: `tests/test_adRuns.lua` (tilføj sektion R-v2)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ADRuns.KINDS = {supply=true, outbound=true}`, `ADRuns.POLICIES = {store=true, sell=true, sellElseStore=true}`, `ADRuns.DEFAULT_SELL_AT_PERCENT=90`, `ADRuns.DEFAULT_STORE_ABOVE_PERCENT=80`, `ADRuns.createRun(store, name, kind)`, run-felter `kind, sourceWayPointIds, products={{fillType="SUGAR", policy="sell"}}, sellAtPercent, storeAbovePercent, collectRound`, `ADRuns.toggleSource(store, runId, wayPointId)`, `ADRuns.setProductPolicy(store, runId, fillTypeName, policy)` (policy "off" fjerner), `ADRuns.getProductPolicy(run, fillTypeName)` → policy eller "off", `ADRuns.serializeProducts/parseProducts`, `ADRuns.XML_VERSION = 2`.
|
||||
|
||||
- [ ] **Step 1: Skriv fejlende tests** — tilføj nederst i `tests/test_adRuns.lua` før den afsluttende opsummering:
|
||||
|
||||
```lua
|
||||
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
|
||||
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
|
||||
check("Rv2 kind", out.kind, "outbound")
|
||||
check("Rv2 supply som standard", ADRuns.createRun(ADRuns.newStore(), "x").kind == nil and "nil" or select(2, ADRuns.createRun(ADRuns.newStore(), "x")).kind, "supply")
|
||||
check("Rv2 sellAt 90", out.sellAtPercent, 90)
|
||||
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
|
||||
check("Rv2 runde fra", out.collectRound, false)
|
||||
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
|
||||
o2 = ADRuns.toggleSource(o2, out.id, 12386)
|
||||
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
|
||||
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
|
||||
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
|
||||
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
|
||||
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
|
||||
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
|
||||
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
|
||||
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
|
||||
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
|
||||
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
|
||||
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
|
||||
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
|
||||
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
|
||||
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
|
||||
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
|
||||
check("Rv2 parse antal", #parsed, 2)
|
||||
check("Rv2 parse politik", parsed[2].policy, "store")
|
||||
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
|
||||
local rtRun = ADRuns.getRun(rt, out.id)
|
||||
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
|
||||
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
|
||||
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
|
||||
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
|
||||
check("Rv2 roundtrip runde", rtRun.collectRound, true)
|
||||
-- v1-post uden kind/sources/products -> supply med standarder
|
||||
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
|
||||
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
|
||||
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
|
||||
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Kør testen** — `luajit tests/test_adRuns.lua` → FAIL (`toggleSource` nil).
|
||||
|
||||
- [ ] **Step 3: Implementér i `adRuns.lua`**:
|
||||
|
||||
```lua
|
||||
ADRuns.XML_VERSION = 2
|
||||
ADRuns.KINDS = {supply = true, outbound = true}
|
||||
ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true}
|
||||
ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100
|
||||
ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95
|
||||
|
||||
local function copyProducts(products)
|
||||
local copy = {}
|
||||
for index, product in ipairs(products or {}) do
|
||||
copy[index] = {fillType = product.fillType, policy = product.policy}
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function clampRange(value, default, min, max)
|
||||
local number = tonumber(value) or default
|
||||
return math.max(min, math.min(max, math.floor(number)))
|
||||
end
|
||||
```
|
||||
`copyRun` får `kind, sourceWayPointIds = copyList(...), products = copyProducts(...), sellAtPercent, storeAbovePercent, collectRound`. `createRun(store, name, kind)` sætter `kind = ADRuns.KINDS[kind] and kind or "supply"`, `sourceWayPointIds = {}`, `products = {}`, `sellAtPercent = 90`, `storeAbovePercent = 80`, `collectRound = false`. `updateRun` håndterer `changes.kind` (kun gyldige), `sourceWayPointIds` (copyList), `products` (copyProducts), `sellAtPercent`/`storeAbovePercent` (clampRange), `collectRound` (`== true`).
|
||||
|
||||
```lua
|
||||
function ADRuns.toggleSource(store, runId, wayPointId)
|
||||
local run = ADRuns.getRun(store, runId)
|
||||
if run == nil then return copyStore(store) end
|
||||
local sources, found = {}, false
|
||||
for _, id in ipairs(run.sourceWayPointIds) do
|
||||
if id == wayPointId then found = true else table.insert(sources, id) end
|
||||
end
|
||||
if not found then table.insert(sources, wayPointId) end
|
||||
return ADRuns.updateRun(store, runId, {sourceWayPointIds = sources})
|
||||
end
|
||||
|
||||
function ADRuns.getProductPolicy(run, fillTypeName)
|
||||
for _, product in ipairs(run.products or {}) do
|
||||
if product.fillType == fillTypeName then return product.policy end
|
||||
end
|
||||
return "off"
|
||||
end
|
||||
|
||||
-- policy "off" fjerner varen; ukendt politik ignoreres (store uændret).
|
||||
function ADRuns.setProductPolicy(store, runId, fillTypeName, policy)
|
||||
local run = ADRuns.getRun(store, runId)
|
||||
if run == nil or (policy ~= "off" and not ADRuns.POLICIES[policy]) then return copyStore(store) end
|
||||
local products = {}
|
||||
for _, product in ipairs(run.products) do
|
||||
if product.fillType ~= fillTypeName then table.insert(products, product) end
|
||||
end
|
||||
if policy ~= "off" then table.insert(products, {fillType = fillTypeName, policy = policy}) end
|
||||
return ADRuns.updateRun(store, runId, {products = products})
|
||||
end
|
||||
|
||||
function ADRuns.serializeProducts(products)
|
||||
local parts = {}
|
||||
for _, product in ipairs(products or {}) do table.insert(parts, product.fillType .. ":" .. product.policy) end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
function ADRuns.parseProducts(text)
|
||||
local products = {}
|
||||
for token in string.gmatch(tostring(text or ""), "%S+") do
|
||||
local name, policy = string.match(token, "^([^:]+):(%a+)$")
|
||||
if name ~= nil and ADRuns.POLICIES[policy] then table.insert(products, {fillType = name, policy = policy}) end
|
||||
end
|
||||
return products
|
||||
end
|
||||
```
|
||||
`toRecords` tilføjer `kind, sources = table.concat(run.sourceWayPointIds, " "), products = serializeProducts, sellAt, storeAbove, collectRound = run.collectRound and 1 or 0`. `fromRecords` læser dem med standarder (`kind` ugyldig/nil → "supply"; `collectRound = tonumber(record.collectRound) == 1`).
|
||||
|
||||
`adRunsStorage.lua`: `writeRecords` skriver `#kind` (string), `#sources`, `#products`, `#sellAt` (int), `#storeAbove` (int), `#collectRound` (int); `readRecords` læser dem (`getString`/`getInt`; manglende attribut → nil → standard i fromRecords).
|
||||
|
||||
- [ ] **Step 4: Kør** `luajit tests/test_adRuns.lua && luajit tests/test_adRunsController.lua` → alle OK.
|
||||
- [ ] **Step 5: Commit** `feat: run-model v2 — retning, kilder, varepolitik, tærskler, runde (XML v2 + v1-migration)`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: adWaitPool udskilt fra adUnloadWait
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adWaitPool.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adUnloadWait.lua`, `adLoadSwap.lua` (excludedPlaceables), `modDesc.xml` (sourceFile før adUnloadWait)
|
||||
- Test: `tests/test_adWaitPool.lua` (ny), eksisterende `tests/test_adSmartPickup.lua` skal stadig køre
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADWaitPool.getCandidates(adEnv, poolGroup) -- {{marker, x, z}}
|
||||
ADWaitPool.claim(markerId, vehicle) / ADWaitPool.releaseVehicle(vehicle) / ADWaitPool.getOwner(markerId)
|
||||
ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable) -- candidate eller nil (nærmeste ledige fra vognens position)
|
||||
ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId) / ADWaitPool.hasRouteBothWays(adEnv, vehicle, toId, homeId)
|
||||
ADWaitPool.redirect(task, destinationID) -- stopper AD's trigger-aktivitet, sætter task.destinationID/state/setPathTo
|
||||
ADWaitPool.getClaims() -- {{marker, vehicle}} sorteret (til GUI)
|
||||
ADWaitPool.STATE_DRIVING -- sættes af adUnloadWait.install (task.STATE_DRIVING or 2)
|
||||
ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables) -- 7. param: {[placeable]=true} udelukkes
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Test** `tests/test_adWaitPool.lua`: mock `FS25_AutoDrive.ADGraphManager` (markers i mapperne "Cow Wait" og "silo", waypoints, `getPathTo` der returnerer `{ {id=target} }` for nåelige), `ADSupplyPlanner` via `dofile`. Tjek: (W1) `getCandidates(adEnv, "Cow Wait")` giver kun Wait-markører i den mappe, tom pulje = alle Wait-mapper; (W2) `pickFree` returnerer nærmeste ledige, springer reserverede over, returnerer eget reserverede; (W3) `claim/releaseVehicle/getOwner`; (W4) `pickFree` med `isReachable` der afviser nærmeste → næste; (W5) `getClaims` sorteret på mappe.
|
||||
- [ ] **Step 2: Kør** → FAIL (fil mangler).
|
||||
- [ ] **Step 3: Implementér** `adWaitPool.lua` ved at flytte `getWaitCandidates`, `claims`, `pathReaches`, `hasRouteTo`, `hasRouteBothWays`, `redirect`, `getClaims`-sorteringen fra `adUnloadWait.lua` (samme kode, offentlige navne som ovenfor; `claims` er `setmetatable({}, {__mode="v"})`; `getClaims` itererer `claims` direkte: `{marker = ADGraphManager:getMapMarkerByWayPointId(id), vehicle}`). `adUnloadWait.lua` kalder dem (`claims[...]` → `ADWaitPool.claim/getOwner/releaseVehicle`; `pickNearestFreeWaitPoint(getWaitCandidates(...), claims, ...)` → `ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, pool), ADWaitPool.claims, x, z, vehicle, isReachable)`); `ADUnloadWait.getClaims/hasRouteTo/hasRouteBothWays` bliver delegater. `ADLoadSwap.findDumpMarker`: `if excludedPlaceables ~= nil and excludedPlaceables[station.owningPlaceable] then skip`.
|
||||
- [ ] **Step 4: Kør** alle tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` → grønne.
|
||||
- [ ] **Step 5: Commit** `refactor: Wait-puljen udskilt i adWaitPool (reservationer, rutetjek, redirect) til deling med udkørsel`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: adPrices — toppris og pris-%
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adPrices.lua`
|
||||
- Test: `tests/test_adPrices.lua`
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADPrices.maxFactor(factors) -- højeste værdi i {[period]=factor}; 1 uden faktorer
|
||||
ADPrices.topPrice(baseNow, factorNow, maxFactor) -- baseNow * maxFactor / factorNow; baseNow når factorNow <= 0
|
||||
ADPrices.percentOfTop(price, top) -- afrundet heltal; 0 når top <= 0
|
||||
ADPrices.isSellingStation(station) -- SellingStation, isSellingPoint, ikke hideFromPricesMenu
|
||||
ADPrices.getQuote(station, fillType) -- {price, top, percent} eller nil (spil-API i pcall)
|
||||
ADPrices.bestQuote(quotes) -- højeste percent, dernæst price; nil ved tom
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Test** (P1) `maxFactor({EARLY_SPRING=0.9, LATE_AUTUMN=1.2})` = 1.2; (P2) `topPrice(500, 1.0, 1.25)` = 625; `topPrice(500, 0, 1.25)` = 500; (P3) `percentOfTop(470, 625)` = 75; `percentOfTop(700, 625)` = 112 (stor efterspørgsel > 100); `percentOfTop(1, 0)` = 0; (P4) `getQuote` med fake station `{fillTypePrices={[5]=500}, getEffectiveFillTypePrice=function(_,ft) return 470 end, isa=function() return true end, isSellingPoint=true}` og `g_fillTypeManager.getFillTypeByIndex` → `{economy={factors={A=1.0,B=1.25}}, pricePerLiter=400}`, `g_currentMission.environment.currentPeriod="A"` → percent 75; (P5) uden `fillTypePrices` bruges `pricePerLiter × factorNow × economyManager:getPriceMultiplier()` (1.0) → top 500, percent 94 (470/500); (P6) `bestQuote` vælger højeste percent.
|
||||
- [ ] **Step 2: Kør** → FAIL.
|
||||
- [ ] **Step 3: Implementér**:
|
||||
|
||||
```lua
|
||||
ADPrices = {}
|
||||
ADPrices.LOG_PREFIX = "ADSmartPickup: "
|
||||
|
||||
function ADPrices.maxFactor(factors)
|
||||
local best = nil
|
||||
for _, factor in pairs(factors or {}) do
|
||||
if type(factor) == "number" and (best == nil or factor > best) then best = factor end
|
||||
end
|
||||
return best or 1
|
||||
end
|
||||
|
||||
function ADPrices.topPrice(baseNow, factorNow, maxFactor)
|
||||
if baseNow == nil or baseNow <= 0 then return 0 end
|
||||
if factorNow == nil or factorNow <= 0 or maxFactor == nil or maxFactor <= 0 then return baseNow end
|
||||
return baseNow * maxFactor / factorNow
|
||||
end
|
||||
|
||||
function ADPrices.percentOfTop(price, top)
|
||||
if top == nil or top <= 0 or price == nil then return 0 end
|
||||
return math.floor(100 * price / top + 0.5)
|
||||
end
|
||||
|
||||
function ADPrices.isSellingStation(station)
|
||||
return station ~= nil and station.isa ~= nil and SellingStation ~= nil and station:isa(SellingStation)
|
||||
and station.isSellingPoint == true and not station.hideFromPricesMenu
|
||||
end
|
||||
|
||||
local function getCurrentPeriod()
|
||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||
return environment ~= nil and environment.currentPeriod or nil
|
||||
end
|
||||
|
||||
function ADPrices.getQuote(station, fillType)
|
||||
local ok, effective = pcall(station.getEffectiveFillTypePrice, station, fillType)
|
||||
if not ok or type(effective) ~= "number" then return nil end
|
||||
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||||
local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
|
||||
local period = getCurrentPeriod()
|
||||
local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
|
||||
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
|
||||
if baseNow == nil or baseNow <= 0 then
|
||||
local multiplier = 1
|
||||
local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil
|
||||
if economy ~= nil and economy.getPriceMultiplier ~= nil then
|
||||
local okMult, value = pcall(economy.getPriceMultiplier, economy)
|
||||
if okMult and type(value) == "number" then multiplier = value end
|
||||
end
|
||||
baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * multiplier
|
||||
end
|
||||
local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors))
|
||||
return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
|
||||
end
|
||||
|
||||
function ADPrices.bestQuote(quotes)
|
||||
local best = nil
|
||||
for _, quote in ipairs(quotes or {}) do
|
||||
if best == nil or quote.percent > best.percent or (quote.percent == best.percent and quote.price > best.price) then best = quote end
|
||||
end
|
||||
return best
|
||||
end
|
||||
```
|
||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adPrices — toppris fra årets prisforløb og pris-% pr. salgssted`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: adOutboundPlanner (ren)
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adOutboundPlanner.lua`
|
||||
- Test: `tests/test_adOutboundPlanner.lua`
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
-- input:
|
||||
-- { capacity = liter, hasAutoload = bool, sellAtPercent, storeAbovePercent, collectRound,
|
||||
-- products = { { fillType = idx, name = "SUGAR", policy = "sell"|"store"|"sellElseStore", isPallet = bool,
|
||||
-- sources = { {wayPointId, available, level, capacity} ... }, -- kun kilder med available > 0
|
||||
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted
|
||||
-- store = {wayPointId, name, free} | nil } ... } }
|
||||
-- output: trip | nil, reasons
|
||||
-- trip = { kind = "sell"|"store", fillTypes = {idx...}, names = {...}, sourceIds = {wayPointId...}, targetWayPointId, targetName, percent|nil }
|
||||
-- reasons = { "SUGAR 78 % af top (kræver 90), lager 41 %", ... }
|
||||
ADOutboundPlanner.plan(input)
|
||||
ADOutboundPlanner.evaluate(product, input) -- {available, fullLoad, storageHigh, storagePercent, priceOk, percent}
|
||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Tests** (`local P = ADOutboundPlanner` efter `dofile`):
|
||||
- P1 salg ved pris over tærskel, uanset mængde: SUGAR sell, sources {{1, 5000, ...}}, quote 92, capacity 20000 → trip.kind "sell", targetWayPointId = quote.wayPointId, sourceIds {1}.
|
||||
- P2 pris under tærskel, lager lavt → nil, reasons[1] = "SUGAR 78 % af top (kræver 90), lager 41 %".
|
||||
- P3 sellElseStore, pris lav, lager 85 % → "store" til store.wayPointId.
|
||||
- P4 store med fuldt læs (available ≥ capacity) og lager 20 % → store.
|
||||
- P5 rangering: to sell-kandidater 95 % og 91 % → første vælges (højeste %).
|
||||
- P6 quote nil (intet nåeligt salgssted), policy sell, lager højt → nil, reason "SUGAR: intet nåeligt salgssted".
|
||||
- P7 store nil ved store-behov → nil, reason "FORAGE: ingen silo med plads".
|
||||
- P8 runde bulk (collectRound, hasAutoload=false): SUGAR i kilde A (3000) og B (9000), capacity 10000 → sourceIds {B, A} (mest først), ét fillType.
|
||||
- P9 runde UAL: STRAW_BASKET (sell 95, station S) + STRAW_CRATE (sell 93, station S) + STRAW_BROOM (sell 96, station T) → fillTypes {BROOM} kun (højest, T) — og med BROOM fjernet: {BASKET, CRATE} samme station S.
|
||||
- P10 uden runde: kun første fillType og kun én kilde (den med mest).
|
||||
- P11 policy off/ingen sources → nil, reasons tom for varer uden kilder ("SUGAR: intet på lager").
|
||||
- [ ] **Step 2: Kør** → FAIL. **Step 3: Implementér**:
|
||||
|
||||
```lua
|
||||
ADOutboundPlanner = {}
|
||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
||||
|
||||
local function sumAvailable(product)
|
||||
local total, high, percent = 0, false, 0
|
||||
for _, source in ipairs(product.sources or {}) do
|
||||
total = total + (source.available or 0)
|
||||
if source.capacity ~= nil and source.capacity > 0 then
|
||||
local p = 100 * (source.level or 0) / source.capacity
|
||||
percent = math.max(percent, p)
|
||||
end
|
||||
end
|
||||
return total, percent
|
||||
end
|
||||
|
||||
function ADOutboundPlanner.evaluate(product, input)
|
||||
local available, storagePercent = sumAvailable(product)
|
||||
local percent = product.quote ~= nil and product.quote.percent or nil
|
||||
return {
|
||||
available = available,
|
||||
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
|
||||
storagePercent = storagePercent,
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
}
|
||||
end
|
||||
|
||||
local function wantsSell(policy) return policy == "sell" or policy == "sellElseStore" end
|
||||
local function wantsStore(policy) return policy == "store" or policy == "sellElseStore" end
|
||||
|
||||
local function reasonFor(product, e, input)
|
||||
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then return product.name .. ": intet på lager" end
|
||||
if wantsSell(product.policy) and product.quote == nil and not (wantsStore(product.policy) and product.store ~= nil) then
|
||||
return product.name .. ": intet nåeligt salgssted"
|
||||
end
|
||||
if wantsStore(product.policy) and product.store == nil and (e.fullLoad or e.storageHigh) and not e.priceOk then
|
||||
return product.name .. ": ingen silo med plads"
|
||||
end
|
||||
local parts = {}
|
||||
if wantsSell(product.policy) and e.percent ~= nil then
|
||||
table.insert(parts, string.format("%d %% af top (kræver %d)", e.percent, input.sellAtPercent or 90))
|
||||
end
|
||||
table.insert(parts, string.format("lager %d %%", math.floor(e.storagePercent + 0.5)))
|
||||
return product.name .. " " .. table.concat(parts, ", ")
|
||||
end
|
||||
|
||||
-- kandidater: {product, e, kind, targetWayPointId, targetName, score}
|
||||
local function collectCandidates(input)
|
||||
local sell, store, reasons = {}, {}, {}
|
||||
for _, product in ipairs(input.products or {}) do
|
||||
local e = ADOutboundPlanner.evaluate(product, input)
|
||||
local added = false
|
||||
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
|
||||
if wantsSell(product.policy) and e.priceOk and product.quote ~= nil then
|
||||
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
|
||||
added = true
|
||||
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then
|
||||
table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name})
|
||||
added = true
|
||||
end
|
||||
end
|
||||
if not added then table.insert(reasons, reasonFor(product, e, input)) end
|
||||
end
|
||||
table.sort(sell, function(a, b)
|
||||
if a.e.percent ~= b.e.percent then return a.e.percent > b.e.percent end
|
||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
||||
return a.product.name < b.product.name
|
||||
end)
|
||||
table.sort(store, function(a, b)
|
||||
if a.e.storagePercent ~= b.e.storagePercent then return a.e.storagePercent > b.e.storagePercent end
|
||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
||||
return a.product.name < b.product.name
|
||||
end)
|
||||
return sell, store, reasons
|
||||
end
|
||||
|
||||
local function sortedSources(product)
|
||||
local sources = {}
|
||||
for _, source in ipairs(product.sources or {}) do
|
||||
if (source.available or 0) >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then table.insert(sources, source) end
|
||||
end
|
||||
table.sort(sources, function(a, b) return a.available > b.available end)
|
||||
return sources
|
||||
end
|
||||
|
||||
local function buildTrip(primary, candidates, input)
|
||||
local trip = {kind = primary.kind, fillTypes = {primary.product.fillType}, names = {primary.product.name}, sourceIds = {}, targetWayPointId = primary.targetWayPointId, targetName = primary.targetName, percent = primary.percent}
|
||||
local members = {primary}
|
||||
if input.collectRound and input.hasAutoload and primary.product.isPallet then
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if candidate ~= primary and candidate.kind == primary.kind and candidate.targetWayPointId == primary.targetWayPointId and candidate.product.isPallet then
|
||||
table.insert(members, candidate)
|
||||
table.insert(trip.fillTypes, candidate.product.fillType)
|
||||
table.insert(trip.names, candidate.product.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
local seen = {}
|
||||
for _, member in ipairs(members) do
|
||||
local sources = sortedSources(member.product)
|
||||
local limit = input.collectRound and #sources or math.min(1, #sources)
|
||||
for index = 1, limit do
|
||||
local id = sources[index].wayPointId
|
||||
if not seen[id] then seen[id] = true; table.insert(trip.sourceIds, id) end
|
||||
end
|
||||
end
|
||||
return trip
|
||||
end
|
||||
|
||||
function ADOutboundPlanner.plan(input)
|
||||
local sell, store, reasons = collectCandidates(input)
|
||||
if sell[1] ~= nil then return buildTrip(sell[1], sell, input), reasons end
|
||||
if store[1] ~= nil then return buildTrip(store[1], store, input), reasons end
|
||||
return nil, reasons
|
||||
end
|
||||
```
|
||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adOutboundPlanner — turvurdering, prisregel, politikker, opsamlingsrunde (ren)`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: adSources — kilde-genkendelse og lagerstatus
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adSources.lua`
|
||||
- Test: `tests/test_adSources.lua`
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADSources.MAX_DISTANCE = 40; ADSources.PALLET_RADIUS = 25
|
||||
ADSources.identify(adEnv, wayPointId, farmId) -- {kind="factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} | nil
|
||||
ADSources.getProducts(adEnv, source, farmId) -- { {fillType, name, available, level, capacity, isPallet} } (kun varer med kapacitet/indhold)
|
||||
ADSources.isPalletFillType(fillType) -- fillTypeDesc.palletFilename ~= nil
|
||||
ADSources.getSellMarkers(adEnv, farmId) -- { {marker, station} } salgssteder (ADPrices.isSellingStation)
|
||||
ADSources.getObjectInfoIndex(source, fillType) -- objectInfo-indeks + antal for varen i et objektlager, eller nil
|
||||
ADSources.spawnOne(source, objectInfoIndex) -- pcall placeable:removeAbstractObjectsFromStorage(index, 1, nil) -> ok
|
||||
ADSources.countStored(source, fillType) -- antal objekter af varen i lageret
|
||||
ADSources.getSourcePlaceables(sources) -- {[placeable]=true} til udelukkelse
|
||||
```
|
||||
Genkendelse: læssestation ved markøren (`ADSmartPickup.getLoadStationAtWayPointId`) med `owningPlaceable.spec_productionPoint` → factory; med `spec_silo` → silo; ellers nærmeste placeable med `spec_objectStorage` inden for 40 m af waypointet (`g_currentMission.placeableSystem.placeables`, `getWorldTranslation(placeable.rootNode)`, `canFarmAccess`) → objectStorage.
|
||||
Fabrik-produkter: `for production in productionPoint.productions: for output in production.outputs: type` (unik); `level = productionPoint.storage:getFillLevel(ft)`, `capacity = storage:getCapacity(ft)` (fallback `storage.capacities[ft]`); paller i nærheden: `g_currentMission.vehicleSystem.vehicles` med `spec_pallet` eller `isPallet`, ejer = farm, fillUnit-varen = ft, afstand ≤ 25 m fra `productionPoint.palletSpawner.spawnPlaces[1].startNode` (fallback placeable.rootNode); `available = level + Σ palle-fyldning`.
|
||||
Silo: `spec_silo.storages[*]` fillLevels > 0 → `level = getOwnStorageLevel`, `capacity = storage:getCapacity(ft)`.
|
||||
Objektlager: `spec_objectStorage.objectInfos[i].objects[1]` med `palletAttributes.fillType/fillLevel` eller `baleAttributes.fillType/fillLevel`; `numObjects` → available = numObjects × fillLevel; level = `spec.numStoredObjects`, capacity = `spec.capacity`; isPallet = true.
|
||||
|
||||
- [ ] **Step 1: Tests** med fakes: (S1) markør ved fabrik → factory med SUGAR output level 150000, capacity 180000, ingen paller → available 150000; (S2) markør ved silo → silo med FORAGE 74500; (S3) markør uden station, objektlager 20 m væk → objectStorage, produkt STRAW_BASKET available 8×1000, isPallet true, `getObjectInfoIndex` = 1 og 8; (S4) paller ved spawner: 2 paller à 1000 STRAW_BASKET inden for 25 m + 1 uden for → available = storage + 2000; (S5) `getSellMarkers` giver kun markører hvis unload-station er SellingStation med isSellingPoint; (S6) `spawnOne` kalder `removeAbstractObjectsFromStorage(1, 1, nil)` (fake tæller); (S7) markør 60 m fra objektlager → nil.
|
||||
- [ ] **Step 2–4:** implementér som beskrevet, kør, grønne. **Step 5: Commit** `feat: adSources — fabrik, silo og objektlager som kilder; paller ved spawner; salgssteder`
|
||||
|
||||
---
|
||||
|
||||
### Task 6: adOutbound — hooks, hold, objektlager-udtag, runde, advarsler
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adOutbound.lua`
|
||||
- Test: `tests/test_adOutbound.lua`
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADOutbound.CHECK_INTERVAL_MS = 5000; ADOutbound.NO_PROGRESS_MS = 15000; ADOutbound.SPAWN_STALL_MS = 30000
|
||||
ADOutbound.isOutboundVehicle(vehicle) -- run for vognen har kind "outbound"
|
||||
ADOutbound.choosePickup(adEnv, vehicle) -- kaldes fra getNextPickup-wrap; returnerer choice {markerIndex, markerName, group, station|nil, fillTypes, fillType} eller nil
|
||||
ADOutbound.buildPlanInput(adEnv, run, vehicle) -- samler kilder/priser/lager til planlæggeren (spil-API)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task, dt) -- true = håndteret (spring original update over)
|
||||
ADOutbound.beforeLoadFinished(adEnv, task) -- true = fortsæt runde (spring original finished over)
|
||||
ADOutbound.releaseInactive()
|
||||
ADOutbound.getHoldFor(vehicle) -- {markerName, reasons} | nil
|
||||
ADOutbound.getRunStatus(run) -- {reasons, warning} (seneste)
|
||||
ADOutbound.install(adEnv) -- wrapper LoadAtDestinationTask.update/finished
|
||||
```
|
||||
|
||||
Adfærd (fra spec afsnit 3–4):
|
||||
- `choosePickup`: `trips[vehicle]=nil`; input = buildPlanInput; `trip, reasons = ADOutboundPlanner.plan(input)`; status[run.id] = reasons. Tur → `stateModule.selectedFillTypes = trip.fillTypes; setFillType(trip.fillTypes[1]); setSecondMarkerByWayPointId(trip.targetWayPointId); raiseDirtyFlag`; `trips[vehicle] = {run, trip, queue = copy(trip.sourceIds), visited = {}, current = trip.sourceIds[1]}`; log `'MT635' udkørsel: 8×STRAW_BASKET fra Straw factory → Halm-salg (95 % af top)`; returnér choice for første kilde (station = kildens læssestation hvis bulk, ellers nil). Ingen tur → `ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable=hasRouteTo)`; fundet → claim, `holds[vehicle] = {markerId, reasons}`, log én gang pr. ændret begrundelse, returnér choice for Wait-markøren (station nil); intet Wait-punkt → hold ved første kilde (warn én gang) og returnér den.
|
||||
- `buildPlanInput`: for hver kilde i `run.sourceWayPointIds`: `ADSources.identify` (nil → spring over + warn én gang "kilde markør X ikke genkendt"); products = union over kilder af `ADSources.getProducts`, filtreret til `ADRuns.getProductPolicy ~= "off"` og til det vognen kan bære (`bulk: AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`, pallet: hasAutoload = `AutoDrive:hasAL(trailer)` for en af `getAllUnits`); pr. produkt med sell-politik: quotes over `ADSources.getSellMarkers` hvor `station.acceptedFillTypes[ft]` (eller `getIsFillTypeSupported`) og `ADWaitPool.hasRouteBothWays(adEnv, vehicle, marker.id, sourceId)` → `ADPrices.getQuote`; bedste = `ADPrices.bestQuote`; ingen nåelig men mindst én unåelig → warnOnce(vehicle, "sell:"..ft, "'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet"); pr. produkt med store-politik: `ADLoadSwap.findDumpMarker(adEnv, vehicle, ft, math.min(available, capacity), nil, isReachable, ADSources.getSourcePlaceables(sources))` → store; nil → warnOnce(vehicle, "store:"..ft, "'%s' finder ingen silo med plads til %s (%s)").
|
||||
- `warnOnce(vehicle, key, text)`: `warned[vehicle][key] ~= text` → Logging.warning + pcall `adEnv.AutoDriveMessageEvent.sendMessageOrNotification(vehicle, adEnv.ADMessagesManager.messageTypes.ERROR, "ADSmartPickup: " .. text, 8000)`; status[run.id].warning = text. Ryddes når nøglens tilstand ændrer sig (ny tekst) eller turen lykkes.
|
||||
- `beforeLoadUpdate`: hold → hvis `task.destinationID == hold.markerId` og `drivePathModule:isTargetReached()`: `specialDrivingModule:stopVehicle(); specialDrivingModule:update(dt)`; timer 5 s → `choosePickup` igen; tur → `ADWaitPool.releaseVehicle`, `holds[vehicle]=nil`, `ADWaitPool.redirect(task, trip.sourceIds[1])`, `task.activatedUALLoading=false`; returnér true. Ikke fremme → false.
|
||||
Tur aktiv og `isTargetReached()`: kilde = `state.sources[task.destinationID]`; objektlager → `updateSpawn` (nedenfor); derefter fremdriftsvagt: `level = AutoDrive.getAllFillLevels(task.trailers)`; stiger → `noProgressMs=0`; ellers `+dt`; `> NO_PROGRESS_MS` og `level > 0` og ingen spawn i gang → `tryNextSourceOrFinish(task)`; returnér false (original kører altid, den aktiverer UAL og stopper vognen).
|
||||
- `updateSpawn(state, task, source, dt)`: første gang: `index = ADSources.getObjectInfoIndex(source, currentFillType)`; hvis `task.activatedUALLoading` og vognen står (`vehicle.lastSpeedReal < 0.0005`): hvis ingen udestående spawn: `ualIsFull()` på en UAL-vogn → done; `countStored == 0` → skift til næste fillType i `trip.fillTypes` (nyt index) eller done; ellers `ADSources.spawnOne` → `pending = {stored = countStored, loaded = countLoaded(trailers), ms = 0}`; udestående: `countStored < pending.stored and countLoaded > pending.loaded` → pending=nil (næste); `pending.ms > SPAWN_STALL_MS` → done + warn "objektlager spawner ikke". `countLoaded` = antal nøgler i `spec_universalAutoload.loadedObjects` summeret over trailers. done → `task:finished()` (via original finished) og returnér.
|
||||
- `tryNextSourceOrFinish(task)`: næste id i `state.queue` som ikke er besøgt, og fri kapacitet ≥ 10 % (`fillFreeCapacity/capacity`) → `visited[current]=true; current=next; ADWaitPool.redirect(task, next); task.activatedUALLoading=false; activePickups opdateres via ADSmartPickup.setActivePickup(vehicle, station, fillTypes)` (ny lille funktion i adSmartPickup); ellers `task:finished()`.
|
||||
- `beforeLoadFinished`: kaldes fra wrap af `finished`; hvis der er tur, kilde ikke fuld (ikke `filledToUnload`) og en ubesøgt kilde med plads → redirect som ovenfor og returnér true (spring original over); ellers `trips[vehicle]=nil` og false.
|
||||
- `releaseInactive`: for inaktive køretøjer: trips/holds ryddes, `ADWaitPool.releaseVehicle`.
|
||||
|
||||
- [ ] **Step 1: Tests** (fake AD-miljø som i `test_adRunsController.lua` + fakes for `ADSources`, `ADPrices`, `ADWaitPool`, `ADRunsController.getRunForVehicle`): (O1) tur → selectedFillTypes/2. markør sat, choice = kilde; (O2) ingen tur → Wait-markør claimet, hold registreret, choice = Wait; (O3) hold + 5 s + nu tur → redirect til kilde, hold frigivet; (O4) objektlager: spawn kaldes én ad gangen; stored falder + loaded stiger → næste spawn; `ualIsFull` → finished kaldt; (O5) spawn uden fremdrift 30 s → finished + WARN; (O6) runde bulk: kilde A fylder vognen (`filledToUnload`) → finished kaldt, ingen redirect; kilde A giver 30 % → redirect til B; (O7) intet nåeligt salgssted → `sendMessageOrNotification` kaldt én gang med ERROR, anden gang ikke; (O8) `releaseInactive` rydder hold og claim.
|
||||
- [ ] **Step 2–4:** implementér, kør, grønne. **Step 5: Commit** `feat: adOutbound — udkørsels-hooks: turvalg, hold ved Wait, objektlager-udtag, opsamlingsrunde, advarsler på skærm`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Controller — udkørsels-runs
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adRunsController.lua`
|
||||
- Test: `tests/test_adRunsController.lua` (sektion C-out)
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADRunsController.createRun(name, kind)
|
||||
ADRunsController.getSourceMarkers(adEnv, farmId) -- markører der er kilder: {marker, kind}
|
||||
ADRunsController.getRunProducts(adEnv, run) -- { {fillType, name, title, policy, isPallet, available, percent|nil, stationName|nil} } sorteret på titel
|
||||
ADRunsController.validateVehicle(adEnv, run, vehicle) -- outbound: reasons noSource | noTrailer | noProducts | needsAutoload | cannotCarry | farFromNetwork
|
||||
ADRunsController.getOutboundStatus(adEnv, run) -- {products = getRunProducts, reasons, warning} (fra ADOutbound.getRunStatus)
|
||||
```
|
||||
`applyRunToVehicle`: `if run.kind == "outbound"`: mode P&D, `setFirstMarkerByWayPointId(run.sourceWayPointIds[1])`, `setSecondMarkerByWayPointId(run.sourceWayPointIds[1])`, `selectedFillTypes` = produkt-fillTypes vognen kan bære, `setFillType(first)`, loopCounter 0, useFolders true, rotateTargets ONLYPICKUP; ingen dump-logik. `getVehicleActivity`: `ADOutbound.getHoldFor(vehicle)` → `{kind="waiting", place=markerName, reason=reasons[1]}`. `updateRun` logger som før. `getRunTargets` for outbound returnerer `{}` (mål vælges pr. tur).
|
||||
|
||||
- [ ] **Step 1: Tests** (C-out1) createRun kind; (C-out2) validate: ingen kilder → noSource; bulk-vogn og kun pallevarer → needsAutoload; ingen politik → noProducts; ok-tilfælde; (C-out3) startRun sætter 1./2. markør = kilde 1 og selectedFillTypes; (C-out4) getRunProducts slår politik op.
|
||||
- [ ] **Step 2–4:** implementér, kør alle tests. **Step 5: Commit** `feat: controller — udkørsels-runs: start, validering, produkter, status`
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Wiring i adSmartPickup, adUnloadWait, modDesc
|
||||
|
||||
**Files:**
|
||||
- Modify: `adSmartPickup.lua` (source() nye gui-filer, `installHook`: outbound-gren i `getNextPickup`-wrap, `ADOutbound.install(adEnv)`, `ADSmartPickup.setActivePickup`, `update`: `ADOutbound.releaseInactive`), `adUnloadWait.lua` (`beforeUpdate`: return hvis `ADOutbound.isOutboundVehicle`), `modDesc.xml` (sourceFiles: adPrices, adOutboundPlanner, adSources, adWaitPool, adOutbound før adUnloadWait/adSmartPickup; version 1.12.0.0; beskrivelse EN udvidet med udkørsel).
|
||||
|
||||
`getNextPickup`-wrap, ny gren først:
|
||||
```lua
|
||||
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle(driver) then
|
||||
local ok, choice = pcall(ADOutbound.choosePickup, adEnv, driver)
|
||||
if not ok then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i udkørsel, bruger AutoDrives rotation: %s", tostring(choice))
|
||||
activePickups[driver] = nil
|
||||
return originalGetNextPickup(self, driver, forcedSkip)
|
||||
end
|
||||
if choice == nil then activePickups[driver] = nil; return originalGetNextPickup(self, driver, forcedSkip) end
|
||||
activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType} or nil
|
||||
self.pickups[choice.group] = self.pickups[choice.group] or {}
|
||||
self.pickups[choice.group].lastTarget = choice.markerIndex
|
||||
self.pickups[choice.group].lastVehicle = driver
|
||||
return choice.markerIndex
|
||||
end
|
||||
```
|
||||
- [ ] **Step 1:** ret filer. **Step 2:** `luajit tests/test_adSmartPickup.lua` grøn; `for t in tests/test_*.lua; do luajit $t | tail -1; done`. **Step 3: Commit** `feat: udkørsel koblet på AutoDrive-hooks; Wait-logik springer udkørsels-traktorer over`
|
||||
|
||||
---
|
||||
|
||||
### Task 9: GUI — tre faner
|
||||
|
||||
**Files:**
|
||||
- Modify: `gui/SmartPickupFrame.lua` (faner, fælles), `gui/SmartPickupFrame.xml` (nye statiske rækker + productPrefab + sourcePrefab), `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
||||
- Create: `gui/SmartPickupSupplyTab.lua` (flyt: fillTargetRows, fillNeedsRows, refreshNeedsRows, fillIngredientRows, describeTargetNeeds, onClickTarget/IngredientMode/Ingredient/Loops/ReturnBelow, `SmartPickupFrame:rebuildSupplyTab(adEnv, run)`), `gui/SmartPickupOutboundTab.lua` (`rebuildOutboundTab(adEnv, run)`: Kilder-rækker (ingredientPrefab med kind="source"), Varer-rækker (productPrefab: MultiTextOption med 4 tilstande Fra/Lager/Salg/Salg, ellers lager + titel "Sukker — 150.0k · 78 % af top (Grain Elevator)"), Regler (sellAtOption 50..100 trin 5, storeAboveOption 50..95 trin 5, collectRoundOption), status-/advarselsrække; callbacks onClickSource/onClickPolicy/onClickSellAt/onClickStoreAbove/onClickCollectRound; `describeOutboundOverview(run)` til Oversigt).
|
||||
- `SmartPickupFrame`: `TAB_OVERVIEW, TAB_SUPPLY, TAB_OUTBOUND = 1, 2, 3`, `TAB_NAMES = {"spu_tab_overview","spu_tab_supply","spu_tab_outbound"}`, `tabKind = {nil, "supply", "outbound"}`; `fillRunOption` viser kun runs med fanens kind; `onNewRunName` opretter med fanens kind (Oversigt → supply); `setStaticRowsVisible(tab)` viser forsynings-rækker (ingredientMode, waitPool, loops, returnBelow) kun på fane 2, udkørsels-rækker (waitPool, sellAt, storeAbove, collectRound) kun på fane 3 (waitPool er fælles); blok-overskrifter via `addTextRow` ("— Mål og varer —" stil: brug tekstnøglerne `spu_section_*`). `refreshLive` opdaterer også produktrækkernes titler (pris-%) hvert sekund og udkørsels-status.
|
||||
- l10n (da/en): `spu_tab_supply` "Forsyning"/"Supply", `spu_tab_outbound` "Udkørsel"/"Outbound", `spu_sources` "Kilder (fabrik, silo eller objektlager)", `spu_products` "Varer", `spu_policy_off/store/sell/sellElseStore` "Fra/Lager/Salg/Salg, ellers lager", `spu_sellAt` "Sælg fra (% af årets toppris)", `spu_sellAt_tooltip`, `spu_storeAbove` "Kør til lager over", `spu_storeAbove_tooltip`, `spu_collectRound` "Saml op på én tur", `spu_collectRound_tooltip`, `spu_section_run/targets/rules/wait/vehicles/status`, `spu_reason_noSource` "ingen genkendt kilde", `spu_reason_noProducts` "ingen vare valgt", `spu_reason_needsAutoload` "pallevarer kræver Universal Autoload på vognen", `spu_product_row` "%s — %s · %s", `spu_price_pct` "%d %% af top (%s)", `spu_price_none` "intet salgssted", `spu_outbound_waiting` "venter: %s", `spu_activity_holding` "holder ved %s (%s)".
|
||||
- [ ] **Step 1:** XML + l10n; **Step 2:** split + ny fane; **Step 3:** `luajit` alle tests (GUI testes in-game); **Step 4: Commit** `feat(gui): faner Oversigt/Forsyning/Udkørsel; udkørsels-run med kilder, varepolitik, regler`
|
||||
|
||||
---
|
||||
|
||||
### Task 10: README, version, byg, deploy, in-game
|
||||
|
||||
- [ ] README: nyt afsnit "Udkørsel (v1.12)" (kilder, politikker, prisregel, lagertærskel, runde, objektlager-krav om markørplacering, advarsler). modDesc 1.12.0.0.
|
||||
- [ ] `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cp ../FS25_ADSmartPickup.zip ~/FS25-data/mods/`
|
||||
- [ ] Commit `chore: v1.12.0.0 — udkørsel af produkter` + push til Gitea (`git push gitea main` med token fra vault).
|
||||
- [ ] In-game-test (bruger): savegame 3 — se spec afsnit 4.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,174 +0,0 @@
|
||||
# Guider til opsætning (marker + flows) — implementeringsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
|
||||
|
||||
**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
|
||||
|
||||
**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
|
||||
|
||||
**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
|
||||
- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
|
||||
- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
|
||||
- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
|
||||
- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
|
||||
- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
|
||||
- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
|
||||
- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
|
||||
- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
|
||||
- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
|
||||
2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
|
||||
3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
|
||||
4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
|
||||
5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
|
||||
|
||||
---
|
||||
|
||||
## Beslutninger (uklarheder i spec'en)
|
||||
|
||||
- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
|
||||
- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
|
||||
- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
|
||||
- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
|
||||
- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
|
||||
- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
|
||||
- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
|
||||
- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
|
||||
- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
|
||||
- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
|
||||
|
||||
---
|
||||
|
||||
# Etape A — fælles ramme + Marker (v1.31.0.0)
|
||||
|
||||
### Task A1: Guide-motoren `adGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADGuide.new(def, draft, context, options) -> guide
|
||||
-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
|
||||
-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
|
||||
-- guide = {def, draft, context, step, single, returnTo, snapshot}
|
||||
ADGuide.visibleSteps(guide) -> {stepId...}
|
||||
ADGuide.position(guide) -> index|nil, total
|
||||
ADGuide.isLast(guide) -> bool
|
||||
ADGuide.next(guide) -> guide, event -- event nil | "done"
|
||||
ADGuide.back(guide) -> guide, event -- event nil | "cancel"
|
||||
ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
|
||||
ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
|
||||
ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
|
||||
ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
|
||||
ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
|
||||
```
|
||||
|
||||
- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
|
||||
- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
|
||||
- [ ] Implementér; kør → ALLE TESTS OK.
|
||||
|
||||
### Task A2: Markguidens trin `adFieldGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
|
||||
- Produces:
|
||||
```lua
|
||||
ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
|
||||
ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
|
||||
ADFieldGuide.openPicker(context) -> guide -- trin "field"
|
||||
ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
|
||||
ADFieldGuide.isVisible(stepId, guide) -> bool
|
||||
ADFieldGuide.rows(guide) -> {row...}
|
||||
-- row = {type="choice", id, title, options={{label, value}}, value}
|
||||
-- | {type="toggle", id, title, off, on, value}
|
||||
-- | {type="check", id, item, title, value}
|
||||
-- | {type="text", label} | {type="link", id, value, label}
|
||||
ADFieldGuide.choose(guide, id, value) -> guide
|
||||
ADFieldGuide.summary(guide) -> {line...}
|
||||
ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
|
||||
ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
|
||||
ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
|
||||
```
|
||||
- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
|
||||
- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
|
||||
|
||||
- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
|
||||
- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
|
||||
|
||||
### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
|
||||
|
||||
**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
|
||||
SmartPickupFrame:initGuideButtons(i18n)
|
||||
SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
|
||||
SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
|
||||
SmartPickupFrame:guideText(label) -> string
|
||||
SmartPickupFrame:addLinkRow(text, action)
|
||||
SmartPickupFrame:onClickGuideRow(state, data)
|
||||
SmartPickupFrame:getGuideButtons() -> knapliste
|
||||
SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
|
||||
```
|
||||
- [ ] Skriv filen; `luajit -bl` syntakstjek.
|
||||
|
||||
### Task A4: Marker-fanen: oversigt, markflåde, guide
|
||||
|
||||
**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
|
||||
|
||||
- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
|
||||
- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
|
||||
- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
|
||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
||||
|
||||
### Task A5: Docs + ship v1.31.0.0
|
||||
|
||||
- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
|
||||
- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
|
||||
|
||||
# Etape B — Flows på samme ramme (v1.32.0.0)
|
||||
|
||||
### Task B1: Flowguidens trin `adFlowGuide.lua`
|
||||
|
||||
**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
|
||||
|
||||
**Interfaces (Produces):**
|
||||
```lua
|
||||
ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
|
||||
ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
|
||||
ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
|
||||
ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
|
||||
ADFlowGuide.summary(guide) -> {line...}
|
||||
ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
|
||||
ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
|
||||
ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
|
||||
```
|
||||
- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
|
||||
- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
|
||||
- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
|
||||
- [ ] FAIL → implementér → OK.
|
||||
|
||||
### Task B2: Flows-fanen i GUI'et
|
||||
|
||||
**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
|
||||
|
||||
- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
|
||||
- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
|
||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
||||
|
||||
### Task B3: Docs + ship v1.32.0.0; selv-review
|
||||
|
||||
- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
|
||||
- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,919 +0,0 @@
|
||||
# Maskinpulje etape 1 (v1.48.0.0) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Markarbejdet kan sende en ledig solo-traktor, der henter et frakoblet puljeredskab (plov, kultivator,
|
||||
såmaskine …), udfører arbejdet med Courseplay, sætter redskabet af på dets AD-parkering og kører hjem.
|
||||
|
||||
**Architecture:** Nyt modul `adPool.lua` (ren planner + tynd spil-adapter) finder puljeredskaber (frakoblet +
|
||||
AD-parkering "X" + markør "Connect X") og puljetraktorer og parrer dem. `ADFieldWork.dispatch` bruger puljen som
|
||||
fallback, når ingen færdig kombination er ledig; en ny fase `fetching` kører `ADHitch.fetch` og fortsætter så i
|
||||
den eksisterende `driving`-fase. Når en opgave slutter, kører `ADHitch.returnHome` redskabet til dets parkering
|
||||
(afkobling) og traktoren hjem. Marker, der venter, betjenes længst-ventende først.
|
||||
|
||||
**Tech Stack:** Lua 5.1 (FS25-scripting), luajit til tests, AutoDrive 3.0.1.2 og Courseplay 8.1.0.3 API'er.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-28-maskinpulje-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Kun etape 1 (markarbejde). Tømmevogne ved høst (`decision.helper == true`) og marksilo-flowet bruger IKKE puljen i denne version.
|
||||
- Puljeredskab = frakoblet + AD-parkering "X" (`advd.parkDestination >= 1`) + markør "Connect X" (store/små bogstaver ligegyldigt).
|
||||
- Puljetraktor = motoriseret, intet koblet bag på, ikke i et flow (`ADRunsController.getRunForVehicle == nil`), ikke optaget, ikke styret af spilleren, egen AD-parkering, flueben i markflåden (`ADFieldFleet.isEnabled`).
|
||||
- Færdige kombinationer foretrækkes altid; puljen kun når `pick`/`pickMany` intet giver.
|
||||
- Redskab nærmest marken, derefter nærmeste traktor til redskabet med nok hk; manglende hk-data = tilladt.
|
||||
- Karantæne for et redskab, der ikke kunne kobles på: 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
||||
- En traktor køres kun hjem, når et af mod'ens egne jobs slutter — aldrig af sig selv.
|
||||
- Flows (runs) rører aldrig puljen.
|
||||
- Kommentarer og logtekster på dansk, som resten af koden. Ingen Co-Authored-By i commits. Push kun til remote `gitea`.
|
||||
- Tests køres fra repo-roden: `luajit tests/test_<navn>.lua` — output slutter med `ALLE TESTS OK`.
|
||||
- Build: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy mens spillet kører: `cp` til `~/FS25-data/mods/.FS25_ADSmartPickup.new` og `mv` på plads.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- **To marker vil have samme plov samtidig** → kun den første får den; den anden venter (reservation) — testet i Task 1 (`pairFor` springer reserverede over).
|
||||
- **Redskab uden hk-data eller traktor uden motor-data** → tillades (intet går i stå) — testet i Task 1 (`powerOk` nil-tilfælde, `numberOf` af tabel-specs).
|
||||
- **Traktor på hjemtur bliver valgt til et nyt job** → må ikke ske; den er optaget til den er hjemme — testet i Task 3 (`ADPool.isReturning`) og brugt i `getRigs`/`poolTractors`.
|
||||
- **Spilleren kobler selv et puljeredskab på og kører** → traktoren køres ikke hjem af mod'en — dækket af, at `returnHome` kun kaldes fra `finishAssignment`/fejlveje (Task 5); ingen tick-regel.
|
||||
- **Kobling fejler (trækøje står skævt)** → traktor hjem, redskab i karantæne, marken prøver igen — testet i Task 1 (karantæne i `pairFor`) og Task 4 (fejlvej).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `adPool.lua` — ren planlægning
|
||||
|
||||
**Files:**
|
||||
- Create: `FS25_ADSmartPickup/adPool.lua`
|
||||
- Test: `tests/test_adPool.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFieldFleet.roleFromSpecs(specs)` (adFieldFleet.lua:27), `ADFieldModel.checkWidth(role, width, requiredWidth)` (bruges via `accept`), `ADHitch.parseMarker(name)` (adHitch.lua).
|
||||
- Produces:
|
||||
- `ADPool.roleOf(specs) -> role|nil` (specs = `ADCpBridge.getSpecs`-tabel; `unloader` når `specs.spec_trailer and specs.spec_dischargeable` og ingen anden rolle).
|
||||
- `ADPool.numberOf(value) -> number|nil`
|
||||
- `ADPool.powerOk(tractorHp, neededHp) -> bool`
|
||||
- `ADPool.connectFor(parkName, markers) -> marker|nil` (markers = `{{name, id}}`)
|
||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) -> pair | nil, why, args`
|
||||
- implement = `{id, name, role, roles?, x, z, width, neededHp, connectId, parkId}`
|
||||
- tractor = `{id, name, x, z, hp, parkId}`
|
||||
- blocked = `{[id] = true}` (reserverede + karantæne, både redskab- og traktor-id)
|
||||
- pair = `{implement = implement, tractor = tractor}`
|
||||
- why ∈ `noPoolImplement` / `noPoolTractor` / `tooWeak` (args = `{neededHp}`)
|
||||
- `ADPool.queueOrder(infos, waitingSince) -> infos (ny tabel)`
|
||||
- `ADPool.reserve(owner, ids)`, `ADPool.release(owner)`, `ADPool.quarantine(id, nowMs)`, `ADPool.blocked(nowMs) -> {[id]=true}`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/test_adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
||||
dofile("FS25_ADSmartPickup/adHitch.lua")
|
||||
dofile("FS25_ADSmartPickup/adPool.lua")
|
||||
local P = ADPool
|
||||
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
|
||||
-- rolle ud fra redskabets egne specs
|
||||
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
|
||||
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
|
||||
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
|
||||
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
|
||||
check("rolle: intet", P.roleOf({}), nil)
|
||||
-- hk fra butiksdata: tal eller tabel (konfigurationer)
|
||||
check("tal", P.numberOf(180), 180)
|
||||
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
|
||||
check("tom tabel", P.numberOf({}), nil)
|
||||
check("nil", P.numberOf(nil), nil)
|
||||
check("hk: nok", P.powerOk(200, 180), true)
|
||||
check("hk: for lidt", P.powerOk(150, 180), false)
|
||||
check("hk: redskab uden data", P.powerOk(150, nil), true)
|
||||
check("hk: traktor uden data", P.powerOk(nil, 180), true)
|
||||
-- Connect-markør til en parkering
|
||||
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
|
||||
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
|
||||
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
|
||||
check("connect: ingen", P.connectFor("Start", markers), nil)
|
||||
check("connect: uden parkering", P.connectFor(nil, markers), nil)
|
||||
-- parring: redskab nærmest marken, så traktor nærmest redskabet
|
||||
local implements = {
|
||||
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
|
||||
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
|
||||
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
|
||||
}
|
||||
local tractors = {
|
||||
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
|
||||
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
|
||||
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
|
||||
}
|
||||
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
|
||||
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
|
||||
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
|
||||
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
|
||||
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
|
||||
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
|
||||
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
|
||||
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
|
||||
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
|
||||
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
|
||||
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
|
||||
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
|
||||
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
|
||||
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
|
||||
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
|
||||
check("par: redskab med flere roller", multi.implement.id, "spreder")
|
||||
-- reservationer og karantæne
|
||||
P.reserve("F12", {"plovNear", "mid"})
|
||||
check("reserveret: redskab", P.blocked(0).plovNear, true)
|
||||
check("reserveret: traktor", P.blocked(0).mid, true)
|
||||
P.release("F12")
|
||||
check("frigivet", P.blocked(0).plovNear, nil)
|
||||
P.quarantine("plovNear", 1000)
|
||||
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
|
||||
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
|
||||
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
|
||||
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
|
||||
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
|
||||
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
|
||||
check("kø: rører ikke input", infos[1].id, 5)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `luajit tests/test_adPool.lua`
|
||||
Expected: fejl `cannot open FS25_ADSmartPickup/adPool.lua`
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Create `FS25_ADSmartPickup/adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- AD Pool
|
||||
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
||||
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
||||
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
||||
|
||||
ADPool = {}
|
||||
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
||||
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
||||
-- tip/snegl er tømmevogn.
|
||||
function ADPool.roleOf(specs)
|
||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||
if role ~= nil and role ~= "unloader" then return role end
|
||||
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
||||
return role
|
||||
end
|
||||
|
||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||
function ADPool.numberOf(value)
|
||||
if type(value) == "number" then return value end
|
||||
if type(value) ~= "table" then return nil end
|
||||
local best = nil
|
||||
for _, entry in pairs(value) do
|
||||
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
||||
function ADPool.powerOk(tractorHp, neededHp)
|
||||
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
||||
return tractorHp >= neededHp
|
||||
end
|
||||
|
||||
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
||||
function ADPool.connectFor(parkName, markers)
|
||||
if type(parkName) ~= "string" then return nil end
|
||||
local wanted = parkName:lower()
|
||||
for _, marker in pairs(markers or {}) do
|
||||
local action, target = ADHitch.parseMarker(marker.name)
|
||||
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function hasRole(item, role)
|
||||
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
||||
end
|
||||
|
||||
local function distanceSq(ax, az, bx, bz)
|
||||
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
||||
end
|
||||
|
||||
local function nearest(items, x, z, keep)
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, item in ipairs(items) do
|
||||
if keep(item) then
|
||||
local distance = distanceSq(item.x, item.z, x, z)
|
||||
if distance < bestDistance then best, bestDistance = item, distance end
|
||||
end
|
||||
end
|
||||
return best
|
||||
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)
|
||||
blocked = blocked or {}
|
||||
local free = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if not blocked[tractor.id] then table.insert(free, tractor) end
|
||||
end
|
||||
local candidates = {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
||||
table.insert(candidates, implement)
|
||||
end
|
||||
end
|
||||
if #candidates == 0 then return nil, "noPoolImplement" end
|
||||
if #free == 0 then return nil, "noPoolTractor" end
|
||||
table.sort(candidates, function(a, b)
|
||||
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
||||
end)
|
||||
local weakest = nil
|
||||
for _, implement in ipairs(candidates) do
|
||||
local tractor = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end)
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end
|
||||
end
|
||||
return nil, "tooWeak", {weakest or 0}
|
||||
end
|
||||
|
||||
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
||||
function ADPool.queueOrder(infos, waitingSince)
|
||||
local waiting, rest = {}, {}
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
||||
end
|
||||
table.sort(waiting, function(a, b)
|
||||
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
||||
if wa ~= wb then return wa < wb end
|
||||
return a.id < b.id
|
||||
end)
|
||||
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
||||
return waiting
|
||||
end
|
||||
|
||||
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
||||
local reservations, quarantined = {}, {}
|
||||
|
||||
function ADPool.reserve(owner, ids)
|
||||
reservations[owner] = ids
|
||||
end
|
||||
|
||||
function ADPool.release(owner)
|
||||
reservations[owner] = nil
|
||||
end
|
||||
|
||||
function ADPool.quarantine(id, nowMs)
|
||||
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
||||
end
|
||||
|
||||
function ADPool.blocked(nowMs)
|
||||
local result = {}
|
||||
for _, ids in pairs(reservations) do
|
||||
for _, id in ipairs(ids) do result[id] = true end
|
||||
end
|
||||
for id, untilMs in pairs(quarantined) do
|
||||
if nowMs < untilMs then result[id] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `luajit tests/test_adPool.lua`
|
||||
Expected: sidste linje `ALLE TESTS OK`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adPool.lua tests/test_adPool.lua
|
||||
git commit -m "feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `adPool.lua` — spil-adapter (indsamling af puljen)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (tilføj nederst)
|
||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (registrér `adPool.lua` efter `adHitch.lua`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADCpBridge.getSpecs(vehicle)` (adCpBridge.lua:153), `ADCpBridge.getWorkWidth(vehicle)` (:363), `ADRunsController.getFarmVehicles(adEnv)`, `ADRunsController.getRunForVehicle(vehicle)`, `ADRunsController.getVehicleId(vehicle)`, `ADRuns.vehicleLabel(vehicle)`, `ADFieldFleet.isEnabled(mode, fleet, id)`, `ADFieldJobs.state`, `ADHitch.rearImplementsOf(vehicle)` (Task 3), `ADPool.isReturning(vehicle)` (Task 3), `adEnv.ADGraphManager:getMapMarkers()`, `:getMapMarkerById(index)`, `g_currentMission.vehicleSystem.vehicles`.
|
||||
- Produces: `ADPool.collect(adEnv, isBusy) -> implements, tractors` (formater som Task 1; implement har også `.vehicle`, tractor har `.vehicle`); `isBusy(vehicle) -> bool` gives af kalderen (markarbejdets optaget-regel).
|
||||
|
||||
- [ ] **Step 1: Tilføj adapteren**
|
||||
|
||||
Tilføj nederst i `FS25_ADSmartPickup/adPool.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- spil ----
|
||||
|
||||
local function parkMarker(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
if type(index) ~= "number" or index < 1 then return nil end
|
||||
return adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
end
|
||||
|
||||
local function positionOf(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
||||
local function neededHpOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
if item == nil then return nil end
|
||||
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
||||
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
||||
end
|
||||
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
||||
end
|
||||
|
||||
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata.
|
||||
local function tractorHpOf(vehicle)
|
||||
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
||||
local kw = motor ~= nil and motor.peakMotorPower or nil
|
||||
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
||||
end
|
||||
|
||||
local function isAttached(vehicle)
|
||||
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||
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)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local implements, tractors = {}, {}
|
||||
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
||||
for _, vehicle in ipairs(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 and not isAttached(vehicle) then
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
||||
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
||||
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
||||
if role ~= nil then
|
||||
local x, z = positionOf(vehicle)
|
||||
table.insert(implements, {
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles, x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
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) then
|
||||
local x, z = positionOf(vehicle)
|
||||
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||
hp = tractorHpOf(vehicle), parkId = park.id})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Registrér modulet**
|
||||
|
||||
I `FS25_ADSmartPickup/modDesc.xml` efter linjen `<sourceFile filename="adHitch.lua"/>`:
|
||||
|
||||
```xml
|
||||
<sourceFile filename="adPool.lua"/>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Syntakstjek og tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adPool.lua >/dev/null && echo syntaks-ok && luajit tests/test_adPool.lua | tail -1`
|
||||
Expected: `syntaks-ok` og `ALLE TESTS OK`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adPool.lua FS25_ADSmartPickup/modDesc.xml
|
||||
git commit -m "feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `adHitch.lua` — hjemtur og "på hjemtur"-status
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adHitch.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (isReturning-register)
|
||||
- Test: `tests/test_adHitch.lua`, `tests/test_adPool.lua`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADHitch.route(adEnv, vehicle, markerIds, onDone)` (findes), `attachedImplements`/`rearImplements` (findes i adHitch.lua).
|
||||
- Produces:
|
||||
- `ADHitch.homeRoute(implementParkIds, tractorParkId) -> {id, ...}` (ren)
|
||||
- `ADHitch.rearImplementsOf(vehicle) -> {{object, z, name}}` (offentlig udgave af den lokale)
|
||||
- `ADHitch.returnHome(adEnv, tractor, onDone) -> bool` (false = intet puljeredskab bag / ingen parkering)
|
||||
- `ADPool.setReturning(vehicle, isOn)`, `ADPool.isReturning(vehicle) -> bool`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Tilføj i `tests/test_adHitch.lua` før `print(failures == 0`:
|
||||
|
||||
```lua
|
||||
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
|
||||
local home = H.homeRoute({4}, 2)
|
||||
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
|
||||
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
|
||||
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
|
||||
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
|
||||
```
|
||||
|
||||
Tilføj i `tests/test_adPool.lua` før `print(failures == 0`:
|
||||
|
||||
```lua
|
||||
-- på hjemtur: optaget indtil hjemme
|
||||
local tractorObject = {}
|
||||
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
|
||||
P.setReturning(tractorObject, true)
|
||||
check("hjemtur: markeret", P.isReturning(tractorObject), true)
|
||||
P.setReturning(tractorObject, false)
|
||||
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `luajit tests/test_adHitch.lua | tail -3; luajit tests/test_adPool.lua | tail -3`
|
||||
Expected: fejl `attempt to call field 'homeRoute' (a nil value)` og `attempt to call field 'isReturning'`
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
I `FS25_ADSmartPickup/adPool.lua`, efter `ADPool.blocked`:
|
||||
|
||||
```lua
|
||||
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
||||
local returning = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADPool.setReturning(vehicle, isOn)
|
||||
returning[vehicle] = isOn and true or nil
|
||||
end
|
||||
|
||||
function ADPool.isReturning(vehicle)
|
||||
return returning[vehicle] == true
|
||||
end
|
||||
```
|
||||
|
||||
I `FS25_ADSmartPickup/adHitch.lua`, i den rene del (efter `ADHitch.waitAfter`):
|
||||
|
||||
```lua
|
||||
-- Hjemtur: hvert puljeredskabs parkering (der sættes det af), så traktorens egen parkering.
|
||||
function ADHitch.homeRoute(implementParkIds, tractorParkId)
|
||||
local route, seen = {}, {}
|
||||
for _, id in ipairs(implementParkIds or {}) do
|
||||
if not seen[id] then seen[id] = true table.insert(route, id) end
|
||||
end
|
||||
if tractorParkId ~= nil then table.insert(route, tractorParkId) end
|
||||
return route
|
||||
end
|
||||
```
|
||||
|
||||
I spil-delen af `adHitch.lua`, lige efter `local function isImplementPark(...) ... end`:
|
||||
|
||||
```lua
|
||||
function ADHitch.rearImplementsOf(vehicle)
|
||||
return ADHitch.rearImplements(attachedImplements(vehicle))
|
||||
end
|
||||
|
||||
local function parkIdOf(adEnv, vehicle)
|
||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
```
|
||||
|
||||
Efter `function ADHitch.route(...) ... end`:
|
||||
|
||||
```lua
|
||||
-- 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)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local parkIds = {}
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do
|
||||
local parkId = parkIdOf(adEnv, implement.object)
|
||||
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
|
||||
if #parkIds == 0 then return false end
|
||||
local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor))
|
||||
ADPool.setReturning(tractor, true)
|
||||
ADHitch.route(adEnv, tractor, route, function(vehicle, ok, detail)
|
||||
ADPool.setReturning(vehicle, false)
|
||||
if onDone ~= nil then onDone(vehicle, ok, detail) end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `luajit tests/test_adHitch.lua | tail -1; luajit tests/test_adPool.lua | tail -1; luajit -bl FS25_ADSmartPickup/adHitch.lua >/dev/null && echo syntaks-ok`
|
||||
Expected: `ALLE TESTS OK` to gange og `syntaks-ok`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adHitch.lua FS25_ADSmartPickup/adPool.lua tests/test_adHitch.lua tests/test_adPool.lua
|
||||
git commit -m "feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Markarbejde — puljen som fallback og fasen `fetching`
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`getRigs` :132-170, `dispatch` :382-482, `follow` :1083-1143, `release` :278-283)
|
||||
- Modify: `FS25_ADSmartPickup/l10n/l10n_da.xml`, `FS25_ADSmartPickup/l10n/l10n_en.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADCpBridge.getSeedFruits/hasSeeds/isMechanicalWeeder(vehicle)`, `ADPool.collect(adEnv, isBusy)`, `ADPool.pairFor(...)`, `ADPool.blocked(nowMs)`, `ADPool.reserve/release/quarantine`, `ADPool.isReturning`, `ADHitch.fetch(adEnv, vehicle, markerId, onDone)`, `ADHitch.returnHome`.
|
||||
- Produces: assignment-felter `pool = {implementId, implementName, connectId}`, `phase = "fetching"`, `hitch = {done=bool, ok=bool, detail=any}`; field-årsager `spu_field_reason_noPoolTractor`, `spu_field_reason_fetching` (tooWeak som fritekst).
|
||||
|
||||
- [ ] **Step 1: Optaget-regel genbrugelig + hjemtur tæller som optaget**
|
||||
|
||||
I `adFieldWork.lua` før `function ADFieldWork.getRigs`:
|
||||
|
||||
```lua
|
||||
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
||||
-- eller på hjemtur fra puljen (sætter redskab af).
|
||||
local function isBusyVehicle(vehicle, id)
|
||||
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
||||
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||
end
|
||||
```
|
||||
|
||||
I `getRigs` erstat `busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)\n or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),` med:
|
||||
|
||||
```lua
|
||||
busy = isBusyVehicle(vehicle, id),
|
||||
```
|
||||
|
||||
og tilføj som sidste alternativ i `busyWhy`-kæden (før `or nil`):
|
||||
|
||||
```lua
|
||||
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fallback i dispatch**
|
||||
|
||||
I `ADFieldWork.dispatch`, lige efter `if rigs == nil then` og FØR `if #offNetwork > 0 ...`, indsæt:
|
||||
|
||||
```lua
|
||||
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
||||
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
|
||||
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
||||
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||
if pooled then return end
|
||||
if poolWhy == "tooWeak" or (poolWhy == "noPoolTractor" and why ~= "allBusy") then
|
||||
why, args = poolWhy, poolArgs
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Tilføj før `function ADFieldWork.dispatch` en ny funktion:
|
||||
|
||||
```lua
|
||||
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
||||
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
||||
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
||||
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
||||
end)
|
||||
local accept = function(implement)
|
||||
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
||||
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
||||
if role == "seeder" then
|
||||
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
||||
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
||||
end
|
||||
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
||||
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
||||
local required = ADFieldModel.requiredWidth(field, role)
|
||||
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
||||
end
|
||||
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
||||
if pair == nil then return false, why, args end
|
||||
local tractor, implement = pair.tractor, pair.implement
|
||||
local assignment = {
|
||||
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||
helper = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
|
||||
startX = tractor.x, startZ = tractor.z,
|
||||
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
||||
hitch = {done = false},
|
||||
}
|
||||
ADFieldJobs.state.assignments[tractor.id] = assignment
|
||||
ADPool.reserve(assignment, {implement.id, tractor.id})
|
||||
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
||||
assignment.hitch = {done = true, ok = ok, detail = detail}
|
||||
end)
|
||||
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
||||
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
||||
return true
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Frigiv reservation ved release**
|
||||
|
||||
I `ADFieldWork.release`, før `ADFieldJobs.state.assignments[assignment.vehicleId] = nil`:
|
||||
|
||||
```lua
|
||||
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Fasen fetching i follow**
|
||||
|
||||
I `ADFieldWork.follow`, efter linjen `wake(vehicle)` og før `if assignment.phase == "driving" then`, gør `if` til en kæde ved at indsætte:
|
||||
|
||||
```lua
|
||||
if assignment.phase == "fetching" then
|
||||
local hitch = assignment.hitch
|
||||
if not hitch.done then return end
|
||||
ADPool.release(assignment)
|
||||
if not hitch.ok then
|
||||
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
|
||||
ADPool.quarantine(assignment.pool.implementId, now())
|
||||
ADFieldWork.release(assignment)
|
||||
note(assignment.fieldId, {retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS,
|
||||
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
||||
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
return
|
||||
end
|
||||
-- koblet på: bredden med redskabet på (for bred -> hjem igen)
|
||||
local width = ADCpBridge.getWorkWidth(vehicle)
|
||||
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
||||
if not okWidth then
|
||||
ADFieldWork.release(assignment)
|
||||
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
||||
ADHitch.returnHome(adEnv, vehicle)
|
||||
return
|
||||
end
|
||||
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
||||
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
||||
note(assignment.fieldId, {reason = false})
|
||||
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
||||
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
||||
elseif assignment.phase == "driving" then
|
||||
```
|
||||
|
||||
(og fjern det oprindelige `if assignment.phase == "driving" then`, som nu er `elseif`.)
|
||||
|
||||
Tilføj også `fetching` til spiller-reglen øverst i `follow`, så AD's kørsel under hentning ikke tolkes som spillerens: `steeredByAi` er allerede sand, mens AD kører; ingen ændring nødvendig.
|
||||
|
||||
- [ ] **Step 5: Tekster**
|
||||
|
||||
`l10n/l10n_da.xml` (ved de andre `spu_field_reason_`):
|
||||
|
||||
```xml
|
||||
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
|
||||
<text name="spu_field_reason_fetching" text="henter '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
|
||||
```
|
||||
|
||||
`l10n/l10n_en.xml`:
|
||||
|
||||
```xml
|
||||
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
|
||||
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
|
||||
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
|
||||
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
|
||||
```
|
||||
|
||||
`tooWeak` bruger fritekst (hk står før rollen, og `fieldReasonText` sætter rollen først): i Step 2 erstattes linjen
|
||||
`why, args = poolWhy, poolArgs` med:
|
||||
|
||||
```lua
|
||||
if poolWhy == "tooWeak" then
|
||||
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
|
||||
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
||||
return
|
||||
end
|
||||
why, args = poolWhy, poolArgs
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Syntakstjek og alle tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||
Expected: `syntaks-ok`, ingen `FEJL`-linjer, `færdig`
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/l10n/l10n_da.xml FS25_ADSmartPickup/l10n/l10n_en.xml
|
||||
git commit -m "feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Markarbejde — hjemtur når opgaven slutter
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`finishAssignment` :834-862, fejlvejen i `follow` "nåede ikke" :1115-1120)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADHitch.returnHome(adEnv, tractor, onDone) -> bool`.
|
||||
- Produces: ingen nye navne.
|
||||
|
||||
- [ ] **Step 1: finishAssignment**
|
||||
|
||||
Erstat i `finishAssignment`:
|
||||
|
||||
```lua
|
||||
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
|
||||
```
|
||||
|
||||
med:
|
||||
|
||||
```lua
|
||||
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
||||
-- bag, så det virker også efter genindlæsning); ellers som før
|
||||
local pooled = not assignment.adHandlesEnd and not assignment.manual and ADHitch ~= nil
|
||||
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
||||
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
||||
end)
|
||||
local park = not pooled and 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 pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: "nåede ikke frem" og "tom" — også hjem med redskabet**
|
||||
|
||||
I `follow`, i grenen der slutter med
|
||||
|
||||
```lua
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
```
|
||||
|
||||
erstat de to linjer med:
|
||||
|
||||
```lua
|
||||
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
||||
local park = parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
```
|
||||
|
||||
I `finishAssignment`, i "tom såmaskine/sprøjte"-grenen efter `ADFieldWork.release(assignment)`, indsæt:
|
||||
|
||||
```lua
|
||||
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Syntakstjek og alle tests**
|
||||
|
||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||
Expected: `syntaks-ok`, ingen `FEJL`, `færdig`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldWork.lua
|
||||
git commit -m "feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Kø — længst ventende mark først
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldJobs.lua` (`stepField` dispatch-gren ~:421-426, `tick` løkke ~:486)
|
||||
- Test: `tests/test_adPool.lua` (dækket af `queueOrder`-tests i Task 1)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADPool.queueOrder(infos, waitingSince)`.
|
||||
- Produces: `ADFieldJobs.waitingSince = {[fieldId] = nowMs}`.
|
||||
|
||||
- [ ] **Step 1: Registrér ventetid**
|
||||
|
||||
I `adFieldJobs.lua` ved siden af `ADFieldJobs.lastDecision = {}`:
|
||||
|
||||
```lua
|
||||
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||
ADFieldJobs.waitingSince = {}
|
||||
```
|
||||
|
||||
I `stepField`, erstat dispatch-grenen:
|
||||
|
||||
```lua
|
||||
local before = ADFieldJobs.assignmentCount()
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
if ADFieldJobs.assignmentCount() > before then ADFieldJobs.lastDispatchAt = ADFieldJobs.now end
|
||||
```
|
||||
|
||||
med:
|
||||
|
||||
```lua
|
||||
local before = ADFieldJobs.assignmentCount()
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
if ADFieldJobs.assignmentCount() > before then
|
||||
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
elseif ADFieldJobs.waitingSince[info.id] == nil then
|
||||
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
|
||||
end
|
||||
```
|
||||
|
||||
I samme funktion, i grenen `decision.action == "advance"` efter `ADFieldJobs.setField(nextField)`:
|
||||
|
||||
```lua
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Kørsel i kø-rækkefølge**
|
||||
|
||||
I `ADFieldJobs.tick` erstat `for index, info in ipairs(ADFields.list()) do` med:
|
||||
|
||||
```lua
|
||||
local infos = ADFields.list()
|
||||
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
||||
for index, info in ipairs(infos) do
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Tests**
|
||||
|
||||
Run: `for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; luajit -bl FS25_ADSmartPickup/adFieldJobs.lua >/dev/null && echo syntaks-ok`
|
||||
Expected: ingen `FEJL`, `syntaks-ok`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/adFieldJobs.lua
|
||||
git commit -m "feat(pulje): marker der venter på en maskine betjenes længst-ventende først"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Version, build, deploy og in-game test
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (`<version>1.47.4.0</version>` → `1.48.0.0`)
|
||||
|
||||
- [ ] **Step 1: Bump version, alle tests, build, deploy**
|
||||
|
||||
```bash
|
||||
cd ~/fs25-adsmartpickup
|
||||
sed -i 's#<version>1\.47\.4\.0</version>#<version>1.48.0.0</version>#' FS25_ADSmartPickup/modDesc.xml
|
||||
for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done
|
||||
rm -f FS25_ADSmartPickup.zip; (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .)
|
||||
cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.new && mv ~/FS25-data/mods/.FS25_ADSmartPickup.new ~/FS25-data/mods/FS25_ADSmartPickup.zip
|
||||
unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -m1 '<version>'
|
||||
```
|
||||
|
||||
Expected: ingen `FEJL`, `<version>1.48.0.0</version>`
|
||||
|
||||
- [ ] **Step 2: Commit og push**
|
||||
|
||||
```bash
|
||||
git add FS25_ADSmartPickup/modDesc.xml
|
||||
git commit -m "chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde)"
|
||||
TOKEN=$(grep -A15 -i "^## Gitea" ~/credentials-vault.md | grep -oE "[a-f0-9]{40}" | head -1); git -c http.extraHeader="Authorization: Basic $(printf 'masterdraco:%s' "$TOKEN" | base64 -w0)" push -q gitea main
|
||||
```
|
||||
|
||||
- [ ] **Step 3: In-game test (savegame4, Golden Lands) — spilleren udfører, vi læser loggen**
|
||||
|
||||
Forudsætning: et redskab til markarbejde (fx kultivator/plov) med AD-parkering "X" og markør "Connect X" (bakkespor), Challengeren solo med egen AD-parkering, og en mark med markør der skal kultiveres/pløjes (plan auto eller Start).
|
||||
|
||||
Forventet i `log.txt` (grep `ADSmartPickup`):
|
||||
1. `F<n>: cultivator: 'Challenger MT635' henter '<redskab>' (pulje)`
|
||||
2. `kobling: 'Challenger MT635' koblede '<redskab>' på ved 'Connect X'`
|
||||
3. `F<n>: cultivator 'Challenger MT635' har '<redskab>' på — kører til <markør>`
|
||||
4. CP arbejder; `F<n>: cultivator '…' færdig`
|
||||
5. `F<n>: '…' sætter redskabet af og kører hjem`, `kobling: … koblede '<redskab>' af ved 'X'`, `… er færdig med turen (2 trin)`
|
||||
|
||||
Fejlscenarie: flyt redskabet 3 m væk fra sin plads → `kunne ikke koble … på` efter 8 s, traktoren kører hjem, fanen viser årsagen, redskabet springes over i 10 min.
|
||||
@@ -1,690 +0,0 @@
|
||||
# Flows/Fields: farvet liste + redigeringsvindue — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
|
||||
|
||||
**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
|
||||
|
||||
**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
|
||||
- Rækker er ≈835 px brede (som lagervisningen).
|
||||
- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
|
||||
- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
|
||||
- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
|
||||
- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
|
||||
- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
|
||||
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
|
||||
- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
|
||||
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rene statusfunktioner for flows (+ fælles farver)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
|
||||
- Test: `tests/test_flowStatus.lua` (ny)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
|
||||
`ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
||||
local V = ADFlowView
|
||||
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
|
||||
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
|
||||
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
|
||||
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
|
||||
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
|
||||
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
|
||||
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
|
||||
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
|
||||
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
|
||||
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
|
||||
|
||||
- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- status pr. flow (Flows-listen) ----
|
||||
|
||||
ADFlowView.LEVEL_COLORS = {
|
||||
running = {0.30, 0.75, 0.35, 1},
|
||||
waiting = {0.95, 0.80, 0.25, 1},
|
||||
error = {0.90, 0.35, 0.30, 1},
|
||||
stopped = {0.55, 0.55, 0.55, 1},
|
||||
}
|
||||
|
||||
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
|
||||
function ADFlowView.flowStatus(s)
|
||||
if s.stopped then return "stopped" end
|
||||
if s.warning ~= nil then return "error" end
|
||||
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
|
||||
if (s.running or 0) > 0 and s.reason == nil then return "running" end
|
||||
if (s.running or 0) > 0 or s.canStop then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
|
||||
- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rækker kan tegnes i et andet layout
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
|
||||
|
||||
- [ ] **Step 1: `addRow`** — replace body:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:addRow(prefab, data)
|
||||
local layout = self.rowTarget or self.boxLayout
|
||||
local row = prefab:clone(layout)
|
||||
row.spuLayout = layout
|
||||
FocusManager:loadElementFromCustomValues(row)
|
||||
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
|
||||
local option = row:getDescendantByName("option")
|
||||
if option ~= nil then
|
||||
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
||||
self.rowData[option] = data
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
|
||||
function SmartPickupFrame:withRowTarget(layout, fn)
|
||||
local previous = self.rowTarget
|
||||
self.rowTarget = layout
|
||||
local ok, err = pcall(fn, self)
|
||||
self.rowTarget = previous
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:clearPanelRows()
|
||||
for _, row in ipairs(self.panelRows or {}) do
|
||||
FocusManager:removeElement(row)
|
||||
row:delete()
|
||||
end
|
||||
self.panelRows = {}
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
|
||||
- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:colorLayout(layout)
|
||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
||||
if colors == nil or layout == nil then return end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local isEvenRow = false
|
||||
for _, element in ipairs(layout.elements) do
|
||||
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
||||
end
|
||||
isEvenRow = not isEvenRow
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:colorRows()
|
||||
self:colorLayout(self.boxLayout)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
|
||||
|
||||
```lua
|
||||
local layout = element.spuLayout or frame.boxLayout
|
||||
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
|
||||
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
||||
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
|
||||
```
|
||||
(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
|
||||
|
||||
- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
|
||||
- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Redigeringsvinduet (overlay + mixin)
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
|
||||
- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
|
||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
|
||||
- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
|
||||
- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
|
||||
|
||||
- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before `</Profiles>`):
|
||||
|
||||
```xml
|
||||
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
|
||||
<Profile name="spuOverlayBackdrop" extends="spuBar">
|
||||
<imageColor value="0 0 0 0.65"/>
|
||||
<positionOrigin value="topLeft"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<Profile name="spuOverlayPanel" extends="spuBar">
|
||||
<imageColor value="0.09 0.09 0.09 0.97"/>
|
||||
<positionOrigin value="topLeft"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<Profile name="spuPanelTitle" extends="spuCellBold">
|
||||
<textSize value="20px"/>
|
||||
</Profile>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: XML overlay** — directly after `</ScrollingLayout>` of `boxLayout`:
|
||||
|
||||
```xml
|
||||
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
|
||||
<GuiElement profile="baseReference" id="editOverlay" visible="false">
|
||||
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
|
||||
<Bitmap profile="spuOverlayPanel" id="editPanel">
|
||||
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
|
||||
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
|
||||
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
|
||||
</Bitmap>
|
||||
</GuiElement>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
|
||||
|
||||
```lua
|
||||
-- Smart Pickup — redigeringsvindue (v1.58)
|
||||
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
|
||||
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
|
||||
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
|
||||
|
||||
function SmartPickupFrame:initEditPanelButtons(i18n)
|
||||
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
|
||||
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
|
||||
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
|
||||
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:editPanelKind()
|
||||
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
|
||||
end
|
||||
|
||||
-- Vinduet dækker listens flade; panelet er centreret i den.
|
||||
function SmartPickupFrame:layoutEditPanel()
|
||||
local box = self.boxLayout
|
||||
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
|
||||
self.editBackdrop:setPosition(x, y)
|
||||
self.editBackdrop:setSize(w, h)
|
||||
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
|
||||
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
|
||||
self.editPanel:setSize(pw, ph)
|
||||
local top = 80 * g_pixelSizeScaledY
|
||||
self.editLayout:setSize(pw, ph - top)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openEditPanel(kind, id)
|
||||
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
|
||||
self:closeGuide()
|
||||
self.editOverlay:setVisible(true)
|
||||
self:layoutEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:closeEditPanel()
|
||||
if not self.editPanelOpen then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self:closeGuide()
|
||||
self:clearPanelRows()
|
||||
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
|
||||
function SmartPickupFrame:fillEditPanel(adEnv)
|
||||
self:clearPanelRows()
|
||||
local hooks = self:editPanelKind()
|
||||
if hooks == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then
|
||||
self.statusOverride = self.i18n:getText("spu_panel_gone")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self.editOverlay:setVisible(false)
|
||||
return
|
||||
end
|
||||
self.editTitle:setText(hooks.title(self, self.editPanelId))
|
||||
self:withRowTarget(self.editLayout, function(frame)
|
||||
if frame.guide ~= nil then
|
||||
frame:fillGuideRows(adEnv)
|
||||
else
|
||||
hooks.fill(frame, adEnv, frame.editPanelId)
|
||||
end
|
||||
end)
|
||||
self:colorLayout(self.editLayout)
|
||||
self.editLayout:invalidateLayout()
|
||||
self:refreshEditPanel(adEnv)
|
||||
end
|
||||
|
||||
-- Hvert sekund: status + knaptekster.
|
||||
function SmartPickupFrame:refreshEditPanel(adEnv)
|
||||
local hooks = self:editPanelKind()
|
||||
if hooks == nil or adEnv == nil then return end
|
||||
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
|
||||
local status = hooks.status(self, adEnv, self.editPanelId)
|
||||
self.editStatus:setText(status.text or "")
|
||||
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
|
||||
if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
|
||||
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
|
||||
self.panelDeleteButtonInfo.disabled = not hooks.canDelete
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelClose()
|
||||
self:safe(function(frame)
|
||||
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
|
||||
if frame.guide ~= nil then
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
return
|
||||
end
|
||||
frame:closeEditPanel()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelStartStop()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelKind()
|
||||
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelDelete()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelKind()
|
||||
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wire into frame** —
|
||||
- `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
|
||||
- `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
|
||||
- `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
|
||||
- `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
|
||||
- `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
|
||||
- `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
|
||||
- `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
|
||||
- Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
|
||||
- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
|
||||
- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
|
||||
- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
|
||||
- Modify: `l10n/*.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
|
||||
- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
|
||||
|
||||
- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
|
||||
|
||||
```xml
|
||||
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
|
||||
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
|
||||
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
|
||||
|
||||
```lua
|
||||
-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
|
||||
function SmartPickupFrame:toggleRun(adEnv, runId)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if adEnv == nil or run == nil then return end
|
||||
self.lastStartFailed = self.lastStartFailed or {}
|
||||
if ADRunsController.getRunStatus(adEnv, run).canStop then
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
self.lastStartFailed[run.id] = nil
|
||||
self.statusOverride = nil
|
||||
else
|
||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||
self.lastStartFailed[run.id] = started == 0 and skipped > 0
|
||||
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
|
||||
self:showOverride(text, reasons)
|
||||
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
|
||||
end
|
||||
end
|
||||
ADRunsController.save()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowLevel(adEnv, run)
|
||||
local row = ADRunsController.getFlowRow(adEnv, run)
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
|
||||
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
|
||||
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
|
||||
end
|
||||
```
|
||||
And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
|
||||
|
||||
- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
|
||||
self.flowListRows = {}
|
||||
local i18n = self.i18n
|
||||
if #ADRunsController.store.runs == 0 then
|
||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||
return
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
for _, run in ipairs(group.runs) do
|
||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
|
||||
local runId = run.id
|
||||
local actionCell = entry.cells[5]
|
||||
self:makeRowClickable(row, function(frame)
|
||||
-- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
|
||||
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||
frame:toggleRun(frame.getAdEnv(), runId)
|
||||
frame:requestRebuild()
|
||||
else
|
||||
frame.selectedRunId = runId
|
||||
frame:openEditPanel("flow", runId)
|
||||
end
|
||||
end)
|
||||
table.insert(self.flowListRows, entry)
|
||||
end
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:refreshFlowListRows(adEnv)
|
||||
local unpackFn = table.unpack or unpack
|
||||
for _, entry in ipairs(self.flowListRows or {}) do
|
||||
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
|
||||
if run ~= nil then
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
local level = self:flowLevel(adEnv, run)
|
||||
local color = ADFlowView.LEVEL_COLORS[level]
|
||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||
entry.cells[1]:setText(tostring(run.name))
|
||||
entry.cells[2]:setText(tostring(cells[2]))
|
||||
entry.cells[3]:setText(tostring(cells[5]))
|
||||
entry.cells[4]:setText(tostring(cells[6]))
|
||||
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
|
||||
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
|
||||
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
|
||||
|
||||
- [ ] **Step 4: `refreshFlowTab`** — replace body:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:refreshFlowTab(adEnv)
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
return
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
local running, total = 0, 0
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
total = total + 1
|
||||
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
|
||||
end
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
|
||||
|
||||
```lua
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
|
||||
canDelete = true,
|
||||
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
|
||||
title = function(frame, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
|
||||
end,
|
||||
fill = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
|
||||
end,
|
||||
status = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id)
|
||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
|
||||
end,
|
||||
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
|
||||
delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
|
||||
}
|
||||
```
|
||||
In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
|
||||
After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
|
||||
|
||||
- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
|
||||
- [ ] **Step 7: Run all tests + XML check.**
|
||||
- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Etape 1 build + in-game test
|
||||
|
||||
- [ ] **Step 1:** bump `modDesc.xml` `<version>` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'` shows 1.58.0.0.
|
||||
- [ ] **Step 2:** commit + push gitea main.
|
||||
- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Ren statusfunktion for marker
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/adFieldView.lua`
|
||||
- Test: `tests/test_fieldStatus.lua` (ny)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
|
||||
|
||||
- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
|
||||
|
||||
```lua
|
||||
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldView.lua")
|
||||
local V = ADFieldView
|
||||
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
|
||||
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
|
||||
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
|
||||
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
|
||||
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
|
||||
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
|
||||
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
|
||||
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
|
||||
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
|
||||
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
```
|
||||
(If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
|
||||
- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
|
||||
- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
|
||||
|
||||
```lua
|
||||
-- ---- status pr. mark (Fields-listen) ----
|
||||
|
||||
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
|
||||
ADFieldView.ERROR_REASONS = {
|
||||
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
|
||||
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
|
||||
}
|
||||
|
||||
-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
|
||||
function ADFieldView.fieldStatus(s)
|
||||
if not s.hasMarker then return "error" end
|
||||
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
|
||||
if s.awaiting or s.startRequested then return "waiting" end
|
||||
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
|
||||
if s.reasonKey ~= nil then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
```
|
||||
- [ ] **Step 4: Run all tests** — green.
|
||||
- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Fields-fanen som farvet liste + "field" i vinduet
|
||||
|
||||
**Files:**
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
|
||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
|
||||
- Modify: `l10n/*.xml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
|
||||
- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
|
||||
|
||||
- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
|
||||
|
||||
```xml
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
|
||||
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
|
||||
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
|
||||
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
|
||||
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
</Bitmap>
|
||||
```
|
||||
The header row (column titles) passes a 7th title `""`.
|
||||
- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
|
||||
|
||||
```lua
|
||||
function SmartPickupFrame:fieldLevel(id)
|
||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
||||
if field == nil or info == nil then return "stopped" end
|
||||
local reason = type(field.reason) == "table" and field.reason.key or nil
|
||||
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
|
||||
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
|
||||
reasonKey = reason})
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldActionText(id)
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
|
||||
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
|
||||
end
|
||||
```
|
||||
(If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
|
||||
- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
|
||||
|
||||
```lua
|
||||
self:makeRowClickable(cells[1].parent, function(frame)
|
||||
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
|
||||
frame:toggleField(fieldId)
|
||||
else
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:openEditPanel("field", fieldId)
|
||||
end
|
||||
end)
|
||||
```
|
||||
Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
|
||||
- [ ] **Step 4: Panel kind** —
|
||||
|
||||
```lua
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
||||
canDelete = false,
|
||||
exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
|
||||
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
|
||||
fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
|
||||
status = function(frame, adEnv, id)
|
||||
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
|
||||
startStop = function(frame, id) frame:toggleField(id) end,
|
||||
delete = function() end,
|
||||
}
|
||||
```
|
||||
`fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
|
||||
`getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
|
||||
- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
|
||||
- [ ] **Step 6: Run all tests + XML check.**
|
||||
- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Etape 2 build + in-game test
|
||||
|
||||
- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
|
||||
- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
|
||||
- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Behovsstyret forsyning ("Feed the factory") — design
|
||||
|
||||
Dato: 2026-09-21 · Status: godkendt af bruger
|
||||
|
||||
## Problem
|
||||
|
||||
En produktion med flere input (fx TMR-blander: hø, græs, ensilage, sukkerroer) kræver i dag
|
||||
én AutoDrive-traktor pr. vare, fordi AD's Pickup&Deliver kun kører én vare fra ét sted.
|
||||
Varerne ligger spredt over mange lokationer på kortet.
|
||||
|
||||
## Løsning
|
||||
|
||||
Én traktor holder alle input fyldt. For hver tur vælger moden den ingrediens aflæsningsmålet
|
||||
har mest brug for, finder en silo der har den, skifter AD's vare og sender traktoren derhen.
|
||||
|
||||
### Betjening (ingen ny GUI, ingen config-fil)
|
||||
|
||||
1. Aflæsningsmål = produktionens markør.
|
||||
2. Multi-vælg ingredienserne i AD's vare-dropdown (>1 vare = funktionen er aktiv).
|
||||
3. "Brug mapper" + "Rotér mål: pålæsning" slået til (krævet for at AD kalder `getNextPickup`).
|
||||
4. Start Pickup&Deliver. AD's loop-tæller virker uændret (N læs → parkér, 0 = uendeligt).
|
||||
|
||||
Med præcis én valgt vare er opførslen uændret (eksisterende mappe-rotation, mindst fyldte først).
|
||||
|
||||
### Logik pr. tur (i `getNextPickup`-wrapperen)
|
||||
|
||||
1. **Ingrediensliste pr. køretøj.** Ved >1 valgt vare gemmes listen i modens egen tilstand
|
||||
(`vehicle → {ingredients, narrowedTo}`). Derefter indsnævres AD's `selectedFillTypes` til den
|
||||
valgte vare, fordi AD ellers selv roterer vare ved triggeren
|
||||
(`ADStateModule:selectPreferredFillTypeFromFillLevels`).
|
||||
Ved næste kald: er AD's valg stadig `{narrowedTo}` → brug den gemte liste. Er det noget andet →
|
||||
brugeren har ændret valget: ny multi-liste erstatter, enkelt-valg sletter tilstanden.
|
||||
2. **Behov.** Find `UnloadingStation` nærmest anden markør (≤ 40 m). For hver ingrediens summeres
|
||||
`level`, `capacity` over `station.targetStorages`; `free = capacity - level`.
|
||||
Ingredienser stationen ikke understøtter, eller med `free < MIN_FREE_LITERS`, springes over.
|
||||
3. **Rangering.** `ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)` → højeste score først.
|
||||
4. **Kilde.** For ingredienserne i behovs-rækkefølge: blandt *alle* AD-markører (ikke kun én mappe)
|
||||
findes markører med farm-tilgængelig `LoadingStation` ≤ 40 m der har varen. Aflæsningsmålets
|
||||
egen placeable udelukkes som kilde. Rangering som eksisterende: eget lager før nabo-lån,
|
||||
mindst fyldte først. Første ingrediens med en kilde vinder.
|
||||
5. **Effekt.** `setFillType(vare)`, `selectedFillTypes = {vare}`, dirty-flag, returnér markørens
|
||||
`markerIndex`. Logges som `ADSmartPickup: '<traktor>' -> <markør> (<vare>, behov …)`.
|
||||
6. **Fallback.** Intet behov / ingen kilde / ingen station / fejl (`pcall`) → AutoDrives
|
||||
originale `getNextPickup` (ved fejl: `Logging.warning`). Aldrig crash.
|
||||
|
||||
## Filer
|
||||
|
||||
| Fil | Ansvar |
|
||||
|---|---|
|
||||
| `FS25_ADSmartPickup/adSupplyPlanner.lua` (ny) | Rene funktioner: `scoreNeed`, `rankNeeds`, `resolveIngredients` (tilstandslogik). Ingen spil-API. |
|
||||
| `FS25_ADSmartPickup/adSmartPickup.lua` | Spil-/AD-adaptere (station ved markør, lagerniveauer, kilde-søgning) + hook. Kilde-søgning generaliseres til at tage en markørliste. |
|
||||
| `FS25_ADSmartPickup/modDesc.xml` | Ny sourceFile før `adSmartPickup.lua`, version 1.1.0.0, beskrivelse. |
|
||||
| `tests/test_adSupplyPlanner.lua` (ny) | Enhedstests af de rene funktioner. |
|
||||
| `tests/test_adSmartPickup.lua` | Integration gennem hook'en med mocks; eksisterende 11 tests skal forblive grønne. |
|
||||
|
||||
## Tests (luajit, mocks)
|
||||
|
||||
- mest trængende ingrediens vælges; vare og AD-valg skiftes; rigtig markør returneres
|
||||
- ingrediens uden plads i målet springes over
|
||||
- ingrediens uden kilde springes over → næste i rækken
|
||||
- kilde i anden mappe end første markør findes
|
||||
- aflæsningsmålets egen station bruges ikke som kilde
|
||||
- gemt liste genbruges når AD's valg er indsnævret; brugerændring nulstiller/erstatter
|
||||
- én vare valgt → eksisterende opførsel uændret
|
||||
- intet behov / ingen aflæsningsstation / API-fejl → `ORIGINAL` (+ WARN ved fejl)
|
||||
|
||||
## Kendte begrænsninger
|
||||
|
||||
- AD læsser fuld vogn; er der mindre plads end ét læs, venter traktoren ved aflæsning (AD-standard).
|
||||
- Ingredienslisten lever kun i hukommelsen — efter savegame-reload multi-vælges varerne igen.
|
||||
- Forudsætter at målet er en placeable med aflæsningstrigger (produktionspunkt/silo/stald),
|
||||
ikke en blandevogn. Bekræftes ved første in-game test via log.txt.
|
||||
- Server-side logik; MP-klienter ser vareskiftet via AD's eget dirty-flag.
|
||||
@@ -1,156 +0,0 @@
|
||||
# Runs-UI ("Cow Feed") — design
|
||||
|
||||
Dato: 2026-09-22 · Status: godkendt af bruger (sektion 1–3) · Version: v1.8
|
||||
|
||||
## Problem
|
||||
|
||||
Al opsætning sker i dag pr. traktor i AutoDrives HUD: mål, multi-valg af varer (tændknap for
|
||||
behovsstyring), mapper + rotation, loop-tæller. Med fire ens "Fastrac 2135" til to stalde og flere
|
||||
Wait-puljer på vej (Cow/Pig/Sheep Wait) mangler overblik og central kontrol: *hvad* kører *hvor*
|
||||
med *hvilke varer*, og hvor venter de.
|
||||
|
||||
## Løsning
|
||||
|
||||
Et **run** er den centrale enhed: navn, aflæsningsmål, varer, Wait-pulje, tildelte traktorer,
|
||||
loop-tæller. En ESC-menu-fane ("Smart Pickup") viser og redigerer runs og starter/stopper dem.
|
||||
"Start" skriver run'et ind i hver traktors AutoDrive-tilstand og kalder AD's start — alt det
|
||||
eksisterende (behovsstyring, fabriks-inputs, Wait-punkter) virker uændret ovenpå, fordi det
|
||||
læser præcis den tilstand. Traktorer uden run kører som i dag.
|
||||
|
||||
Valgt tilgang: **run-lag oven på AutoDrives tilstand** (fravalgt: egen dispatcher der kalder AD's
|
||||
lav-niveau-opgaver — genopfinder P&D og knækker ved AD-opdateringer; konfig-fil uden UI — ikke det
|
||||
brugeren bad om).
|
||||
|
||||
## 1. Datamodel og lagring
|
||||
|
||||
```
|
||||
Run
|
||||
id løbenummer (stabilt, unikt i savegame)
|
||||
name "Cow Feed"
|
||||
targetWayPointId AD-waypoint-id for mål-markøren (stald/fabrik); vises som markørnavn
|
||||
ingredientMode "auto" | "manual"
|
||||
ingredients liste af fillType-NAVNE (kun ved manual), fx {"STRAW","FORAGE"}
|
||||
waitPoolGroup AD-mappenavn (fx "Cow Wait"); tom streng = alle Wait-mapper, nærmeste ledige
|
||||
loops 0 = uendeligt, N = antal ture (AD's loop-tæller)
|
||||
vehicleIds liste af køretøjs-referencer (se nedenfor)
|
||||
|
||||
Runtime (ikke gemt):
|
||||
isRunning afledt: mindst én tildelt traktor har AD aktiv og run-medlemskab
|
||||
runMembership vehicle -> run.id (svagt nøglet tabel)
|
||||
```
|
||||
|
||||
- **Køretøjs-reference:** AD's `uniqueId` (streng) når den findes på køretøjet, ellers savegame-id
|
||||
(`vehicle.currentSavegameId`). Navne bruges aldrig som nøgle (fire hedder "Fastrac 2135 4WS").
|
||||
- **WaitPool** er afledt, ikke gemt: alle AD-mapper med ordet "Wait" i navnet (`ADSupplyPlanner.isWaitMarker`).
|
||||
Omdøbning sker i AutoDrive; vi læser kun.
|
||||
- **Fil:** `savegameN/ADSmartPickup_runs.xml`, samme mappe som `AutoDrive_config.xml`. Skrives i
|
||||
spillets gem-hook (som AD), læses ved `loadMap`. Ukendte køretøjs-referencer droppes med loglinje;
|
||||
run'et beholdes. Runs der kørte ved gem genstartes IKKE ved load (AD gendanner selv aktive køretøjer).
|
||||
- **Regler:** en traktor kan kun være i ét run (tildeles den et andet, flyttes den — UI'et siger det).
|
||||
Singleplayer først; multiplayer kræver events og er uden for scope.
|
||||
|
||||
XML-format:
|
||||
|
||||
```xml
|
||||
<ADSmartPickupRuns version="1">
|
||||
<run id="1" name="Cow Feed" target="11066" ingredientMode="manual" ingredients="STRAW FORAGE"
|
||||
waitPool="Cow Wait" loops="0">
|
||||
<vehicle id="..."/>
|
||||
<vehicle id="..."/>
|
||||
</run>
|
||||
</ADSmartPickupRuns>
|
||||
```
|
||||
|
||||
## 2. UI — ESC-fane "Smart Pickup"
|
||||
|
||||
Forbillede: FarmOperationsDashboard (`TabbedMenuFrameElement`, XML-layout med `SmoothList`,
|
||||
`MultiTextOption`, `Button`; registreret via `g_gui:loadGui` + indsættelse i `InGameMenu.pagingElement`).
|
||||
|
||||
```
|
||||
┌ Smart Pickup ─────────────────────────────────────────────────────────────┐
|
||||
│ [Runs] [Wait-puljer] (under-faner) │
|
||||
├──────────────────┬────────────────────────────────────────────────────────┤
|
||||
│ RUNS │ RUN: Cow Feed ● Kører (2/2) │
|
||||
│ ▶ Cow Feed 2/2 │ Mål: [Cow 1 Food ▼] (AD-markør) │
|
||||
│ Cow 2 Feed 1/2 │ Varer: (•) Auto: alt målet tager / fabrikken │
|
||||
│ TMR Fabrik 1/1 │ ( ) Vælg: [x] Halm [x] TMR [ ] Vand │
|
||||
│ │ Wait-pulje: [Cow Wait ▼] (6 punkter)│
|
||||
│ [+ Nyt run] │ Ture (loop): [0 = uendeligt ◄►] │
|
||||
│ [Slet run] │ Traktorer: │
|
||||
│ │ Fastrac 2135 (#1) vogn: 1.000.000 l halm+TMR ✔ │
|
||||
│ │ Fastrac 2135 (#2) vogn: — INGEN VOGN ✖ │
|
||||
│ │ [+ Tilføj traktor ▼] [Fjern] │
|
||||
│ │ [▶ Start run] [■ Stop run] Status: 1 advarsel │
|
||||
└──────────────────┴────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Runs-liste (venstre):** alle runs med "kører X af Y". Valg → detaljer til højre. `+ Nyt run`
|
||||
opretter "Run N" (navn redigeres via AD-lignende tekstdialog). `Slet run` spørger ja/nej.
|
||||
- **Mål:** dropdown over AD-markører der ligger ved en aflæsningsstation (`getUnloadStationAtMarker`
|
||||
≠ nil) — ikke alle markører.
|
||||
- **Varer:** *Auto* = det eksisterende: fabrik → ønskede produktioners inputs (v1.6); stald → alle
|
||||
varer stalden tager (`getHusbandryIsFillTypeSupported`); andet mål → stationens understøttede varer.
|
||||
*Vælg* = afkrydsning blandt målets mulige varer.
|
||||
- **Wait-pulje:** dropdown "Alle (nærmeste ledige)" + hver mappe med "Wait" i navnet (antal punkter).
|
||||
- **Ture:** 0–99.
|
||||
- **Traktorer:** dropdown over farmens køretøjer med `vehicle.ad` (navn + `#løbenummer`). Pr. række:
|
||||
vogn-kapacitet (AD `getAllFillLevels`), hvilke af run'ets varer vognen kan bære, og ✔/✖ med tekst:
|
||||
"ingen vogn", "kan ikke bære: TMR", "N m fra vejnettet", "i run X (flyttes)".
|
||||
- **Start/Stop** gælder hele run'et; ✖-traktorer springes over med besked i rækken.
|
||||
- **Wait-puljer-fanen:** liste over Wait-mapper: antal punkter, hvilke traktorer holder hvor lige nu
|
||||
(fra `adUnloadWait`'s reservationer). Kun visning.
|
||||
- Live-opdatering hvert sekund mens siden er åben. Dansk + engelsk l10n. Ingen nye tastaturgenveje.
|
||||
|
||||
## 3. Start/Stop, integration, fejl og test
|
||||
|
||||
**Start run** (pr. tildelt traktor):
|
||||
1. Validér: vogn tilkoblet og kan bære ≥1 af run'ets varer (`AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`);
|
||||
≤ 30 m fra vejnettet (`ADGraphManager:getDistanceFromNetwork`); mål-markør findes. Fejl → ✖ på rækken,
|
||||
traktoren springes over, run'et fortsætter med de andre.
|
||||
2. Skriv AD-tilstand: `stateModule:setMode(AutoDrive.MODE_PICKUPANDDELIVER)`;
|
||||
`setSecondMarkerByWayPointId(target)`; `setFirstMarkerByWayPointId(nærmeste markør med læssestation)`
|
||||
(kun en gyldig start — moden vælger reel kilde pr. tur); `selectedFillTypes` = run'ets varer
|
||||
(Auto: udledt liste) + `raiseDirtyFlag`; loop-tæller; per-køretøj `vehicle.ad.settings.useFolders`
|
||||
= til og `rotateTargets` = kun pålæsning (`current` og `new`).
|
||||
3. `vehicle:startAutoDrive()`.
|
||||
4. `runMembership[vehicle] = run.id`.
|
||||
|
||||
**Stop run:** `vehicle:stopAutoDrive()` pr. tildelt traktor; medlemskab bevares (konfigureret, ikke kørende).
|
||||
|
||||
**Integration:** de eksisterende hooks læser AD-tilstanden uændret. Run'et læses to steder:
|
||||
(a) `adUnloadWait`: Wait-kandidater begrænses til run'ets mappe, hvis sat; (b) `choosePickup`:
|
||||
for run-traktorer tages ingredienslisten fra run'et (Auto/manual) i stedet for "≥2 valgt i dropdown",
|
||||
så tændknappen ikke længere kræves. Traktorer uden run: præcis som i dag.
|
||||
|
||||
**Fejlhåndtering:** al UI-kode og Start/Stop i `pcall`; fejl vises i status-linjen og logges med
|
||||
`ADSmartPickup:`-præfiks; aldrig crash. Manglende AD-API → fanen viser "AutoDrive-API ikke fundet",
|
||||
resten af moden upåvirket.
|
||||
|
||||
**Test:** rene funktioner i luajit (run-model: opret/omdøb/tildel/flyt; validering; Auto-vareliste;
|
||||
XML ↔ tabel; kandidat-filtrering pr. Wait-pulje). GUI og AD-skrivning verificeres in-game efter
|
||||
tjekliste: opret run → tildel 2 traktorer (én uden vogn) → Start (én ✖) → Stop → gem/load → run bevaret.
|
||||
|
||||
**Afgrænsning (YAGNI):** singleplayer; ingen kilde-begrænsning pr. run; ingen omdøbning af
|
||||
Wait-mapper; ingen auto-genstart ved load; ingen genvejstast-dialog.
|
||||
|
||||
## Filer (forventet)
|
||||
|
||||
- `FS25_ADSmartPickup/adRuns.lua` — run-model + XML (rene funktioner hvor muligt)
|
||||
- `FS25_ADSmartPickup/adRunsController.lua` — Start/Stop, validering mod AD/spil-API, medlemskab
|
||||
- `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` + `gui/SmartPickupFrame.xml` + `gui/guiProfiles.xml`
|
||||
- `FS25_ADSmartPickup/l10n/l10n_da.xml`, `l10n_en.xml`
|
||||
- `tests/test_adRuns.lua`
|
||||
- Tilpasninger i `adSmartPickup.lua` (ingredienser fra run) og `adUnloadWait.lua` (pulje-filter)
|
||||
|
||||
## Efterfølgende (ikke i v1.8): læs-bytte fra Wait
|
||||
|
||||
In-game 2026-09-22 21:55: begge stalde fulde af halm, mangler TMR; alle fire traktorer venter ved
|
||||
Cow Wait med vognene fulde af halm → ingen kan hente TMR (deadlock).
|
||||
|
||||
Design: mens en traktor venter, tjekkes målets behov. Hvis målet er fuldt for vognens vare, og en
|
||||
ANDEN vare i traktorens ingrediensliste mangler så meget at et fuldt læs passer (hysterese), og
|
||||
vognen kan bære den: find en silo (lager-placeable med aflæsningstrigger ved en AD-markør, fri plads
|
||||
≥ rest, rute til/fra) — foretræk den moden ellers henter varen fra — og omdiriger aflæsningsopgaven
|
||||
dertil. AD læsser af, vognen er tom, næste tur vælger moden den manglende vare efter behov. Log:
|
||||
"bytter læs: N l STRAW tilbage i <silo>, målet mangler FORAGE". Ingen silo → bliv ved Wait.
|
||||
Kræver at traktoren har begge varer i sin liste (run eller multi-valg).
|
||||
@@ -1,92 +0,0 @@
|
||||
# Flow-tavle — design
|
||||
|
||||
Dato: 2026-09-23 · Status: godkendt af bruger (grundform valgt: flow-tavle) · Version: v1.13
|
||||
|
||||
## Problem
|
||||
|
||||
Opsætningen er markør-baseret: brugeren vælger AD-markører som kilder og mål, og moden gætter
|
||||
bygningen ud fra markørens geometri (trigger bag næsen, 40 m, 12 m). Fejl viser sig som
|
||||
"kilde-markør 10873 er ikke …" i stedet for "Kostald 1", først når run'et starter. Forsyning og
|
||||
Udkørsel er to faner med hver sin lange rulleside, Oversigt er en tredje. Brugeren: "det er noget rod".
|
||||
|
||||
## Løsning
|
||||
|
||||
Én tavle med ét flow pr. række: **Vare · Fra · Til · Regel · Traktorer · Status**. Fra og Til vælges
|
||||
som **bygninger**; moden finder selv markøren ved bygningens læsse-/aflæsningstrigger og viser den.
|
||||
Redigering sker i en detail-blok under tavlen for det valgte flow. Faner: **Flows** · **Lagre**.
|
||||
Motoren (planlægger, hooks, XML-model) er uændret: et flow ER et run; bygninger oversættes til
|
||||
markører når run'et startes og planlægges.
|
||||
|
||||
Fravalgt: "Bygninger først" (runs implicit → svært at se hvad der kører) og "Sætnings-run"
|
||||
(mindst ændring, men fanerne forbliver adskilte).
|
||||
|
||||
## 1. Bygninger (`adBuildings.lua`, spil-adapter)
|
||||
|
||||
`ADBuildings.list(adEnv, farmId)` → liste af bygninger gården kan bruge:
|
||||
|
||||
```
|
||||
{ id, name, kind = "factory"|"silo"|"husbandry"|"objectStorage"|"sell",
|
||||
placeable, loadMarker = {id, name, distance} | nil, unloadMarker = {…} | nil,
|
||||
canBeSource = bool, canBeTarget = bool, canSell = bool }
|
||||
```
|
||||
|
||||
- Kilde: fabrik, silo, stald (læssestation), objektlager. Mål (forsyning): stald, fabrik.
|
||||
Salg: SellingStation der er et rigtigt salgssted (ikke fabrikkers/staldes indleveringer).
|
||||
Siloer og palleskure er automatiske lagermål (som i dag) og vælges ikke.
|
||||
- `id` = `placeable.uniqueId`; mangler det, `"pos:" .. afrundet x/z`.
|
||||
- Markør-opslag: for hver AD-markør (Wait-markører udelukket) afstanden til bygningens
|
||||
læsse-triggere (loadTriggers.triggerNode) og aflæsnings-triggere (unloadTriggers exactFillRootNode/
|
||||
triggerNode/aiNode) samt objektlagerets/pallespawnerens node; nærmeste ≤ 40 m vinder.
|
||||
Siloer bruger det strenge opslag (bag næsen) for læssemarkøren, som i dag.
|
||||
- Én scanning pr. sekund højst (cache), da fanen spørger ved hver opdatering.
|
||||
- `ADBuildings.find(adEnv, farmId, id)`, `ADBuildings.byMarker(adEnv, farmId, wayPointId)` (omvendt
|
||||
opslag til migration).
|
||||
|
||||
## 2. Model
|
||||
|
||||
Run får `sourceBuildingIds` og `targetBuildingIds` (lister af bygnings-id). De eksisterende
|
||||
`sourceWayPointIds`/`targetWayPointIds` bevares som **afledte** felter: controlleren udfylder dem fra
|
||||
bygningerne ved Start og ved hver plan (`ADRunsController.resolveMarkers(adEnv, run)`), så
|
||||
planlægger og hooks er uændrede. XML-version 3 skriver `#sourceBuildings`/`#targetBuildings`;
|
||||
version 2-filer læses, og bygninger udledes fra markørerne ved første indlæsning
|
||||
(`ADBuildings.byMarker`); kan en markør ikke oversættes, beholdes den som rå markør.
|
||||
|
||||
Forsyning: Fra = "alle siloer" (ingen valg; sources er alle markører som i dag). Til = bygninger.
|
||||
Udkørsel: Fra = bygninger. Til = politik pr. vare (Lager/Salg/Salg, ellers lager) — målet vælges
|
||||
automatisk pr. tur som i dag.
|
||||
|
||||
## 3. Fanen Flows
|
||||
|
||||
**Tavle** (øverst, tekstrækker, én pr. run, den valgte markeret med "▶"):
|
||||
`Mælk · Kostald 1 + Kostald 2 → Mejeri (salg) · pris ≥ 90 % · Virtus · venter: 42 % af top`
|
||||
- Vare: run'ets varer (forsyning auto = "behov"), Fra/Til = bygningsnavne (+ "!" hvis en bygning
|
||||
mangler markør), Regel = kort tekst, Traktorer = navne, Status = én linje (som Oversigt i dag:
|
||||
kører/holder/venter/begrundelse/advarsel).
|
||||
- Under tavlen: Wait-puljer (som i dag) og en linje "Vælg flow ovenfor for at rette".
|
||||
|
||||
**Detail-blok for valgt flow** (efter tavlen; run-vælgeren `Flow:` er en MultiTextOption):
|
||||
- *Retning*: Ind til bygning / Ud fra bygning (klonet Ja/Nej-række).
|
||||
- *Fra* (kun Ud): afkrydsning pr. bygning der kan være kilde: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `— INGEN MARKØR inden for 40 m`.
|
||||
- *Til* (kun Ind): afkrydsning pr. stald/fabrik med samme markørtekst.
|
||||
- *Varer*: Ind: Auto/Vælg + afkrydsning; Ud: politik pr. vare med mængde og pris-% (som i dag).
|
||||
- *Regler*: Ind: ture, kør hjem under; Ud: sælg fra, lager over, saml op.
|
||||
- *Vent ved*: Wait-pulje.
|
||||
- *Traktorer*: som i dag (låst når vognen ikke passer).
|
||||
- Knapper: Nyt flow / Omdøb / Slet / Start-Stop (som i dag). Nyt flow spørger navn og starter som "Ind".
|
||||
|
||||
Fanen **Lagre** uændret. Oversigt og de to gamle faner udgår.
|
||||
|
||||
## 4. Validering og fejl
|
||||
|
||||
- Bygning uden markør: rækken siger det, tavlen viser "!" ved navnet, Start afviser med
|
||||
`noMarker:<bygning>` i statuslinjen ("Kostald 2 mangler en AD-markør ved mælketanken").
|
||||
- Bygning der er forsvundet (solgt): udgår af listen; run'et beholder id'et og viser "(mangler)".
|
||||
- Alle eksisterende afvisningsgrunde uændrede.
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- `adBuildings`: kind pr. placeable, markør-opslag (nærmeste ≤ 40 m, Wait udelukket, silo strengt),
|
||||
byMarker, id-fallback.
|
||||
- model: v3 roundtrip, v2-migration med byMarker-oversættelse, rå markør bevares.
|
||||
- controller: resolveMarkers udfylder waypoint-lister; noMarker-validering; Start med bygninger.
|
||||
- GUI in-game (savegame 3 og 2).
|
||||
@@ -1,201 +0,0 @@
|
||||
# Udkørsel af produkter — design
|
||||
|
||||
Dato: 2026-09-23 · Status: godkendt af bruger (afsnit 1–4) · Version: v1.12
|
||||
|
||||
## Problem
|
||||
|
||||
Modden styrer i dag kun forsyning *ind* til stalde og fabrikker. Fabrikkernes output (sukker,
|
||||
TMR, halmvarer på paller) og varer der står på lager skal køres *ud*: til en silo som
|
||||
mellemlager, eller til et salgssted — men kun når prisen er god, så man ikke sælger i bund.
|
||||
Uden det løber fabrikkernes udgangslager fuldt, og produktionen stopper. Paller ligger ofte i
|
||||
objektlagre (fx HVF-pakkens palle-/ballelager) hvor man normalt klikker "tag ud" manuelt.
|
||||
|
||||
## Løsning
|
||||
|
||||
Et run får en **retning**: *Forsyning* (som i dag) eller *Udkørsel*. Et udkørsels-run har en
|
||||
liste af **kilder** (fabrik, silo eller objektlager), en **vareliste** med politik pr. vare
|
||||
(Lager / Salg / Salg, ellers lager / Fra), en **pristærskel** (% af årets toppris), en
|
||||
**lagertærskel** (% fyldning) og valget **opsamlingsrunde**. Før hver tur vurderer en ren
|
||||
planlægger om en tur er berettiget, vælger vare(r) + mål, og sætter AutoDrives tilstand;
|
||||
AutoDrive kører, læsser (også paller via Universal Autoload) og læsser af. Ingen tur → traktoren
|
||||
holder ved et Wait-punkt og prøver igen hvert 5. sekund.
|
||||
|
||||
Valgt tilgang: nyt modul der wrapper AutoDrives læsseopgave (samme mønster som Wait-modulet
|
||||
wrapper aflæsseopgaven). Fravalgt: AutoDrives egen "automatisk aflæsningsmål" (ingen pristærskel,
|
||||
ingen lager-mulighed, ingen ventelogik) og egen kørselsstyring med DriveTo-opgaver (genopfinder
|
||||
AutoDrives læsse-/pallehåndtering).
|
||||
|
||||
## 1. Faner og datamodel
|
||||
|
||||
### Faner i ESC-siden
|
||||
|
||||
1. **Oversigt** (som i dag): alle runs med status, traktorernes aktivitet, behovsrække. Udkørsels-
|
||||
runs vises med prislinje i stedet for behov: `SUGAR 412 kr (78 % af top) → venter`.
|
||||
2. **Forsyning**: de nuværende runs, samme indhold, grupperet i blokke med overskrift:
|
||||
*Run* (vælger + Nyt/Omdøb/Slet) · *Mål og varer* · *Ventepunkter og ture* · *Traktorer* ·
|
||||
*Status* (+ Start/Stop).
|
||||
3. **Udkørsel**: *Run* · *Kilder* (liste af markører, tilføj/fjern) · *Varer* (én række pr. vare:
|
||||
navn, politik, aktuel pris i % af top) · *Regler* (pristærskel 50–100, standard 90;
|
||||
lagertærskel 50–95, standard 80; opsamlingsrunde Ja/Nej) · *Ventepunkter* · *Traktorer* ·
|
||||
*Status* (+ Start/Stop).
|
||||
|
||||
Run-vælger, Wait-pulje, traktorliste, knapper og statuslinje er fælles byggeklodser; hver fane
|
||||
viser kun runs med sin egen `kind`.
|
||||
|
||||
### Kilder
|
||||
|
||||
Kilde = AD-markør. Kildetypen genkendes ved markøren (nærmeste inden for 40 m, som triggere i
|
||||
dag):
|
||||
|
||||
- **Fabrik** (produktionspunkt): tilgængeligt pr. vare = udgangslager + paller af varen der
|
||||
allerede står ved pallespawneren (inden for 25 m af spawner-noden).
|
||||
- **Silo** (spec_silo med læssestation): tilgængeligt = lagerniveau for varen.
|
||||
- **Objektlager** (spec_objectStorage): tilgængeligt = antal × fyldning pr. palle for varen
|
||||
(`objectInfos[*].objects[*].palletAttributes`). Udtag sker automatisk, se afsnit 3.
|
||||
|
||||
Varelisten for et run = alle varer kilderne rummer eller producerer, som mindst én tildelt vogn
|
||||
kan bære. Bulk-varer kræver tipvogn/tank; pallevarer kræver Universal Autoload.
|
||||
|
||||
### Datamodel (adRuns.lua, rene funktioner, ny store ved hver ændring)
|
||||
|
||||
```
|
||||
Run (fælles)
|
||||
id, name, waitPoolGroup, vehicleIds, kind = "supply" | "outbound"
|
||||
Run (supply, uændret)
|
||||
targetWayPointId, targetWayPointIds, ingredientMode, ingredients, loops, returnBelowPercent
|
||||
Run (outbound)
|
||||
sourceWayPointIds liste af AD-waypoint-id'er (1..n)
|
||||
products liste af {fillType = NAVN, policy = "store"|"sell"|"sellElseStore"|"off"}
|
||||
sellAtPercent 50..100, standard 90
|
||||
storeAbovePercent 50..95, standard 80
|
||||
collectRound bool, standard false
|
||||
```
|
||||
|
||||
XML-version 2 i samme `ADSmartPickup_runs.xml`. Version 1-filer læses og får `kind="supply"`.
|
||||
Ukendte varer i `products` (mod fjernet) ignoreres ved indlæsning med én log-linje.
|
||||
|
||||
## 2. Turvurdering og prisregel
|
||||
|
||||
`adOutboundPlanner.lua` er rent Lua (ingen spil-API). Input: run, vognens kapacitet (liter) og
|
||||
om den har UAL, kildestatus pr. vare `{available, level, capacity}` pr. kilde, prisstatus pr.
|
||||
vare `{percent, stationId, reachable}`. Output: en tur eller `nil` + begrundelse.
|
||||
|
||||
Pr. vare:
|
||||
|
||||
- **Tilgængeligt**: sum over kilderne.
|
||||
- **Fuldt læs**: tilgængeligt ≥ vognens kapacitet.
|
||||
- **Lager højt**: mindst én kilde har `level/capacity ≥ storeAbovePercent/100` for varen.
|
||||
- **Pris-%**: bedst betalende *nåelige* salgssted der tager varen (adPrices). Toppris =
|
||||
stationens aktuelle grundpris for varen × (årets højeste periodefaktor ÷ nuværende periodes
|
||||
faktor). Pris-% = aktuel effektiv pris ÷ toppris. Stor efterspørgsel giver > 100 % og opfylder
|
||||
altid reglen. Mangler stationens grundpris, bruges `fillType.pricePerLiter × højeste faktor ×
|
||||
sværhedsgradens multiplikator` som toppris.
|
||||
|
||||
Beslutning, i rækkefølge:
|
||||
|
||||
1. **Salgskandidater**: politik ∈ {sell, sellElseStore}, pris-% ≥ sellAtPercent, tilgængeligt > 0.
|
||||
Kører **uanset mængde**. Rangering: højeste pris-% først, derefter størst tilgængeligt.
|
||||
2. **Lagerkandidater**: politik ∈ {store, sellElseStore} og (fuldt læs ELLER lager højt).
|
||||
Rangering: højeste fyldningsgrad først, derefter størst tilgængeligt.
|
||||
3. Ingen kandidat → hold ved Wait. Log én gang pr. ændret begrundelse:
|
||||
`venter: SUGAR 78 % af top (kræver 90), lager 41 %`.
|
||||
|
||||
**Opsamlingsrunde** (`collectRound`):
|
||||
|
||||
- Bulk-vogn: én vare, men fra flere kilder efter tur indtil vognen er fuld (afsnit 3).
|
||||
- UAL-vogn: alle kandidater med samme mål på én tur; vognens vareliste sættes til dem alle, og
|
||||
AutoDrive skifter selv vare når der ikke kommer flere af den første.
|
||||
|
||||
**Mål**:
|
||||
|
||||
- Salg: det salgssted der betaler bedst for hovedvaren (første i rangeringen), tager alle rundens
|
||||
varer, og kan nås begge veje.
|
||||
- Lager: nærmeste silo-markør med læsse+aflæsningsstation der tager varen, fri plads ≥ hele læsset,
|
||||
ikke selv kilde, fabrik eller stald, rute begge veje (samme regler som læs-byttet).
|
||||
|
||||
Prisen tjekkes kun ved turens start.
|
||||
|
||||
## 3. Kørsel, hold og pallelæsning
|
||||
|
||||
`adOutbound.lua` med to hooks:
|
||||
|
||||
**Start (controller).** Som forsyning: PickupAndDeliver, rotation "kun pålæsning", mapper til.
|
||||
1. markør = første kilde, 2. markør = første kilde (pladsholder, sættes pr. tur). Turen
|
||||
registreres i `activePickups`, så "stå og læs"-grebet og 30 s-sikkerhedsventilen gælder.
|
||||
|
||||
**Hook 1 — næste pålæsning** (det eksisterende `getNextPickup`-wrap): udkørsels-traktor →
|
||||
planlæggeren spørges. Tur → vognens `selectedFillTypes` = turens varer, 2. markør = målet,
|
||||
kilden returneres. Ingen tur → ledigt Wait-punkt fra run'ets pulje returneres (reservation +
|
||||
rutetjek via adWaitPool), traktoren markeres "holder".
|
||||
|
||||
**Hook 2 — læsseopgaven** (prepend på `LoadAtDestinationTask.update`):
|
||||
|
||||
- *Holder ved Wait:* traktoren stoppes ved punktet; planlæggeren spørges hvert 5. sekund. Tur →
|
||||
opgaven omdirigeres til kilden (`redirect`), varer + 2. markør sættes, "holder" slippes.
|
||||
- *Ved objektlager:* når markøren er nået, traktoren står stille og AutoDrive har aktiveret UAL:
|
||||
kald `placeable:removeAbstractObjectsFromStorage(objectInfoIndex, 1, nil)` for turens vare;
|
||||
vent til lagerets antal falder OG UAL's antal læssede stiger; gentag. Stop når
|
||||
`ualIsFull()`, lageret er tomt for varen, eller intet er sket i 30 s → `task:finished()`.
|
||||
Ved runde med flere pallevarer tages varerne efter tur. `connection = nil` så
|
||||
AutomaticBaleStorage-moddens kø ikke aktiveres.
|
||||
- *Kør uanset mængde:* stiger vognens fyldning ikke i 15 s og vognen ikke er tom → `task:finished()`.
|
||||
- *Runde, bulk:* når læsning stopper ved kilde A med fri plads ≥ 10 % og en anden kilde i listen
|
||||
har samme vare, omdirigeres opgaven til kilde B i stedet for at afslutte. Hver kilde højst én
|
||||
gang pr. tur.
|
||||
|
||||
**Aflæsning** er AutoDrives egen (tip i silo; tip eller UAL-aflæsning ved salgssted).
|
||||
`adUnloadWait` springer udkørsels-traktorer over (run'ets `kind`). Efter aflæsning går AutoDrive
|
||||
selv til næste pålæsning → hook 1 igen. Stop som i dag.
|
||||
|
||||
`adWaitPool.lua` udskilles fra `adUnloadWait.lua`: reservationer (markør-id → køretøj),
|
||||
`hasRouteTo`/`hasRouteBothWays`, `redirect`, kandidatliste for en pulje. Begge moduler bruger den.
|
||||
|
||||
## 4. Fejl, advarsler og tests
|
||||
|
||||
**Uopnåeligt salgssted / ingen silo.** Kan det bedst betalende salgssted ikke nås, tages det
|
||||
næstbedste nåelige. Kan intet nås (eller ingen silo med plads ved Lager), vises en rød
|
||||
AutoDrive-notifikation på skærmen
|
||||
(`AutoDriveMessageEvent.sendMessageOrNotification(vehicle, ERROR, …)`):
|
||||
`ADSmartPickup: 'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR – tjek vejnettet`.
|
||||
Samme tekst i AutoDrives notifikations-historik, log.txt (WARN) og run'ets statuslinje.
|
||||
Traktoren holder ved Wait og prøver igen hvert 5. sekund. Gentages først når traktor, station
|
||||
eller vare ændrer sig.
|
||||
|
||||
**Fejlhåndtering:** spil-API-kald i pcall; fejl → hold ved Wait + WARN én gang, aldrig
|
||||
AutoDrive-stop. Validering ved Start (nye grunde): `noSource` (markør uden genkendelig kilde),
|
||||
`needsAutoload` (pallevare uden UAL), `noProducts` (ingen vare med politik ≠ Fra). Objektlager
|
||||
der ikke spawner → 30 s-vagt afslutter læsningen med det der er.
|
||||
|
||||
**Tests (luajit, uden spil):**
|
||||
|
||||
- planlægger: prisregel, tærskler, alle fire politikker, rangering, runde bulk/UAL, uanset mængde
|
||||
- priser: toppris, pris-%, stor efterspørgsel > 100 %, fallback uden stationspris
|
||||
- model + XML v2 + migration fra v1
|
||||
- objektlager-udtag som tilstandsmaskine mod fake-lager (fuld, tom, stall)
|
||||
- kilde-genkendelse mod fake-markører (fabrik/silo/objektlager, paller ved spawner)
|
||||
- controller-validering (noSource, needsAutoload, noProducts)
|
||||
|
||||
**In-game (savegame 3):** sukker → salgssted ved høj pris; TMR → Farma-silo; halmkurve fra
|
||||
fabrik og fra objektlager → halm-salgsstedet (UAL); uopnåeligt salgssted → advarsel på skærm.
|
||||
|
||||
## Filer (forventet)
|
||||
|
||||
```
|
||||
FS25_ADSmartPickup/
|
||||
adRuns.lua kind, sourceWayPointIds, products, tærskler, collectRound; XML v2
|
||||
adRunsStorage.lua v2 læs/skriv, v1-migration
|
||||
adOutboundPlanner.lua NY, ren
|
||||
adPrices.lua NY, ren del + spiladapter
|
||||
adSources.lua NY, kilde-genkendelse + lagerstatus
|
||||
adOutbound.lua NY, hooks, objektlager-udtag, hold, runde
|
||||
adWaitPool.lua NY, udskilt fra adUnloadWait.lua
|
||||
adUnloadWait.lua bruger adWaitPool; springer outbound over
|
||||
adRunsController.lua start/validering for outbound
|
||||
adSmartPickup.lua source() af nye filer; getNextPickup-wrap kalder adOutbound
|
||||
gui/SmartPickupFrame.lua faner + fælles blokke
|
||||
gui/SupplyTab.lua NY
|
||||
gui/OutboundTab.lua NY
|
||||
l10n/l10n_da.xml, l10n_en.xml, README.md
|
||||
tests/test_adOutboundPlanner.lua, test_adPrices.lua, test_adSources.lua,
|
||||
tests/test_adOutbound.lua, test_adRuns.lua (v2), test_adRunsController.lua (outbound)
|
||||
```
|
||||
@@ -1,151 +0,0 @@
|
||||
# Markarbejde, etape 1: slåafgrøder (græs, kløver, lucerne)
|
||||
|
||||
Dato: 2026-09-25 · Mod: FS25_ADSmartPickup · Branch: `feature/markarbejde`
|
||||
|
||||
## Mål
|
||||
|
||||
Når en græs-, kløver- eller lucernemark er klar, sender Smart Pickup selv maskinerne ud. Slåmaskinen slår
|
||||
marken via Courseplay. Ved **ensilage** samler en læssevogn skårene op og kører dem til den rigtige silo.
|
||||
Ved **hø** vender en vender først græsset (og en river river det evt. sammen), og derefter samles høet op.
|
||||
Alt kørsel på vej sker med AutoDrive. Alt arbejde på marken sker med Courseplay.
|
||||
|
||||
Succes: en mark sat til *Auto* går fra "klar" til "færdig" med græsset i en silo, uden at spilleren rører
|
||||
noget, og menuen viser hele tiden, hvad der sker og hvorfor.
|
||||
|
||||
Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandling og såning, baller.
|
||||
|
||||
## Arbejdsdeling
|
||||
|
||||
| Del | Ansvar |
|
||||
|---|---|
|
||||
| **Smart Pickup (vores)** | Hvilken mark, hvilket trin, hvilken maskine, hvilken silo. Holder status og viser den |
|
||||
| **AutoDrive** | Kørsel på vej til markens `F`-markør og til silo; overtager ved "vogn fuld" og genstarter Courseplay |
|
||||
| **Courseplay** | Markgrænse, kursus, selve arbejdet på marken, "fuld"/"færdig"-signaler |
|
||||
|
||||
## Beslutninger (med begrundelse)
|
||||
|
||||
1. **Faste sæt.** Redskabet sidder fast på traktoren (eller er selvkørende, fx Krone BiG M 450). Mod'en
|
||||
kobler aldrig redskaber på. *Begrundelse:* auto-påkobling var det, der aldrig blev pålideligt i AI-Bonden.
|
||||
2. **Markflåde med forslag og flueben.** Mod'en foreslår sæt ud fra redskabets spec (`spec_mower`,
|
||||
`spec_tedder`, `spec_windrower`, `spec_forageWagon`); kun sæt med flueben bliver sendt ud.
|
||||
*Begrundelse:* mod'en må aldrig tage en maskine, spilleren selv bruger. Et sæt, spilleren sidder i
|
||||
(`getIsControlled`/`getIsEntered`), springes altid over.
|
||||
3. **Staging-markør pr. mark = AutoDrive-markør med navnet `F<marknummer>`** (fx `F109`), stort/småt
|
||||
ligegyldigt, evt. med mellemrum (`F 109`). Mangler den, vises marken med "!" og sendes ikke ud.
|
||||
4. **Auto/Manuel pr. mark.** Auto starter selv ved "klar"; Manuel venter på *Start nu*. Standard: Manuel
|
||||
(intet sker uventet efter opdatering).
|
||||
5. **Plan pr. mark: Ensilage eller Hø.** Standard: Ensilage. **Riv** er et valg pr. mark, standard *fra*:
|
||||
læssevognene i Unrealistic Pack kan køre 40 m bredt og samler spredt hø op.
|
||||
6. **Bredde.** Maskiner kører med deres *aktuelle* bredde (Unrealistic Packs `variableWorkWidth`,
|
||||
`aiKeepCurrentWidth`). Courseplay kan højst 50 m → et sæt over 50 m sendes ikke ud (advarsel).
|
||||
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden.
|
||||
**Revideret efter in-game test 25/9:** et nyt Courseplay-kursus til læssevognen lå forskudt i forhold til
|
||||
skårene (anden start/retning/forager). Derfor genbruger vender, river og læssevogn **slåmaskinens
|
||||
kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra
|
||||
slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus
|
||||
laves et nyt som reserve (logges).
|
||||
**Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med
|
||||
eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥
|
||||
slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til
|
||||
både hø og ensilage.
|
||||
**Sideriver (25/9):** mens riven arbejder, måles nedlægningsområdets midte (runtime-felt
|
||||
`dropWindrowWorkAreaIndex`, ellers riverens XML) i traktorens ramme minus rivens CP-forskydning =
|
||||
skårets sideforskydning (gemt pr. mark). Læssevognen på rivens kursus får `toolOffsetX = egen −
|
||||
skårets` (±10 m) og får sin egen værdi tilbage bagefter.
|
||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`). *Tving lager* pr. mark (som "Lager til" i
|
||||
flows). **Revideret 25/9:** ensilage går først til egne **gæringssiloer** (produktion skår -> SILAGE,
|
||||
fx FermentingSiloPack/GrassSilageSilo), nærmest marken, ellers nærmeste silo med plads; hø kun til
|
||||
almindelige siloer (gæringssiloer laver også hø om til ensilage).
|
||||
8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne
|
||||
vælges (korte ture). Oversigten viser "!" ved marken, hvis intet Smart Pickup-udkørselsflow har
|
||||
siloen som kilde — hjemkørslen klares af de eksisterende flows, ikke af markdelen.
|
||||
9. **"Klar"** = ≥ 90 % af markens prøvepunkter står i høstvinduet for markens slåafgrøde
|
||||
(`FSDensityMapUtil.getFruitArea`, samme metode som Arbejdsseddel, bevist i spillet). `field.fieldState`
|
||||
bruges ikke (frossen NPC-plan for spillerejede marker).
|
||||
10. **Slåafgrøde** afgøres ud fra spillets konvertere: `MOWER` giver et skår, og `TEDDER` gør skåret til en
|
||||
`DRY…`-type (græs, lucerne, kløver, kløver2). Rug/bønner (→ halm) er udelukket. Ensilage-varen er
|
||||
skåret (`GRASS_WINDROW` …), hø-varen er tørskåret (`DRYGRASS_WINDROW` …).
|
||||
11. **Én maskine, én mark.** Flere marker kan køre samtidig. Højst 2 læssevogne pr. mark.
|
||||
12. **Rækkefølge.** Næste trin starter først, når det forrige er *færdigt* på hele marken (ingen vender
|
||||
bag slåmaskinen). Enklere og uden kollisioner mellem Courseplay-kurser.
|
||||
13. **Fejl stopper ikke alt.** Kan Courseplay ikke lave kursus, prøves igen efter 5 min; imens står marken
|
||||
med grunden. Andre marker kører videre.
|
||||
14. **Genindlæsning.** Markernes trin gemmes i `ADSmartPickup_fields.xml` i savegame-mappen. Efter
|
||||
indlæsning genoptages trinnet: sæt, der ikke længere kører, bliver sendt ud igen.
|
||||
|
||||
## Opbygning
|
||||
|
||||
Rene moduler (ingen spil-API, testes med `luajit`):
|
||||
|
||||
| Fil | Ansvar |
|
||||
|---|---|
|
||||
| `adFieldModel.lua` | Mark-plan og -status som data: plan, tilstand, overgange (`nextStage`), bredde-regler, gem/indlæs-records |
|
||||
| `adFieldFleet.lua` | Rolle ud fra redskabs-specs, forslag, valg af ledigt sæt (nærmest markøren) |
|
||||
| `adFieldView.lua` | Rækker og tekster til fanen Marker (sortering, filtre, statusord) |
|
||||
|
||||
Spil-moduler (tynde, pcall'ede):
|
||||
|
||||
| Fil | Ansvar |
|
||||
|---|---|
|
||||
| `adFields.lua` | Ejede marker, polygon/prøvepunkter, afgrøde, klar-andel, `F`-markør, marksiloer |
|
||||
| `adCpBridge.lua` | Start Courseplay på en mark (grænse → kursus → start, async med annullering), læs aktiv/færdig |
|
||||
| `adFieldJobs.lua` | Tick hvert sekund: opdag klar, send sæt ud, følg ankomst → CP → færdig, næste trin, gem |
|
||||
| `gui/SmartPickupFieldTab.lua` | Fanen **Marker**: tabel, detalje, markflåde |
|
||||
|
||||
### Forløb pr. mark
|
||||
|
||||
```
|
||||
Ensilage: idle → ready → mowing → collecting → done
|
||||
Hø: idle → ready → mowing → tedding → [raking] → collecting → done
|
||||
done → idle, når marken ikke længere er slået (vokser igen)
|
||||
```
|
||||
|
||||
Hvert arbejdstrin har undertilstande pr. sæt: `driving` (AutoDrive til `F`) → `starting` (CP laver
|
||||
kursus) → `working` (CP kører) → `returning` (AutoDrive til parkering). Trinnet er færdigt, når Courseplay
|
||||
meldte færdig for mindst ét sæt, og intet sæt på marken stadig arbejder.
|
||||
|
||||
**Slå/vende/rive:** AutoDrive `DRIVETO` til `F`-markøren. Ved stop inden for 30 m starter
|
||||
`adCpBridge` Courseplay med `fieldPosition` = et prøvepunkt inde i marken. Når CP er færdig: AutoDrive
|
||||
`DRIVETO` til sættets parkering (sættets AD-parkeringsmarkør, ellers bliver det stående ved `F`).
|
||||
|
||||
**Opsamling:** Før CP startes sættes AutoDrive til `PICKUPANDDELIVER`, første markør = `F`, anden =
|
||||
silo, `setStartHelper(true)` med hjælper Courseplay. Så klarer AutoDrive og Courseplay selv
|
||||
fuld → aflæs → tilbage → `restartCP`. Ved CP færdig: AutoDrive tømmer resten (`enableParkAtJobFinished`)
|
||||
og parkerer.
|
||||
|
||||
## Fanen Marker
|
||||
|
||||
- Tabel: **Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner**. Kun slåafgrøde-marker som standard
|
||||
(*Vis alle marker* viser resten). Klik på en række → detalje.
|
||||
- Detalje: Plan (Ensilage/Hø), Auto/Manuel, Riv, Tving lager (liste som "Lager til"), **Start nu**/**Stop**,
|
||||
status med grund, markør og slåbredde.
|
||||
- Markflåde: foreslåede sæt med flueben, rolle og aktuel bredde; advarsel over 50 m.
|
||||
- Knapper på fanen: Start nu / Stop på valgt mark (samme placering som flows).
|
||||
- Statusord: *Venter · Klar · Slås · Vendes · Rives · Samles op · Færdig* + grund ("venter på
|
||||
slåmaskine", "! mangler markør F109", "læssevogn for smal: 20 m < 40,8 m", "Courseplay: intet kursus").
|
||||
|
||||
## Fejlhåndtering
|
||||
|
||||
| Situation | Hvad sker |
|
||||
|---|---|
|
||||
| Ingen `F`-markør | "!" i tabellen, intet sendes |
|
||||
| Intet ledigt sæt med rollen | Status "venter på …", prøver hvert 5 s |
|
||||
| Sæt for bredt (> 50 m) eller for smalt (< slåbredde) | Sættet springes over med grund |
|
||||
| AutoDrive kan ikke finde vej | Sættet frigives, status med grund, andet sæt prøves |
|
||||
| CP-grænse/kursus fejler | 5 min pause for marken, grund vises |
|
||||
| Spilleren sætter sig i et sæt | Sættet frigives fra marken (arbejdet fortsætter ikke automatisk) |
|
||||
| Genindlæsning | Trin genoptages fra gemt fil |
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adFieldModel.lua`, `tests/test_adFieldFleet.lua`, `tests/test_adFieldView.lua`: alle
|
||||
overgange, bredde-regler, forslag, sortering/filtre, gem/indlæs.
|
||||
- In-game testprotokol (manualen + denne spec): én mark Manuel → Start nu → slå → saml op → silo, derefter
|
||||
Hø, derefter Auto. Loglinjer: `ADSmartPickup: F109 …`.
|
||||
|
||||
## Senere udvidelser (noter)
|
||||
|
||||
- **Flere maskiner om samme mark, maks 5** (Courseplays multi-tool-grænse, 2026-09-25): slå/vende/rive med
|
||||
op til 5 sæt på én mark samtidig via Courseplays konvoj (multi-tool), og op til 5 læssevogne. Kræver at
|
||||
`ADFieldModel.maxRigs` bliver en indstilling pr. mark (1–5) og at sættene får samme CP-kursus med
|
||||
hver sin position i konvojen.
|
||||
@@ -1,145 +0,0 @@
|
||||
# Guider til opsætning: marker og flows — design
|
||||
|
||||
Dato: 2026-09-26 · Status: udkast til godkendelse · Mod: FS25_ADSmartPickup (v1.30.4.0 → v1.31/v1.32)
|
||||
|
||||
Leveres i to etaper, der hver kan testes for sig:
|
||||
- **Etape A (v1.31):** fælles guide-ramme + Fields-fanen ("Opsæt mark").
|
||||
- **Etape B (v1.32):** Flows-fanen på samme ramme ("Opsæt flow"), så de to faner er ens at bruge.
|
||||
|
||||
## Formål
|
||||
|
||||
Fields-fanen er blevet en lang liste af valg, der er svær at overskue (brugerens ord: "uoverskueligt").
|
||||
Målet er, at man sætter en mark op med **ét spørgsmål ad gangen**, og at alt, der kan afgøres automatisk,
|
||||
aldrig bliver spurgt om. En eksisterende opsætning skal kunne rettes uden at gå hele guiden igennem.
|
||||
|
||||
Succes: brugeren kan sætte F22 op fra bunden på under et minut uden at scrolle, og kan rette ét valg
|
||||
(fx lager) med to tryk.
|
||||
|
||||
## Hvad brugeren ser
|
||||
|
||||
### Oversigt (fanens standardvisning)
|
||||
|
||||
1. Marktabellen som i dag (Mark · Afgrøde · Plan · Kørsel · Status (kort) · Maskiner).
|
||||
2. Når en mark er valgt: et **resumé** på én linje pr. emne, hver med sin egen række man kan klikke for at
|
||||
rette det ene trin ("Ret"):
|
||||
- Afgrøde: `Behold det der gror` / `-> Hvede`
|
||||
- Afgrødens valg: `Ensilage, river` / `Halm hakkes`
|
||||
- Klargøring: `Automatisk` / `Plov, kalk`
|
||||
- Pleje: `Gødning + ukrudt`
|
||||
- Maskiner: `3 faste` / `Ledige maskiner` · `1 ad gangen, 1 aflæsningsvogn`
|
||||
- Lager: `Marksilo` / `Automatisk` / `F22 Silo`
|
||||
- Status (hele teksten, ombrudt)
|
||||
3. Knapper i bundlinjen (når en mark er valgt): **[Opsæt mark]**, **[Start nu / Stop]**, **[Markflåde]**.
|
||||
Uden valgt mark: **[Markflåde]**.
|
||||
4. Den lange detaljeliste og den fælles markflåde nederst forsvinder fra oversigten.
|
||||
|
||||
### Guide (erstatter fanens indhold, mens den er åben)
|
||||
|
||||
Ét trin pr. skærm: overskrift ("Trin 3 af 7 — Græs: ensilage eller hø?"), en kort forklaring, valgene som
|
||||
rækker, og bundknapperne **[Næste]** (eller **[Gem]** på sidste trin), **[Tilbage]**, **[Annullér]**.
|
||||
Trin der ikke er relevante for marken springes over, og tællingen følger de viste trin.
|
||||
|
||||
| # | Trin | Vises når | Valg | Standard |
|
||||
|---|------|-----------|------|----------|
|
||||
| 1 | Mark | guiden er åbnet uden valgt mark | liste over marker (nr, afgrøde, status) | — |
|
||||
| 2 | Afgrøde | altid | "Behold det der gror" + afgrøder der kan sås | nuværende plan |
|
||||
| 3 | Afgrødens valg | slåafgrøde (nu eller mål): ensilage/hø + river ja/nej. Høstafgrøde med halm: hakkes/samles/presses | se "Vises når" | nuværende plan |
|
||||
| 4 | Klargøring | altid (efter høst / bar mark) | "Automatisk (anbefalet)" / "Vælg selv" → plov, kultivering, kalk, tromle, mulch | Automatisk |
|
||||
| 5 | Pleje | afgrøde der gror | gødning ja/nej, ukrudt ja/nej | ja/ja |
|
||||
| 6 | Maskiner | altid | "Faste maskiner til marken?" ja/nej; ja → liste (kun ledige + dem der allerede er faste her); antal ad gangen (1–5); aflæsningsvogne ved høst (1–5) | nej / 1 / 1 |
|
||||
| 7 | Lager | høst/slåning | marksilo findes: "Brug marksiloen?" ja/nej; ellers/nej: "Automatisk (anbefalet)" / "Vælg selv" → de 8 nærmeste siloer | Automatisk |
|
||||
| 8 | Oversigt | altid | resumé med [Ret] pr. linje; [Kør automatisk] (Auto) / [Gem uden at starte] (Manual) | — |
|
||||
|
||||
### Redigering
|
||||
|
||||
- **[Opsæt mark]** på en opsat mark åbner guiden med de nuværende valg udfyldt.
|
||||
- **[Ret]** på en resumé-linje (oversigt eller trin 8) åbner kun det ene trin; **[Gem]** der gemmer og går
|
||||
tilbage dit man kom fra.
|
||||
- **[Annullér]** kasserer ændringer i guiden (intet gemmes før [Gem]/[Kør automatisk]).
|
||||
|
||||
### Markflåde
|
||||
|
||||
Egen visning (knap **[Markflåde]**): "Brug alle ledige maskiner" + listen med flueben, "fast på F22",
|
||||
"i gang på F108", bredde-advarsler. Samme indhold som i dag, flyttet ud af markens detaljer.
|
||||
|
||||
## Hvad der ALDRIG spørges om (automatisk)
|
||||
|
||||
Scanning af marken og markøren, hvornår der høstes/slås/gødes/sprøjtes, påfyldning (eget lager før køb),
|
||||
tømning ved forkert vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter, gæringssilo ved
|
||||
ensilage, marksilo-tømning (advarsel hvis intet flow tømmer den).
|
||||
|
||||
## Etape B: Flows-fanen på samme måde
|
||||
|
||||
Brugerens ønske: "det samme for flow opsætningen så det bliver ensrettet".
|
||||
|
||||
**Oversigt:** flow-tavlen som i dag (én linje pr. flow med status). Valgt flow → resumé med [Ret]-linjer
|
||||
(Retning · Fra · Til/Lager · Varer · Regler · Vent ved · Traktorer · Status) og bundknapperne
|
||||
**[Opsæt flow]**, **[Start/Stop]**, **[Nyt flow]**, **[Omdøb]**, **[Slet]**, **[Start alle / Stop alle]**.
|
||||
Den lange detaljeliste forsvinder.
|
||||
|
||||
**Guiden "Opsæt flow"** — samme trin-skærm, knapper og [Ret]/[Annullér] som markguiden:
|
||||
|
||||
| # | Trin | Vises når | Valg | Standard |
|
||||
|---|------|-----------|------|----------|
|
||||
| 1 | Navn | nyt flow | tekst (FS25-tekstdialog) | "Flow N" |
|
||||
| 2 | Retning | altid | Ind til bygning (forsyning) / Ud fra bygning (udkørsel) | nuværende |
|
||||
| 3 | Fra / Til | altid | bygninger med markør, fast rækkefølge (navn, markør), "Vis alle" | nuværende |
|
||||
| 4 | Varer | altid | forsyning: "Automatisk (det bygningen mangler)" / "Vælg selv" → varer; udkørsel: pr. vare Sælg / Lager / Begge / Fra | Automatisk / nuværende |
|
||||
| 5 | Lager | udkørsel med Lager | "Automatisk (nærmeste med plads)" / "Vælg selv" → lagerbygninger | Automatisk |
|
||||
| 6 | Regler | altid | udkørsel: sælg ved pris-%, lager over %, samlerunde ja/nej; forsyning: antal ture, retur fra Wait under % | nuværende / standard |
|
||||
| 7 | Vent ved | altid | Wait-puljer | nuværende |
|
||||
| 8 | Traktorer | altid | ledige traktorer (+ dem i flowet); traktorer i andre flows skjult med linje "N i andre flows" | nuværende |
|
||||
| 9 | Oversigt | altid | resumé med [Ret]; [Start flow] / [Gem uden at starte] | — |
|
||||
|
||||
Automatisk (spørges aldrig): markør ved bygningen, rutetjek, bedste foder først/parallelt foder, læs-bytte,
|
||||
kildevalg (mindst/fyldeste først), fordeling på flere mål, hvile af tomme kilder, traktornavne efter flowet.
|
||||
|
||||
## Arkitektur
|
||||
|
||||
- **`adGuide.lua` (ny, ren Lua, testet):** den fælles guide-motor, uafhængig af marker/flows:
|
||||
trin-liste, aktuelt trin, kladde, næste/tilbage/spring (til [Ret]), "returnér til oversigt", tælling
|
||||
"Trin 3 af 7" over de synlige trin. Både markguiden og flowguiden er en trin-definition oven på den.
|
||||
- **`gui/SmartPickupGuide.lua` (ny):** den fælles trin-skærm i menuen (overskrift, forklaring, valgrækker,
|
||||
bundknapper Næste/Tilbage/Annullér/Gem) — ét sted, så de to guider ser og opfører sig ens.
|
||||
- **`adFlowGuide.lua` (ny, etape B, ren Lua, testet):** flowguidens trin, valg, standarder og resumé; gemmer
|
||||
via `ADRunsController.updateRun`/`assignVehicle` som i dag.
|
||||
- **`adFieldGuide.lua` (ny, ren Lua, testet):** markguidens trin (oven på `ADGuide`).
|
||||
- `ADFieldGuide.open(field, info, context)` → guide-tilstand `{fieldId, draft, steps, index, returnTo}`;
|
||||
`draft` er en kopi af markens plan (+ faste maskiner), så intet ændres før gem.
|
||||
- `ADFieldGuide.steps(draft, info, context)` → listen af synlige trin (tabellen ovenfor).
|
||||
- `ADFieldGuide.choices(step, draft, info, context)` → rækkerne for trinnet (tekstnøgler + værdier).
|
||||
- `ADFieldGuide.choose(guide, key, value)` → ny guide med ændret draft (immutabelt).
|
||||
- `ADFieldGuide.next/back/jump(guide, step)`; `ADFieldGuide.result(guide)` → `{plan, rigIds}` til gem.
|
||||
- `ADFieldGuide.summary(field, info, context)` → resumé-linjerne `{step, key, args}`.
|
||||
- `context` = det spil-afhængige: marksilo findes?, siloer i nærheden, maskiner (ledige/faste), afgrøder
|
||||
der kan sås. Hentes af GUI'et, så modellen kan testes uden spillet.
|
||||
- **`gui/SmartPickupFieldTab.lua`:** oversigt (tabel + resumé) og guide-visning; bygger rækker ud fra
|
||||
`ADFieldGuide`, gemmer via `ADFieldJobs.setPlan` og `ADFieldFleet.dedicate`. Fil-størrelse: guide-visningen
|
||||
lægges i `gui/SmartPickupFieldGuide.lua` (ny), så ingen fil passerer ~800 linjer.
|
||||
- **`gui/SmartPickupFrame.lua`:** bundknapper for fanen (Opsæt mark, Start/Stop, Markflåde, Næste/Tilbage/
|
||||
Annullér/Gem i guiden) og routing af klik.
|
||||
- **Uændret:** `ADFieldModel` (plan-felter og lagring), `ADFieldJobs`, `ADFieldWork`, gemt XML. Guiden er et
|
||||
nyt ansigt på de samme data — eksisterende opsætninger virker videre.
|
||||
|
||||
## Fejl og kanttilfælde
|
||||
|
||||
- Mark uden markør: guiden kan køres, men oversigten siger "sæt markøren F22" og [Kør automatisk] gemmer som
|
||||
Manual indtil markøren findes (samme regel som i dag).
|
||||
- Maskine der er fast på en anden mark vises ikke i trin 6 (som v1.30.3); linje "N maskiner er faste på
|
||||
andre marker".
|
||||
- Marken forsvinder (solgt/bygget over) mens guiden er åben: guiden lukkes, oversigten vises.
|
||||
- Guiden er åben og menuen lukkes: kladden kasseres.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adGuide.lua`: motoren — næste/tilbage over skjulte trin, spring+retur, tælling, annullér.
|
||||
- `tests/test_adFlowGuide.lua` (etape B): trin pr. retning, varer auto/vælg, lager-trin kun ved Lager, resumé.
|
||||
- `tests/test_adFieldGuide.lua`: synlige trin pr. marktype (slåafgrøde, høst med halm, bar mark, mål-afgrøde),
|
||||
standardvalg, udfyldning fra eksisterende plan, `choose` ændrer kun kladden, `result` giver plan + rigIds,
|
||||
[Ret]-spring og tilbage, "Automatisk" vs "Vælg selv" i klargøring og lager, marksilo-spørgsmålet kun når en
|
||||
findes, resumé-linjerne.
|
||||
- Hele suiten skal være grøn; GUI-routing verificeres in-game (testplan-punkt).
|
||||
|
||||
## Uden for scope
|
||||
|
||||
Vinmarker (egne roller), genoptagelse af Courseplay efter genstart, fanerne Factories/Silos/Barns/Pallet sheds.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Markarbejde, etape 2–3 + multi-maskine: høst, roer, jordbehandling, såning
|
||||
|
||||
Dato: 2026-09-26 (nat) · Bygger på `2026-09-25-markarbejde-etape1-design.md` · Branch `feature/markarbejde-2`
|
||||
|
||||
Brugeren gav fuldt mandat ("byg resten, ALLE PUNKTER, du træffer selv gode valg") og tester i spillet dagen
|
||||
efter. Beslutningerne herunder er truffet uden dialog og skal læses igennem.
|
||||
|
||||
## Mål
|
||||
|
||||
Alle gårdens marker kan køres af Smart Pickup: slåafgrøder (etape 1) og nu **høstafgrøder** (korn, raps,
|
||||
majs, roer, kartofler … alt der høstes med mejetærsker/optager), inkl. **aflæsningsvogne**, **jordbehandling**
|
||||
efter høst og **såning**. Hvert trin kan køres med **1–5 maskiner** på samme mark (Courseplays multi-tool).
|
||||
|
||||
## Beslutninger
|
||||
|
||||
1. **Høst er én pipeline for alle høstafgrøder.** Roer og korn adskiller sig kun ved hvilken høster der
|
||||
kan tage afgrøden. Høsteren matches på afgrøden: skærebordets/optagerens frugttyper (`spec_cutter`)
|
||||
skal omfatte markens afgrøde. Finsnittere (cutter med `fruitTypeConverters`) bruges ikke til høst.
|
||||
2. **Aflæsning under høst = Courseplays combine-unloader** (`COMBINE_UNLOADER_CP`, bevist i AI-Bonden):
|
||||
vognen følger høsteren, tømmer den og kører til en **station**. Stationen vælges af Smart Pickups
|
||||
silovalg (nærmeste silo med plads der tager afgrøden, eller *Tving lager*); findes den ikke i
|
||||
Courseplays stationsliste, bruges nærmeste station. Antal vogne pr. mark: 1–3 (plan).
|
||||
3. **Høsteren sendes hjem** (AD-parkering) når Courseplay er færdig og tanken er tømt. Aflæsningsvogne
|
||||
stoppes når høsten er færdig; har de rest, kører AutoDrive den til lageret (samme reserve som
|
||||
læssevognen), ellers parkering.
|
||||
4. **Efter høst** (valg pr. mark): *Ingenting*, *Kultivér* eller *Pløj*, og derefter evt. **Så**.
|
||||
Såmaskinen sættes til planens afgrøde (standard: samme afgrøde som blev høstet). Kan afgrøden ikke
|
||||
sås i den nuværende periode, venter marken med grund. Tom for frø -> sættet springes over med grund
|
||||
(moden fylder ikke frø på; det gør brugeren/AutoDrive-refill).
|
||||
5. **Multi-maskine (1–5 pr. trin, plan pr. mark).** Trin med eget kursus (slå, vend, riv, høst,
|
||||
kultivér, pløj, så) laver ét Courseplay-kursus med `multiTools = n` på første sæt; de øvrige kopierer det,
|
||||
og hver får sin bane (`laneOffset`: 2 -> −1,1 · 3 -> −1,0,1 · 4 -> −2,−1,1,2 · 5 -> −2…2). Et multi-trin
|
||||
startes kun når **alle n** sæt er ledige (ellers kører én bane ikke). Læssevognen på et multi-kursus
|
||||
kræver også n vogne, én pr. bane.
|
||||
6. **Rolle-rækkefølge** når et køretøj har flere redskaber: høster > læssevogn > river > vender >
|
||||
slåmaskine > såmaskine > kultivator > plov > aflæsningsvogn (traktor+vogn som Courseplay kan bruge som
|
||||
combine-unloader).
|
||||
7. **Afgrødetype pr. mark:** slåafgrøde (etape 1) eller høstafgrøde (alt andet med en høstbar frugt). Plan
|
||||
*Høst* er standard for høstafgrøder. Marker uden afgrøde (pløjet/stub) kan få *Start nu* for
|
||||
jordbehandling/såning alene.
|
||||
8. **Stadig ikke med:** gødning/kalk/sprøjtning, baller, halm efter mejetærsker, auto-påfyldning af frø.
|
||||
|
||||
## Opbygning (ændringer)
|
||||
|
||||
| Fil | Ændring |
|
||||
|---|---|
|
||||
| `adFieldModel.lua` | Planer `harvest`, jordbehandling, såning, `rigs`/`unloaders`; trin `harvesting/cultivating/plowing/sowing`; hjælperoller pr. trin; `lanePositions(n)` |
|
||||
| `adFieldFleet.lua` | Nye roller; `pick` tager afgrøde (høster skal kunne tage den); `pickMany` til multi-trin |
|
||||
| `adCpBridge.lua` | `startUnloader`, `canHarvest`, frø-valg (`setSeedFruit`, `isPlantable`, `hasSeeds`), multi-tool kursus + lane |
|
||||
| `adFields.lua` | Afgrødetype (slå/høst), sidst høstede afgrøde |
|
||||
| `adFieldWork.lua` (ny) | Udsendelse og opfølgning af sæt (AD/CP) — flyttet ud af `adFieldJobs.lua` der blev for stor |
|
||||
| `gui/SmartPickupFieldTab.lua` | Plan-rækker pr. afgrødetype, maskiner pr. trin, aflæsningsvogne, efter høst, så + afgrøde |
|
||||
@@ -1,103 +0,0 @@
|
||||
# Marksilo-flow — design
|
||||
|
||||
Dato: 2026-09-27 · Status: godkendt i chat, afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Høsten lægges ofte i siloer ude ved markerne (mellemlager). I dag skal spilleren selv huske at køre varerne
|
||||
hjem. Marksilo-flowet kigger jævnligt alle marksiloer igennem og sender selv op til **x** ledige vogne ud for at
|
||||
hente varerne hjem til gårdens lagre.
|
||||
|
||||
**Succes:** står der varer i en marksilo, som gården kan tage, bliver de kørt hjem uden at spilleren gør noget,
|
||||
med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre flows eller spilleren.
|
||||
|
||||
## Brugerens valg (fra brainstorm)
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvor er "hjem"? | Nærmeste af de gårdlagre, spilleren vælger i flowet, der tager varen og har plads |
|
||||
| Hvad er "x vogne"? | Lånte ledige vogne fra hele gården — ingen fast tilknytning — højst x ude samtidig |
|
||||
| Hvilke vogne må lånes? | Samme regel og flueben som markarbejdet (`fleetMode` "all"/"ticked", `ADFieldJobs.state.fleet`) |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
|
||||
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
|
||||
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
|
||||
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
|
||||
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
|
||||
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
|
||||
|
||||
## Flowet
|
||||
|
||||
Marksilo-flowet er ÉN fast opsætning pr. savegame — ikke en ny run-type. Beslutning (2026-09-27, under
|
||||
planlægning): run-editoren (`SmartPickupFrame`) er bygget til præcis to typer (forsyning/udkørsel) mange steder,
|
||||
og runs har faste vogne, Start/Stop pr. vogn og Wait-puljer, som intet af det passer til lånte vogne. En egen
|
||||
underfane + egen gemmefil giver langt færre indgreb i eksisterende kode.
|
||||
|
||||
Opsætning (`ADSmartPickup_fieldsilos.xml` i savegame-mappen, gemmes sammen med runs):
|
||||
- `enabled` — til/fra, standard fra
|
||||
- `storeIds` — gårdlagre (bygnings-id'er fra `ADBuildings`, kun `kind == "silo"`)
|
||||
- `maxVehicles` — x, 1–10, standard 2
|
||||
- `minLiters` — tærskel pr. silo og vare: 5000 / 10000 / 25000 / 50000, standard 10000
|
||||
|
||||
## Planlægning (ren logik, nyt modul `adFieldSiloPlanner.lua`, testes med luajit)
|
||||
|
||||
Input: marksiloer `{id, markerId|nil, stock = {[fillType] = liters}}`, gårdlagre `{id, markerId, accepts =
|
||||
{[fillType] = freeLiters}, x, z}`, aktive lån `{siloId, fillType, liters}`, ledige vogne `{id, x, z, capacity,
|
||||
carries = {[fillType] = true}}`, `maxVehicles`, `minLiters`.
|
||||
|
||||
1. **Kvalificerede opgaver:** pr. (silo, vare) hvor silo har markør, `liters >= minLiters`, og mindst ét
|
||||
gårdlager har `freeLiters >= minLiters` for varen. Resten = liters minus det aktive lån på samme (silo, vare)
|
||||
allerede er på vej med; kun opgaver med rest `>= minLiters` tæller.
|
||||
Siloer uden markør → status `noMarker`; varer intet gårdlager tager → status `noFarmStore`.
|
||||
2. **Rækkefølge:** fuldeste rest først; lige → lavest silo-id (stabilt).
|
||||
3. **Vogne:** ledige pladser = `maxVehicles - #aktive lån`. For hver opgave i rækkefølge, så længe der er
|
||||
pladser: nærmeste ledige vogn (til siloen), der kan bære varen; resten reduceres med vognens kapacitet;
|
||||
samme opgave får flere vogne, så længe resten stadig er `>= minLiters`.
|
||||
4. **Gårdlager pr. lån:** nærmeste gårdlager (til siloen) der tager varen med `freeLiters >= min(vognens
|
||||
kapacitet, rest)`; ellers det med mest plads; ellers intet (opgaven springes over denne runde).
|
||||
|
||||
Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId, liters}}, status = {...}}`.
|
||||
|
||||
## Udførelse (`adFieldSilos.lua`)
|
||||
|
||||
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
|
||||
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
|
||||
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
|
||||
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
|
||||
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
|
||||
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
|
||||
(flows tager allerede kun deres egne tilknyttede vogne).
|
||||
- **Afsend:** AutoDrive "Hent og aflever": første markør = silomarkør, anden = gårdlagerets markør, vare =
|
||||
lånets vare, `loopCounter = 1`, parkering ved job slut slået til (`enableParkAtJobFinished`); start som
|
||||
markarbejdet (`startAd` → mode:start). Log: `marksilo: '<vogn>' henter <liter> l <vare> i <silo> → <lager>`.
|
||||
- **Frigivelse:** lånet slettes når AD ikke længere er aktivt for vognen (turen og parkeringen er færdig),
|
||||
når spilleren sætter sig i den, eller når flowet stoppes (stop = AD stoppes på lånte vogne).
|
||||
- **Vagt:** er vognen ikke kommet i gang (ingen stigning i fyldning og står stille) efter 120 s, stoppes AD,
|
||||
lånet slettes, og der logges én advarsel pr. (silo, vare), så samme opgave ikke fejler i ring hvert minut
|
||||
(opgaven får 10 min pause).
|
||||
- **Genindlæsning:** lån gemmes ikke. Ved indlæsning er alle vogne frie for flowet; en vogn AutoDrive selv
|
||||
genstarter, kører sin tur færdig som almindelig AD-tur.
|
||||
|
||||
## GUI
|
||||
|
||||
Ny underfane "Marksiloer" efter "Måned". Indstillinger: gårdlagre (eksisterende vælger),
|
||||
"Maks. vogne ude" (1–10), "Mindste mængde" (5.000 / 10.000 / 25.000 / 50.000 l), Start/Stop.
|
||||
Status: `x/y vogne ude · n siloer venter`, og én række pr. marksilo med indhold: silo, vare, liter, på vej,
|
||||
status (`ok` / `mangler markør` / `ingen gård tager <vare>`). Tekster i `l10n_da` + `l10n_en` (`spu_fs_*`).
|
||||
|
||||
## Uden for scope
|
||||
|
||||
- Salg direkte fra marksilo (kan senere bruge udkørslens prisregel).
|
||||
- Faste vogne i flowet (valg 1 i brainstorm) — fravalgt.
|
||||
- Lån der overlever genindlæsning.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adFieldSiloPlanner.lua`: kvalificering (tærskel, markør, gård tager varen), fuldeste først,
|
||||
x-loft inkl. aktive lån, flere vogne til én stor silo, vogn der ikke kan bære varen springes over, nærmeste
|
||||
gårdlager med plads, fallback til mest plads, intet lager → ingen afsendelse.
|
||||
- `tests/test_adFieldSiloConfig.lua`: standarder, grænser (x 1–10, tærskel kun de fire værdier), til/fra af gårdlager, records frem og tilbage.
|
||||
- In-game: silo ved en mark med 50.000 l hvede, gårdlager = Farma-silo, x = 2 → to vogne kører, afleverer,
|
||||
parkerer; markarbejdet tager ikke de lånte vogne imens.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Flow-vogne — design
|
||||
|
||||
Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har
|
||||
det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`).
|
||||
|
||||
## Formål
|
||||
|
||||
Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke
|
||||
længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har
|
||||
noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen.
|
||||
|
||||
## Brugerens valg
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning |
|
||||
| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor |
|
||||
| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet |
|
||||
| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af
|
||||
flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml`
|
||||
(`wagons="traktorId=vognId;…"`).
|
||||
- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og
|
||||
"bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde
|
||||
stille uden at flowet er stoppet).
|
||||
- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to
|
||||
tjek i træk (60 s mellem tjek).
|
||||
|
||||
## Adfærd
|
||||
|
||||
1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor]
|
||||
= redskab` (ændringer gemmes med næste gem).
|
||||
2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn:
|
||||
- vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect <vognens parkering>", derefter startes
|
||||
traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn".
|
||||
- vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s.
|
||||
- vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør).
|
||||
3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med
|
||||
`ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets.
|
||||
4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke).
|
||||
5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere
|
||||
— stoppet, traktorer kørt hjem".
|
||||
|
||||
## Puljen og flows
|
||||
|
||||
- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**.
|
||||
- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab.
|
||||
- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå.
|
||||
|
||||
## Komponenter
|
||||
|
||||
- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning
|
||||
væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`,
|
||||
`sendHome(run)`).
|
||||
- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records.
|
||||
- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`,
|
||||
`startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`).
|
||||
- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller.
|
||||
- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken.
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af
|
||||
bygning væk, ejer-opslag (stoppet vs kørende).
|
||||
- `tests/test_adRuns.lua`: records med `stopped` og `wagons`.
|
||||
- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter
|
||||
deres egne vogne og kører.
|
||||
|
||||
## Uden for scope
|
||||
|
||||
- Valg af vogn i GUI (automatisk er valgt).
|
||||
- At et flow tager en anden vogn end sin egen.
|
||||
@@ -1,149 +0,0 @@
|
||||
# Maskinpulje — design
|
||||
|
||||
Dato: 2026-09-28 · Status: godkendt i chat (afsnit 1–4), afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Plove, harver, kultivatorer, såmaskiner, tømmevogne m.m. bruges kun en gang imellem. I dag skal hvert af dem have
|
||||
en traktor koblet fast på hele året, fordi markarbejdet kun kan sende en traktor, der allerede har det rigtige
|
||||
redskab bag sig. Maskinpuljen gør traktorer og redskaber til fælles puljer: et job venter i kø, til en traktor er
|
||||
ledig; traktoren henter det redskab, jobbet kræver, udfører arbejdet, parkerer redskabet på dets plads og kører
|
||||
selv hjem til sin egen parkering.
|
||||
|
||||
**Succes:** med fx 3 ledige traktorer, 2 plove og 1 kultivator bliver markernes arbejde udført uden at nogen
|
||||
traktor står låst til et redskab — færre køretøjer at købe og vedligeholde. Flows, der kører hele tiden (fodring,
|
||||
slurry, mælk …), påvirkes ikke.
|
||||
|
||||
## Brugerens valg (fra brainstorm)
|
||||
|
||||
| Spørgsmål | Valg |
|
||||
|---|---|
|
||||
| Hvordan findes redskabets Connect-markør? | Navneregel: AD-parkering "X" ↔ markør "Connect X" |
|
||||
| Hvem er i puljen? | Automatisk ud fra opsætningen — ingen afkrydsning |
|
||||
| Hvilken traktor henter? | Nærmeste ledige med nok hk |
|
||||
| Omfang | Markarbejde + tømmevogne ved høst + marksilo-flowet. Flows er undtaget |
|
||||
| Tømmevogne ved høst | Også fra puljen, men dedikeret til marken, til høsten dér er færdig |
|
||||
| Arkitektur | Udvid markarbejdets eget maskinvalg (ikke en separat puljemanager) |
|
||||
|
||||
## Begreber
|
||||
|
||||
- **Puljeredskab:** et redskab eller en vogn, der holder **frakoblet**, har en AD-parkering (`advd.parkDestination`
|
||||
→ markør "X"), og hvor der findes en markør "Connect X" (`ADHitch.parseMarker` = attach, navn = X; store/små
|
||||
bogstaver ligegyldigt). Uden Connect-markør er redskabet ikke i puljen — det er spillerens måde at holde det
|
||||
udenfor.
|
||||
- **Puljevogn:** puljeredskab med rollen `unloader` (vogn med tip/snegl, `spec_trailer` + `spec_dischargeable`).
|
||||
- **Puljetraktor:** motoriseret køretøj uden noget koblet bag på (`ADHitch.rearImplements` tom), ikke i et flow
|
||||
(`ADRunsController.getRunForVehicle == nil`), ikke optaget (samme `busy`-regel som `getRigs`), ikke styret af
|
||||
spilleren, med egen AD-parkering (`vehicle.advd.parkDestination >= 1`) og flueben i markflåden
|
||||
(`ADFieldFleet.isEnabled`).
|
||||
- **Færdig kombination:** det markarbejdet bruger i dag — traktor med redskabet allerede koblet på (`getRigs`).
|
||||
- **Puljekombination:** {traktor, redskab, Connect-markør-id, redskabets parkering-id, traktorens parkering-id}.
|
||||
|
||||
AD-parkering: hver maskine har sin egen `advd.parkDestination`; AD bruger det valgte redskabs, når et er koblet på
|
||||
(`AutoDriveVehicleData.lua:88-95`). Traktorens egen parkering læses derfor altid på traktoren selv.
|
||||
|
||||
## Komponenter
|
||||
|
||||
### `adPool.lua` (nyt, ren logik + tynd spil-adapter)
|
||||
|
||||
Ren logik (testes med luajit):
|
||||
- `ADPool.roleOf(specs)` — rolle for et frakoblet redskab ud fra dets egne specs via `ADFieldFleet.ROLE_SPECS`;
|
||||
vogn med `spec_trailer` + `spec_dischargeable` og intet andet → `unloader`.
|
||||
- `ADPool.isPoolImplement(item)` — `item = {attached, parkMarkerName, connectMarkerId}` → frakoblet, med parkering
|
||||
og Connect-markør.
|
||||
- `ADPool.powerOk(tractorPower, neededPower)` — true når et af tallene mangler (nil/0), ellers `tractorPower >=
|
||||
neededPower`.
|
||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, reserved, accept)` — redskab med rollen nærmest marken
|
||||
(ikke reserveret, `accept(implement)` sand), derefter nærmeste ledige traktor til redskabet med `powerOk`.
|
||||
Returnerer `pair` eller `nil, why, args` (`noPoolImplement` / `noPoolTractor` / `tooWeak` med hk-tal).
|
||||
- `ADPool.queueOrder(fields)` — markers ventende rækkefølge: `waitingSince` stigende, lige → lavest mark-id.
|
||||
- `ADPool.isReturnDue(rearImplements, poolParkIds)` — traktor har et puljeredskab bag sig (dets parkering har en
|
||||
Connect-markør) → hjemtur skal køres.
|
||||
|
||||
Spil-adapter:
|
||||
- `ADPool.collect(adEnv)` → `implements, tractors` fra `g_currentMission.vehicleSystem.vehicles` (gårdens), med
|
||||
position, rolle, specs til `acceptFor`, hk og parkering/Connect-id.
|
||||
- hk: `g_storeManager:getItemByXMLFilename(vehicle.configFileName).specs` → traktor `power`, redskab
|
||||
`neededPower`; mangler de, bruges nil (→ tilladt).
|
||||
- Reservationer: `ADPool.reserve(implementId, tractorId, owner)` / `release(owner)`; i hukommelsen, ryddes ved
|
||||
genindlæsning (se Genindlæsning).
|
||||
- Karantæne: redskab, der ikke kunne kobles på, springes over i 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
||||
|
||||
### `adHitch.lua` (findes, v1.47.4 — udvides let)
|
||||
|
||||
Udfører kørslerne: `fetch` (kør til Connect + kobl på), `route` (trin-tur, afkobling ved redskabets parkering).
|
||||
Ny: `ADHitch.returnHome(adEnv, tractor, onDone)` = `route({redskabets parkering, traktorens parkering})`; ved
|
||||
parkeringen kobles puljeredskabet af (samme regel som i dag i ruter).
|
||||
|
||||
### Markarbejdet (`adFieldWork.lua`, `adFieldJobs.lua`)
|
||||
|
||||
- `dispatch`: som i dag først færdige kombinationer. Returnerer `pick`/`pickMany` intet (`noRig`/`allBusy`/
|
||||
`cannotDo`), forsøges `ADPool.pairFor` for de manglende. Puljekombinationens assignment får `phase = "fetching"`,
|
||||
`pool = {implementId, connectId, implementParkId, tractorParkId}`, nøgle = traktorens id.
|
||||
- `follow`: fase `fetching` → når `ADHitch.fetch` melder ok: bredde tjekkes med redskabet på (`checkWidth`; for
|
||||
bred → hjemtur + årsag `tooWide`), derefter samme vej som i dag (`startAd` til markens markør / påfyldning,
|
||||
`phase = "driving"`). Fejl → hjemtur, karantæne, markens `retryAt` som ved `noRig`.
|
||||
- `finishAssignment`: har traktoren et puljeredskab bag sig (`ADPool.isReturnDue`) → `ADHitch.returnHome` i
|
||||
stedet for den almindelige parkeringstur; fase `returning`; assignment slettes, når hjemturen er færdig (eller
|
||||
fejler — så med advarsel).
|
||||
- Kø: `tick` gennemgår ventende marker i `ADPool.queueOrder`. `waitingSince` sættes, når en mark første gang ikke
|
||||
fik en maskine, og nulstilles ved udsendelse. `DISPATCH_GAP_MS` (10 s) beholdes.
|
||||
- Fanen viser faserne "henter X" og "sætter X af" og årsagerne `noPoolTractor`/`tooWeak`.
|
||||
|
||||
### Tømning ved høst
|
||||
|
||||
- Helper-dispatch (`role = "unloader"`, `helper = true`) bruger samme fallback: puljevogn + puljetraktor.
|
||||
- Efter `fetch`: `startUnloader` som i dag. Assignmentet bliver hos marken, til høststadiet er slut (som i dag);
|
||||
først da `returnHome`. Ingen frigivelse mellem læs.
|
||||
|
||||
### Marksilo-flowet (`adFieldSilos.lua`, `adFieldSiloPlanner.lua`)
|
||||
|
||||
- `collectVehicles` tilføjer puljekombinationer (puljevogn der kan bære varen + nærmeste puljetraktor) som
|
||||
kandidater med `pool = {...}`; planneren behandler dem som vogne (position = vognens).
|
||||
- `withoutHarvestReserve` tæller puljevogne med, så høsten går først.
|
||||
- Lån med `pool`: `ADHitch.fetch` → AD "Hent og aflever" som i dag (`enableParkAtJobFinished`) → når AD er
|
||||
inaktiv: `returnHome` → lånet slettes, når traktoren er hjemme. Vagten (120 s uden fremgang) starter efter
|
||||
koblingen.
|
||||
|
||||
## Genindlæsning
|
||||
|
||||
Reservationer og assignments gemmes ikke (som i dag). Efter indlæsning ser en traktor med puljeredskab ud som en
|
||||
færdig kombination; når markarbejdet eller marksilo-flowet afslutter et job med den, køres `returnHome`. Reglen
|
||||
afhænger kun af, hvad der hænger bag traktoren, ikke af gemt tilstand. En traktor, som ingen af vores jobs bruger,
|
||||
køres aldrig hjem af sig selv (spilleren kan have koblet et puljeredskab på for at køre selv).
|
||||
|
||||
## Fejl
|
||||
|
||||
| Situation | Handling |
|
||||
|---|---|
|
||||
| Kobling fejler (`noAttachable`, `stoppedAway`) | Traktor hjem, redskab i karantæne 10 min, marken prøver igen (andet redskab eller venter) |
|
||||
| Redskab for bredt når koblet på | Hjemtur, årsag `tooWide` i fanen |
|
||||
| Spilleren sætter sig i traktoren | Assignment afbrydes som i dag; reservationer frigives |
|
||||
| Ingen traktor stærk nok | Årsag `tooWeak` med hk-tal: "ingen traktor med ≥180 hk ledig til 'Plov 1'" |
|
||||
| Hjemtur fejler | Advarsel i loggen; traktor og redskab frigives (spilleren må ordne det) |
|
||||
|
||||
## Undtaget / uden for scope
|
||||
|
||||
- Flows (runs) — deres køretøjer er aldrig i puljen (uændret regel i `getRigs`).
|
||||
- Vogne uden Connect-markør — dedikerede som i dag.
|
||||
- Genbrug af et redskab direkte til næste mark uden at parkere (senere optimering).
|
||||
- Flere redskaber pr. traktor (front + bag).
|
||||
- Egen GUI-side for puljen.
|
||||
|
||||
## Etaper
|
||||
|
||||
1. **v1.48.0.0 — markarbejde med pulje:** `adPool.lua`, `returnHome`, dispatch-fallback, fase fetching/returning,
|
||||
kø-rækkefølge, fane-tekster. In-game: savegame4, mark der skal harves/pløjes, redskab med parkering + Connect,
|
||||
solo-traktor henter → arbejder → parkerer → hjem.
|
||||
2. **v1.49.0.0 — tømmevogn ved høst fra puljen.**
|
||||
3. **v1.50.0.0 — marksilo med puljevogne** (inkl. høst-reserve over puljevogne).
|
||||
|
||||
## Test
|
||||
|
||||
- `tests/test_adPool.lua`: puljeredskab ja/nej (koblet på, uden parkering, uden Connect), `roleOf` inkl.
|
||||
`unloader`, `powerOk` (nok/for lidt/manglende data), `pairFor` (redskab nærmest marken, traktor nærmest redskabet,
|
||||
reserveret og karantæne springes over, `tooWeak`/`noPoolTractor`/`noPoolImplement`), `queueOrder`,
|
||||
`isReturnDue`.
|
||||
- `tests/test_adHitch.lua`: `returnHome`-rutens trin.
|
||||
- `tests/test_adFieldJobs.lua`: færdig kombination foretrækkes; fallback til pulje; kø længst ventende først.
|
||||
- `tests/test_adFieldSiloPlanner.lua` (etape 3): puljekandidater og høst-reserve med puljevogne.
|
||||
@@ -1,131 +0,0 @@
|
||||
# Flows og Fields: farvet liste + redigeringsvindue
|
||||
|
||||
Dato: 2026-09-29 · Status: godkendt i chat ("men kør"), afventer review af denne spec
|
||||
|
||||
## Formål
|
||||
|
||||
Flows- og Fields-fanen er i dag én lang rullende side: listen over flows/marker efterfulgt af den valgte
|
||||
posts redigeringslinjer. Det er svært at overskue (brugerens screenshot 29/9 17:15). Målet:
|
||||
|
||||
- Fanen viser **kun listen**, med farvet status og **Start/Stop pr. række**.
|
||||
- **Klik på en række** åbner et **redigeringsvindue** (popup) oven på listen med de nuværende linjer.
|
||||
|
||||
Succes = man kan se med ét blik hvad der kører/venter/fejler, starte/stoppe en post uden at vælge den først,
|
||||
og redigere i et vindue der lukkes med Luk/ESC — uden at noget af flow-/mark-logikken ændres.
|
||||
|
||||
## Ikke-mål
|
||||
|
||||
- Ingen ændring af flow-/mark-logik, guiderne ("Nyt flow", "Opsæt mark") eller deres trin.
|
||||
- Oversigt-tavlen og Fields' flåde-visning er uændrede.
|
||||
- Ingen ny FS25-GUI-skærm/dialog (fravalgt: al rækkekode skulle genbygges; fokus/ESC oven på ESC-menuen er
|
||||
skrøbeligt). "Popup" er et panel inde i samme frame.
|
||||
|
||||
## Brugeroplevelse
|
||||
|
||||
### Flows-fanen (liste)
|
||||
|
||||
Én række pr. flow: `● navn · varer · kørende/i alt · statustekst · [Start]/[Stop]`.
|
||||
|
||||
| Niveau | Prik/statusfarve | Hvornår |
|
||||
|---|---|---|
|
||||
| running | grøn | mindst én traktor kører og ingen er blokeret |
|
||||
| waiting | gul | startet, men venter: ingen varer at hente, venter på vogn, traktor optaget, holder ved Wait |
|
||||
| error | rød | kan ikke køre: sidste start afvist (fx ingen vogn), advarsel (`!!`) |
|
||||
| stopped | grå | stoppet / aldrig startet |
|
||||
|
||||
- `[Start]/[Stop]` bruger den eksisterende `ADRunsController.startRun/stopRun` (samme regler som knaplinjen, inkl.
|
||||
sprunget-over-besked i højre side).
|
||||
- Klik på rækken (ikke knappen) åbner redigeringsvinduet for flowet.
|
||||
- Knaplinjen (i dag Tilbage, Start/Stop, Opsæt flow, Nyt flow, Slet) bliver: Tilbage, Nyt flow. Start/Stop sidder på
|
||||
rækkerne; "Opsæt flow" og "Slet" flyttes ind i vinduet.
|
||||
|
||||
### Fields-fanen (liste)
|
||||
|
||||
Nuværende kolonner (Mark, Afgrøde, Plan, Kørsel, Status, Traktorer) + `●` forrest og `[Start]/[Stop]` bagerst.
|
||||
|
||||
| Niveau | Hvornår |
|
||||
|---|---|
|
||||
| running (grøn) | marken har aktive opgaver der arbejder |
|
||||
| waiting (gul) | startet men venter: på maskine, vejr, påfyldning, "startes når en maskine er ledig" |
|
||||
| error (rød) | mangler markør, arbejdsbredde for bred/ukendt, ingen egnet maskine |
|
||||
| stopped (grå) | ingen plan / stoppet |
|
||||
|
||||
- `[Start]/[Stop]` genbruger `onClickFieldStartStop` (inkl. "Overtag høster").
|
||||
- Klik på rækken åbner vinduet med markens resumé og [Ret]-linjer.
|
||||
|
||||
### Redigeringsvinduet (fælles)
|
||||
|
||||
- Mørk halvgennemsigtig baggrund over listen; centreret panel (~70 % bredde, ~80 % højde).
|
||||
- Top: titel ("Rediger: Cow Food" / "Mark F12"), statuslinje i niveau-farve.
|
||||
- Midte: egen rullende liste med præcis det indhold, der i dag vises under listen:
|
||||
flow → `fillFlowSummary`-linjerne; mark → `fillFieldOverview`-linjerne. [Edit]/[Ret] åbner trinnet
|
||||
(guiden i enkelt-trin-tilstand) **inde i vinduet** med Gem/Tilbage som i dag.
|
||||
- Bund: `Luk` · `Start/Stop` · `Slet` (kun flow). ESC/Tilbage lukker vinduet (ikke menuen); står et trin åbent,
|
||||
går ESC først tilbage til resuméet.
|
||||
- Mens vinduet er åbent, opdateres statuslinjen og listen bagved hvert sekund som i dag.
|
||||
|
||||
## Arkitektur
|
||||
|
||||
### Rene funktioner (testbare)
|
||||
|
||||
- `ADFlowView.flowStatus(s)` → `{level, text}` hvor `s = {running, total, canStop, lastStartSkipped, warning,
|
||||
waitReason, holdReason, stopped}`. Afgør niveau efter tabellen ovenfor.
|
||||
- `ADFieldView.fieldStatus(s)` → `{level, text}` hvor `s = {working, awaiting, starting, missingMarker,
|
||||
widthProblem, noMachine, hasPlan}`.
|
||||
- `ADFlowView.LEVEL_COLORS` (grøn/gul/rød/grå) — flyttes/udleveres fra `SmartPickupStorageView`s lokale tabel,
|
||||
som også eksporterer `setTextColor` til genbrug.
|
||||
|
||||
### GUI
|
||||
|
||||
- **XML** (`SmartPickupFrame.xml`): efter `boxLayout` et `GuiElement id="editOverlay"` (skjult) med
|
||||
`Bitmap id="editBackdrop"` (fuld flade, sort ~60 %), `Bitmap id="editPanel"` (centreret, baggrund som
|
||||
fs25-paneler) med `Text id="editTitle"`, `Text id="editStatus"`, `ScrollingLayout id="editLayout"` (+ slider) og
|
||||
en knaplinje med tre `Button`s (`editClose`, `editStartStop`, `editDelete`).
|
||||
- **Rækkeprefabs**: `flowPrefab` og `fieldPrefab` får en `Bitmap name="dot"` (profil `spuDot`) forrest og en
|
||||
`Button name="rowAction"` bagerst. Kolonnebredder justeres så rækken stadig er ≈835 px.
|
||||
- **Mål-layout for rækker**: `addRow`/`addTextRow`/`addCellRow`/`addLinkRow`/`addGuideRow` kloner i dag altid ind i
|
||||
`self.boxLayout`. Der indføres `self.rowTarget` (standard `boxLayout`); vinduet sætter det til `editLayout`
|
||||
mens det fyldes. `clearRows` holder klonerne pr. layout, så listen og vinduet kan genopbygges hver for sig.
|
||||
- **Ny fil** `gui/SmartPickupEditPanel.lua` (mixin på `SmartPickupFrame`, `source()`s som de andre):
|
||||
`openEditPanel(kind, id)`, `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`,
|
||||
knap-callbacks. `kind ∈ {"flow","field"}`.
|
||||
- **Input**: `backButtonInfo`-callback og ESC tjekker først `self.editPanelOpen` (luk trin → luk vindue →
|
||||
normal Tilbage). Klik på backdrop gør intet (blokerer klik til listen bagved via `mouseEvent` der returnerer
|
||||
"brugt").
|
||||
- **Guide i vinduet**: `openFlowStep`/`openFieldStep` sætter `returnTo = "panel"`, så Gem/Tilbage vender tilbage til
|
||||
vinduets resumé i stedet for at lukke guiden til fanen.
|
||||
|
||||
### Dataflow
|
||||
|
||||
Klik på række → `openEditPanel(kind, id)` → `selectedRunId`/`selectedFieldId` sættes (eksisterende felter) →
|
||||
`requestRebuild` → `rebuild` tegner listen i `boxLayout` og, når vinduet er åbent, vinduets indhold i
|
||||
`editLayout`. Start/Stop i række eller vindue kalder de eksisterende controller-funktioner og `requestRebuild`.
|
||||
|
||||
## Fejlhåndtering
|
||||
|
||||
- Alt nyt GUI-arbejde kører i `self:safe(...)` som resten af frame'n; fejl vises i statuslinjen og logges.
|
||||
- Forsvinder flowet/marken mens vinduet er åbent (slettet), lukkes vinduet og statuslinjen siger hvorfor.
|
||||
- `rowAction`-knappen er deaktiveret når handlingen ikke er mulig (fx mark uden markør → ingen Start).
|
||||
|
||||
## Test
|
||||
|
||||
- luajit: `tests/test_flowStatus.lua` og `tests/test_fieldStatus.lua` for alle niveauer i tabellerne +
|
||||
grænsetilfælde (0 traktorer, stoppet med advarsel, venter + afvist).
|
||||
- Eksisterende tests skal forblive grønne (`test_noStrayGlobals` dækker den nye fil).
|
||||
- In-game (brugeren): liste med farver, Start/Stop pr. række, åbn/luk vindue med klik/Luk/ESC, [Edit] → Gem inde i
|
||||
vinduet, Slet flow fra vinduet, klik på listen bag vinduet gør intet, mark "Overtag høster" virker fra rækken.
|
||||
|
||||
## Levering
|
||||
|
||||
To etaper, hver sin version og in-game test:
|
||||
|
||||
1. **Flows**: rene statusfunktioner + fælles vindue + Flows-liste.
|
||||
2. **Fields**: mark-status + Fields-liste genbruger vinduet.
|
||||
|
||||
## Risici
|
||||
|
||||
- **Klik gennem backdrop** til listen: afbødes ved at backdrop'en fanger `mouseEvent`; fallback: skjul listens rækker
|
||||
(`setVisible(false)`) mens vinduet er åbent.
|
||||
- **Musehjul/rulning** i to ScrollingLayouts: hver får sin egen slider; FS25 ruller det layout musen er over.
|
||||
- **Knapper i rækkeprefabs** (Button i en klonet Bitmap) er ikke brugt før i modden: onClick sættes efter
|
||||
`clone` pr. række (som `makeRowClickable`), ikke via XML-callback.
|
||||
Reference in New Issue
Block a user