Files
fs25-adsmartpickup/docs/superpowers/specs/2026-09-23-udkoersel-design.md

202 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```