docs: design-spec for udkørsel af produkter (v1.12) — kilder, politikker, prisregel, objektlager-udtag
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# 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)
|
||||
```
|
||||
Reference in New Issue
Block a user