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

11 KiB
Raw Blame History

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)