Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f676135ac | |||
| 2a36de0837 | |||
| c415bdc7a4 | |||
| a6506fea4e | |||
| 8abcbcdcb8 | |||
| 3c67591cb3 | |||
| 784144cd4e | |||
| 13e5dbf188 | |||
| 4de0bfcb46 | |||
| 53346cc916 | |||
| 4e8e9aa350 | |||
| f8864e26c3 | |||
| b33f7682b4 | |||
| d420d5026d | |||
| 5827143327 | |||
| 76f51c7be7 | |||
| 089fb98782 | |||
| 1739b01d92 | |||
| 87b030d492 | |||
| fbd3b2c643 | |||
| 0a4ef9624d | |||
| 781c81752c | |||
| 8b4df6b15e | |||
| 1c6cda3c10 | |||
| 02bd9b8fd4 | |||
| 5a4be10996 | |||
| da9edaaabc | |||
| 3702073c8c | |||
| ccd019d32a | |||
| 6486d77cb4 | |||
| 9c55859853 | |||
| fd3f002275 | |||
| f893b5e8ca | |||
| fc646ab00b | |||
| 45f58abcf0 | |||
| 3d11ee9748 | |||
| 985114c9bf | |||
| 0cac934480 | |||
| 9b5d71b180 | |||
| 07dada58a9 | |||
| f64959e2c3 | |||
| e892348374 | |||
| 19ed87ce0f | |||
| 44f1204be9 | |||
| e9fd920f7e | |||
| 9435db929d | |||
| f068c910c7 | |||
| 0cfb983506 | |||
| 345e7303c1 | |||
| 1ca1a30b0e | |||
| 21f2e6fee6 | |||
| 5649c6c59d | |||
| 3f7b85ba54 | |||
| 6ec6a75f95 | |||
| a694d17a67 | |||
| 793f792f8b | |||
| 2454dd17e7 | |||
| 7922b8d4d0 | |||
| 3d6a3761e5 | |||
| fece920028 | |||
| be11283ba7 | |||
| 3e7e3d588e | |||
| 6af89c3034 | |||
| 5cf093c3f1 | |||
| f472f5dd80 | |||
| acbbe04a11 | |||
| db9fdf2178 | |||
| 417a44841d | |||
| a305de25a5 | |||
| 5b26415d1e | |||
| 82db248775 | |||
| a5c52cee09 | |||
| a390a05e8c | |||
| 368a4784f7 | |||
| d2ac2fb36d | |||
| b0d22236b0 | |||
| 1759d62600 | |||
| e778ab44b4 | |||
| 59e18acc9c | |||
| 8d33499a37 | |||
| 5a2701fc62 | |||
| 6f46bb543a | |||
| 473f6110bf | |||
| 760fbac328 | |||
| 6eff1dcae8 | |||
| 7730f68dec | |||
| 0e714d2968 | |||
| 3f19738743 | |||
| 2eec23add7 | |||
| 3ac53b7d1f | |||
| 2dc39b7849 | |||
| 2d170a30ff | |||
| cdd253ee5f | |||
| 4072445105 | |||
| 4760ee5e19 | |||
| 4863e422ad | |||
| 127f4cc9d9 | |||
| e7b0e5c8f5 | |||
| b8704f1f95 | |||
| f2ebc54b25 | |||
| b57940a933 | |||
| be043ac220 | |||
| fde3fd4399 | |||
| 5a715bc3e9 | |||
| 28d2f2c086 | |||
| 6aeb8e4179 | |||
| 5e1d6a3659 | |||
| 5bb1ad6ae0 | |||
| 521dc10df5 | |||
| f3db8e4e5c | |||
| df514fb6ec | |||
| 3ec4aaa179 | |||
| 5ceb7da87d | |||
| c1060cddf9 | |||
| 9c1c87b8dd | |||
| 9348443f61 | |||
| 331d192f78 | |||
| 7d372eb5ab | |||
| 9a284c875e | |||
| 7e0a991ce6 | |||
| 25867e2d88 | |||
| 490eecc25e | |||
| bcfa237321 | |||
| f1b8b64668 | |||
| de88846166 | |||
| 3a1d49ccef | |||
| 6afcae6fe1 | |||
| 12647087ef | |||
| a6a0dbf121 | |||
| 565d7923cd | |||
| 2b3793eeaa | |||
| 29a142d3df | |||
| a626fcdd95 | |||
| 4ae268376d | |||
| 19893aaf41 | |||
| 37768c1507 | |||
| d7f4d0b3d3 | |||
| c2ab20d9aa | |||
| 2e64ad537c | |||
| fca26f5b4d | |||
| 31c993c6eb | |||
| ccc6b608dc | |||
| e9272b8b8f | |||
| 1a8931116b | |||
| 8a0d7f2489 | |||
| f93ae798e3 | |||
| 33b4f754b0 | |||
| 6fbf05273e | |||
| 2beca0924a | |||
| 5df4fdc7c3 | |||
| 5274925f2a | |||
| 307676004e | |||
| 50301b61b9 | |||
| 39fa72ba0d | |||
| ac3db0d826 | |||
| 4aa221820d | |||
| 6a835b9efb | |||
| f9d875fc3e | |||
| 954dd25c51 | |||
| 10838779bc | |||
| 59b6ac1e47 | |||
| a04c16552a | |||
| dde6f4b8eb | |||
| 151954bd37 | |||
| 9b51a68731 | |||
| 92af260a79 | |||
| 7cbd8fb878 | |||
| f174c10da5 | |||
| a95cb9e71d | |||
| 780b6a0daa | |||
| 61e85e1402 | |||
| 4c60fd555f | |||
| 9ef1eb6655 | |||
| 77253b5d2b | |||
| 1b0a2c152b | |||
| 813ee6d0b9 | |||
| 455c9c7598 | |||
| 92e65a6431 | |||
| e02d8af3d5 | |||
| 5d06c46fa9 | |||
| 6138b6b874 | |||
| abbe40da01 | |||
| 36f381fc36 | |||
| 9be8fa98f0 | |||
| 08d983800c | |||
| 41e50ccec8 | |||
| 725ad7b459 | |||
| cb68c81a3f | |||
| 6fd917caca | |||
| 8a18187103 | |||
| 0fca6e8a9a | |||
| 038cb5be54 | |||
| 05832603e4 | |||
| c34a472b1c | |||
| 7a073ae047 | |||
| 322f7b1c30 | |||
| 293d66156f | |||
| af71548b0b |
@@ -1 +1,5 @@
|
||||
*.zip
|
||||
docs/superpowers/
|
||||
bench/
|
||||
.superpowers/
|
||||
tests/
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
# Ændringslog — FS25_ADSmartPickup
|
||||
|
||||
Nyeste version øverst. Versionsnummeret står i `FS25_ADSmartPickup/modDesc.xml`. Brugervejledningen
|
||||
står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUAL.md](docs/USER_MANUAL.md)
|
||||
(engelsk). De ældste afsnit (v1.1–v1.13) er de oprindelige udviklernoter, der tidligere stod i README.
|
||||
|
||||
---
|
||||
|
||||
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
|
||||
|
||||
- **1.60.21** Flow-vogne: kun et redskab med lastrum (ikke plov, kultivator eller såmaskine) huskes som flowets vogn. Før kunne en plov, der hang bag en flow-traktor, blive gemt som gyllevogn, og gyllen blev aldrig kørt.
|
||||
- **1.60.20** Vinhøsterens bredde: også en lille bredde fra Courseplay (under 1,5 m) erstattes af vinrækkens 3 m.
|
||||
- **1.60.19** Vinhøster uden kendt arbejdsbredde får vinrækkens 3 m (før "bredde ukendt", og marken blev ikke startet).
|
||||
- **1.60.18** Vinhøsterens afgrøde læses fra `inputFruitTypeIndex` (feltnavnet i FS25).
|
||||
- **1.60.17** Vinhøstere (fx Grégoire GL86) genkendes som høstere af den afgrøde, de er konfigureret til
|
||||
(spec_vineCutter). Før fik vinmarker "ingen kan" (in-game: F22 hvide druer).
|
||||
- **1.60.16** Tipvogne skiftes under AD-aflæsning til en tip-side, AutoDrive må aflæsse fra (`allowAIDischarge`).
|
||||
Farmtech EDK 650 (Unrealistic) tillader kun bagudtip; stod den på venstre, højre eller kornluge, åbnede AD
|
||||
aflæsningen, men intet løb ud, og vognen holdt med fuldt læs. CP-genstartslisten logges kun, når den ændrer sig.
|
||||
- **1.60.15** Humle (HOPS, Dakota) behandles som vinafgrøde: aldrig slåafgrøde, og markmaskiner kører på rækkerne som ved druer.
|
||||
- **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står
|
||||
med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway".
|
||||
- **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets
|
||||
markopgaver starter CP med eget job, så HUD'en kunne stå på fx bunkersilo, og HUD-starten gav "CP: No silo found".
|
||||
- **1.60.12** "Start alle" ophæver altid pausen: et flow på pause, hvis traktorer stadig kørte en gemt AD-tur,
|
||||
blev talt som "kørte allerede" og forblev stoppet — traktoren hentede sin vogn og kørte den straks hjem igen.
|
||||
- **1.60.11** Udkørselsregler vises kun, når en vare bruger dem: salgsgrænserne ("Sælg fra", "Sælg altid
|
||||
over") kun ved Salg / Salg, ellers lager, og "Kør til lager over" kun ved Lager / Salg, ellers lager.
|
||||
Gælder Regler-trinnet, resuméet og status (et rent lagerflow viser fx kun "lager > 80 %").
|
||||
- **1.60.10** Guiden "Opsæt flow" tildeler ledige vogne til solo-traktorer (traktor uden vogn bagpå):
|
||||
guiden foreslår en vogn (navnematch, ellers "Disponibel …"), vognen kan skiftes pr. traktor på linjen
|
||||
"↳ Vogn", og flowet husker parret ved Gem.
|
||||
- **1.60.9** Tydelig linje øverst i Fields, når "Stop alt"-pausen er aktiv (pausen gemmes med savegamet).
|
||||
- **1.60.8** Gemt produkt (ensilage/hø) gælder kun slåafgrøder.
|
||||
- **1.60.7** Traktor med redskab på sin egen parkering er et parkeret sæt, ikke en forældreløs påkobling.
|
||||
- **1.60.6** Tømning ved stop bruger aldrig marksiloer; kan en silo ikke tage imod, prøves den næste
|
||||
(op til 3), før der parkeres med last.
|
||||
- **1.60.4–1.60.5** Redskab/vogn der er koblet på uden ejer efter genindlæsning, sættes tilbage efter
|
||||
2 min uden opgave. Flowet husker kun vognen, når et kørende flow stoppes.
|
||||
- **1.60.3** Diagnoselinje når vognen står ved målet med plads, uden at læsset falder, i 30 s.
|
||||
- **1.60.2** Vin og oliven er aldrig slåafgrøder.
|
||||
- **1.60.1** Fanen Smart Pickup ligger lige over spillets Indstillinger i ESC-menuen.
|
||||
- **1.60.0** Lagerfanernes "Kræver handling": klik åbner eller opretter et forsyningsflow (løber tør) eller et
|
||||
salgsflow (sælg nu), eller siger "producér/køb", når et flow ikke hjælper. Statuslinjen i lagerfanerne
|
||||
viser ikke længere det sidst valgte flows status.
|
||||
|
||||
## 1.59 — Fields som farvet liste, Pleje Automatisk/Manuelt
|
||||
|
||||
- **1.59.0** Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet.
|
||||
- **1.59.1–1.59.5** Review-rettelser til listen og redigeringsvinduet (placering, fokus, ESC, [Start]
|
||||
kræver markør); vinduet i menuens mørke tone; tekstrækker op til 90 tegn; forklaring af farver i hint.
|
||||
- **1.59.2** Flow der ikke er stoppet og har kørende traktorer, tæller som kørende efter genindlæsning.
|
||||
- **1.59.3** Under "Stop alt"-pausen vises marker som stoppet med [Start]; [Start] genoptager kun den mark.
|
||||
- **1.59.6–1.59.7** Traktorer der ikke findes længere (solgt), fjernes fra flowet 2 min efter indlæsning,
|
||||
med besked på skærmen; rød besked når flowet står uden traktorer.
|
||||
- **1.59.8** Pleje har Automatisk/Manuelt som Klargøring (Automatisk = gødning, ukrudt og kalk).
|
||||
- **1.59.9–1.59.11** Markvinduet viser ikke marksilo-advarsler; kolonnebredder i Marksiloer og Måned.
|
||||
|
||||
## 1.58 — Flows som farvet liste med redigeringsvindue
|
||||
|
||||
- **1.58.0** Flows-fanen som farvet liste med [Start]/[Stop] pr. række; klik åbner et redigeringsvindue
|
||||
(Luk/ESC, Start/Stop, Slet).
|
||||
|
||||
## 1.57 — Forsyning og Wait-valg
|
||||
|
||||
- **1.57.0** Mindst fyldte kilde først kun blandt kilder med mindst 25 % af et læs (ellers fyldeste).
|
||||
- **1.57.1–1.57.2** "Vent ved: Alle" vælger det ledige Wait-punkt nærmest flowets kilder.
|
||||
- **1.57.3** Manuel påkobling starter traktoren i et flow, hvis Start blev afvist.
|
||||
- **1.57.4** AutoDrive tælles ikke som Courseplay (flow-traktor ventede på sig selv efter indlæsning).
|
||||
|
||||
## 1.56 — Bedste pasform i maskinpuljen
|
||||
|
||||
- **1.56.0** Puljen vælger den mindste traktor med hk nok; afstand kun inden for ±15 %; over 2,5× redskabets
|
||||
behov kun som nødløsning.
|
||||
- **1.56.1–1.56.5** Flow prøver én gang mere at hente vognen; traktor startes, når spilleren selv har kørt
|
||||
den til Connect og koblet vognen på; AD "Kør til" et puljeredskabs parkering kobler det af; flowet
|
||||
husker vognen før et stop; afviste traktorer ved Start vises med grund.
|
||||
|
||||
## 1.55 — Redskaber foldes ved stop
|
||||
|
||||
- **1.55.0** Redskaber slukkes, løftes og foldes til transport, når en mark, et flow eller "Stop alt" stoppes.
|
||||
- **1.55.1** Tømning ved stop: 45 s ved målet uden at læsset falder → silo, ellers parkering med last.
|
||||
|
||||
## 1.51–1.54 — Flow-vogne og kobling
|
||||
|
||||
- **1.51.0** Flow-vogne: flowets vogn huskes pr. traktor, solo-traktor henter sin vogn ved start, stop/slet
|
||||
sætter vognen på plads og kører traktoren hjem; stoppede flows er ledige i maskinpuljen.
|
||||
- **1.51.1** Konsolkommandoen `spuNameFleet` giver traktorer uden for flows butiksnavne og parkeringer
|
||||
"P <navn>".
|
||||
- **1.51.3–1.51.4** Flow-traktor finder sin vogn på parkeringsnavnet; stoppet flow tømmer vognen før den
|
||||
parkeres.
|
||||
- **1.51.5** Nye Courseplay-kurser får altid 2 forager-runder.
|
||||
- **1.52.0** Lagerfanerne får vareoversigt med nøgletal, "Kræver handling" og fyldningsbjælker.
|
||||
- **1.52.1** Flow startet igen under tømning/hjemtur kører videre med vognen.
|
||||
- **1.53.0** Koblingsrækkevidde 3 m og bakning de sidste 3–8 m ved Connect-markører.
|
||||
- **1.53.1–1.53.2** Tømning ved stop kun ved flowets egne mål; afkobling bekræftes (op til 20 s).
|
||||
- **1.54.0** Flow-vogne holdes altid af deres flow; markredskaber kun bag traktorer.
|
||||
|
||||
## 1.48–1.50 — Maskinpulje
|
||||
|
||||
- **1.48.0** Maskinpulje etape 1: markarbejdet sender en solo-traktor efter et puljeredskab (frakoblet, med
|
||||
parkering + Connect-markør), når ingen færdig kombination er ledig; bagefter sættes redskabet af på sin
|
||||
parkering, og traktoren kører hjem. Marker der venter, betjenes længst-ventende først.
|
||||
- **1.48.1–1.48.4** Bar mark med mål-afgrøde klargøres straks (kun såningen venter); plov før kultivator i
|
||||
rolletabellen; markflåden viser puljetraktorer og -redskaber med status.
|
||||
- **1.49.0** Etape 2: tømmevogne ved høst fra puljen.
|
||||
- **1.50.0** Etape 3: marksilo-flowet låner også solo-traktor + puljevogn.
|
||||
- **1.50.2–1.50.5** Plov foldes ud før afkobling; kultivering/pløjning måles på jorden (≥95 %); tom/fuld
|
||||
marksilo og fuld fabrik håndteres.
|
||||
|
||||
## 1.46–1.47 — Connect- og Afkobl-markører
|
||||
|
||||
- **1.46.0** Connect-markører: en traktor der kører AD "Kør til" en "Connect <navn>"-markør, kobler
|
||||
redskabet på ved ankomst.
|
||||
- **1.47.0–1.47.4** Afkobl-markører; Connect virker begge veje; ture i flere trin; vognen sættes af ved sin
|
||||
egen AD-parkering.
|
||||
|
||||
## 1.41–1.45 — Navne, Stop alt, Courseplay-genstart
|
||||
|
||||
- **1.41.0** Markmaskiner med modelnavn navngives efter rollen (Læssevogn 5, Mejetærsker 1 …).
|
||||
- **1.41.1** Navnefilen skrives i spillets gem-hook.
|
||||
- **1.42.0** "Stop alt og parkér" og "Start alle" med pause der gemmes.
|
||||
- **1.42.1–1.42.5** Parkeringspunkt findes også på vognen/redskabet; højst én maskine sendt ud hvert
|
||||
10. sekund; vent op til 5 min på Courseplays kursusberegning; Flows-fanen med valgt flow øverst.
|
||||
- **1.43.0** Høster med korn i tanken efter høsten: tømmevogn sendes ud, før marken går videre.
|
||||
- **1.44.0** Marksilo: "Kør overskud til produktion" med foderreserve.
|
||||
- **1.45.0** Maskiner som Courseplay kører, huskes ved hvert gem og startes igen efter indlæsning.
|
||||
|
||||
## 1.36–1.40 — Marksilo-flow og bygningsnavne
|
||||
|
||||
- **1.36.0** Fanen Marksiloer: låner ledige tømmevogne og kører marksiloernes varer hjem til gårdlagre.
|
||||
- **1.36.x** Flow-guiden viser ubrugelige traktorer med grund (foldet sammen); grunden "AutoLoad er slået
|
||||
til på vognen"; pallespawnere viger for aflæsningens markør.
|
||||
- **1.37.0** CP-maskiner midt i et job genstartes efter indlæsning.
|
||||
- **1.38.0** Manuel transport af brede høstere ("Kører selv på vejen" Ja/Nej).
|
||||
- **1.38.x** Rettelser til CP-startpunkt, genstart og marksilo-navne.
|
||||
- **1.39.0** Egne bygningsnavne fra `ADSmartPickup_names.xml` bruges overalt.
|
||||
- **1.40.0** Nye bygninger navngives automatisk; knappen "Omdøb bygning" i bygningsfanerne.
|
||||
|
||||
## 1.33–1.35 — Courseplay-kursusgenbrug og fanen Måned
|
||||
|
||||
- **1.33.0** Udkørsel: "Sælg altid når kilden er over X %".
|
||||
- **1.33.1–1.33.4** Vinmarker, maskiner langt fra vejnettet sendes ikke ud, intet sendes de første 2 min
|
||||
efter indlæsning, hurtigere rutetjek.
|
||||
- **1.34.0** Genbrug af Courseplay-kurser (markform-signatur + kursusregister pr. maskine).
|
||||
- **1.34.1–1.34.2** Slåmarker klar først i fuld højde; utoppede kartofler tæller som høstklare.
|
||||
- **1.35.0** Fanen Måned: marker i gang, næste trin, og om høst-/såvinduet lukker ved månedsskiftet.
|
||||
- **1.35.x** Fremdrift for høst/slåning; diagnose af stillestående AD-køretøjer; fyldte traktorer
|
||||
registreres "på vej".
|
||||
|
||||
## 1.32 — Guiden "Opsæt flow"
|
||||
|
||||
- **1.32.0** Flows-fanen med tavle + resumé og trin-for-trin-guide; nyt flow oprettes først ved Gem.
|
||||
- **1.32.1–1.32.5** Oprydning i gammel GUI-kode; høsterens restkorn; advarsel når markflåden er tom;
|
||||
forkert vare hældes kun ud som lille rest; vinkursus i gangen.
|
||||
|
||||
## 1.31 — Guiden "Opsæt mark"
|
||||
|
||||
- Markfanen får resumé (klik på en linje = ret ét trin) og en trin-for-trin-guide.
|
||||
|
||||
## 1.21–1.30 — Markarbejde
|
||||
|
||||
- **1.21** Fanen Marker: slåafgrøder (slå, vend, riv, saml op) med AutoDrive til F-markøren og Courseplay
|
||||
på marken; arbejdsbredde fra Courseplay.
|
||||
- **1.22–1.25** Vender/river/læssevogn genbruger slåmaskinens kursus; smalle maskiner; sideriver; ensilage
|
||||
til gæringssilo.
|
||||
- **1.26–1.27** Høst, klargøring, pleje, multi-tool, vejr, påfyldning, mål-afgrøde pr. mark, halm efter høst;
|
||||
ydelsesforbedringer.
|
||||
- **1.28** Profiler og hak-audit; æg-paller; forsyning med flere mål vælger målet med færrest vogne på vej.
|
||||
- **1.29** Bebyggede marker udelades; minimum arbejdsbredde 1,5 m; påfyldning via AD og købestationer;
|
||||
lange tekstrækker ombrydes.
|
||||
- **1.30** Faste maskiner pr. mark; markflåden er tydeligt fælles.
|
||||
|
||||
## 1.14–1.20 — Forsyning, lagerfaner og Overblik
|
||||
|
||||
- **1.14** Bedste foder først (lavere foder kun når truget er mindst 75 % tomt); læs på vej tælles med;
|
||||
rest under 100 l regnes som tom.
|
||||
- **1.15–1.16** Lagerfaner pr. bygningstype med statistik pr. vare; første brugermanual.
|
||||
- **1.17** Traktorer navngives efter deres flow; kilder med fælles lager regnes som ét stop; lagerture til
|
||||
egne fabrikker; jordbundne kortanlæg regnes som gårdens.
|
||||
- **1.18** "Lager til" (tvunget aflæsningssted, fodring ved ≥ 10.000 l); siloer der kun kan fyldes; generelle
|
||||
lagre som kilde; æg/uld-paller fra stalde.
|
||||
- **1.19** Fanen Overblik; "Lager til" filtreres efter varer; parallelt foder (høns).
|
||||
- **1.20** Start/Stop alle flows; klik på et flow i Overblik hopper til redigering.
|
||||
|
||||
## 1.10–1.11
|
||||
|
||||
- **1.10** Overblik i fanen; "Kør hjem fra Wait under X %" (10–90, standard 50); foderprioritering efter
|
||||
spillets fodervægt.
|
||||
- **1.11** Flere mål pr. run; faneikon; forkert last læsses af i nærmeste silo; forbedret Wait-logik.
|
||||
|
||||
## v1.13 — Flow-tavle (bygningsbaseret opsætning)
|
||||
|
||||
ESC-fanen har nu to under-faner: **Flows** og **Lagre**. Flows viser én række pr. flow (run):
|
||||
`> Mælk: Sukker · Kostald 1 + Kostald 2 → salg · pris ≥ 90 %, lager > 80 % · Virtus · kører (1/1) — SUGAR 78 % af top`
|
||||
og under tavlen en detail-blok for det valgte flow: **Retning** (ind til bygning / ud fra bygning),
|
||||
**Fra** (udkørsel) eller **Til** (forsyning) som afkrydsning pr. **bygning** — moden finder selv den
|
||||
AD-markør der står nærmest bygningens læsse-/aflæsningstrigger (≤ 40 m; siloer via det strenge
|
||||
opslag) og viser den: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `INGEN MARKØR inden for
|
||||
40 m`. Derefter Varer, Regler, Vent ved, Traktorer og Status som før. Forsyning har "Fra = alle
|
||||
siloer" (som hidtil). Wait-puljerne står nederst.
|
||||
|
||||
Gamle runs oversættes automatisk: markører der er en bygnings markør bliver til bygningen; rå
|
||||
markører uden bygning beholdes og vises som `markør '…'`. Start afviser med `'<bygning>' mangler en
|
||||
AD-markør ved triggeren`, når en valgt bygning ingen markør har. Runs gemmes som XML version 3.
|
||||
|
||||
## v1.12 — Udkørsel af produkter
|
||||
|
||||
Et run har nu en **retning**: *Forsyning* (som hidtil) eller *Udkørsel*. ESC-fanen har tre
|
||||
under-faner: **Oversigt** · **Forsyning** · **Udkørsel**.
|
||||
|
||||
Et udkørsels-run består af:
|
||||
|
||||
- **Kilder** (én eller flere AD-markører): en **fabrik** (produktionspunkt, dens outputs),
|
||||
en **silo** (`spec_silo`, dens indhold) eller et **objektlager** (palle-/ballelager som HVF-
|
||||
pakkens; paller tages ud automatisk). Kilden genkendes inden for 40 m af markøren.
|
||||
- **Varer** med politik pr. vare: **Fra** · **Lager** · **Salg** · **Salg, ellers lager**.
|
||||
Bulk-varer kræver tipvogn/tank; pallevarer kræver **Universal Autoload** på vognen.
|
||||
- **Regler:** *Sælg fra* (% af årets toppris, standard 90), *Kør til lager over* (% fyldning i
|
||||
kilden, standard 80), *Saml op på én tur* (opsamlingsrunde).
|
||||
- Wait-pulje og traktorer som i forsynings-runs.
|
||||
|
||||
**Turvurdering** (før hver pålæsning, og hvert 5. sekund mens traktoren holder):
|
||||
|
||||
1. *Salg*: en vare med Salg-politik, hvis bedst betalende **nåelige** salgssted giver mindst
|
||||
tærsklen af årets toppris — **kører uanset mængde**. Højeste pris-% først. Toppris = stationens
|
||||
grundpris nu × (årets højeste periodefaktor ÷ nuværende). Stor efterspørgsel giver > 100 %.
|
||||
2. *Lager*: en vare med Lager-politik, når der er et **fuldt læs** eller kildens lager er over
|
||||
tærsklen — til nærmeste silo med plads til hele læsset og vejnet begge veje (aldrig en kilde,
|
||||
fabrik eller stald). Mest fyldte kilde først.
|
||||
3. Ingen tur → traktoren holder ved et ledigt Wait-punkt (run'ets pulje) og prøver igen.
|
||||
Loggen viser hvorfor: `holder ved Wait 3: SUGAR 78 % af top (kræver 90), lager 41 %`.
|
||||
|
||||
**Opsamlingsrunde:** tipvogn = samme vare fra flere kilder efter tur til vognen er fuld (næste
|
||||
kilde kun ved ≥ 10 % fri plads); autoload-vogn = flere pallevarer med samme mål på én tur.
|
||||
|
||||
**Objektlager:** markøren skal stå så vognens autoload-zone dækker lagerets spawn-område (som når
|
||||
man læsser manuelt). Moden beder lageret spawne **én palle ad gangen** og venter til UAL har taget
|
||||
den, så der aldrig ligger paller tilbage. Sidder lageret fast (kendt spilfejl), afsluttes
|
||||
læsningen efter 30 s med det der er. Ved en autoload-kilde uden fremdrift i 15 s køres der med
|
||||
det der er ("uanset mængde").
|
||||
|
||||
**Advarsler på skærmen:** kan intet salgssted nås (vejnet), eller findes ingen silo med plads,
|
||||
vises en rød AutoDrive-notifikation (`'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR –
|
||||
tjek vejnettet`), samme tekst i AD's notifikations-historik, i log.txt (WARN) og i run'ets status.
|
||||
Traktoren holder ved Wait og kører selv når vejen er koblet på. Advarslen gentages først når
|
||||
traktor, station eller vare ændrer sig.
|
||||
|
||||
**Prisregel (v1.12.1, metode fra Distribution Redux):** pris-% = nuværende periodes faktor ÷ årets
|
||||
højeste faktor i spillets faste sæsonkurve (samme data som prisgrafen) × stationens egen afvigelse
|
||||
(effektiv pris ÷ stationens grundpris nu). Sværhedsgraden falder ud; flade kurver (top/bund < 1,02)
|
||||
regnes som 100 %; stor efterspørgsel giver > 100 %.
|
||||
|
||||
**Reserve (v1.12.1):** før salg fratrækkes det gårdens egne fabrikker og stalde skal bruge det næste
|
||||
døgn (forbrug × 24 t minus det de allerede har, højst deres fri plads; fabrikker = ønskede
|
||||
produktioners inputs, stalde = foder/halm/vand-rate). Der sælges kun når et helt læs kan tages uden
|
||||
at røre reserven. Loggen/UI'et siger fx `SUGARBEET: 196.0k reserveret til TMR-fabrik`. Lager-politik
|
||||
rører ikke reserven (varen bliver på gården).
|
||||
|
||||
**Paller til lager (v1.12.1):** Lager-mål for pallevarer = nærmeste objektlager (palleskur) der tager
|
||||
varen og har plads; AutoDrive læsser UAL-vognen af dér.
|
||||
|
||||
**Tætte siloer (AutoDrive-indstilling):** AutoDrive tipper ved den første aflæsningstrigger inden for sin
|
||||
globale "Max trigger distance" (standard 25 m). Står siloerne tættere end det, kan nabosiloen få læsset —
|
||||
sæt AD-indstillingen til **10 m**. Moden advarer i loggen første gang et lagermål har en anden
|
||||
aflæsningsmarkør inden for afstanden.
|
||||
|
||||
**Fanen Lagre (v1.12.1):** gårdens beholdning pr. bygning (fabrikker med ▸ ved outputs, stalde,
|
||||
palleskure som antal paller, siloer), opdateret hvert sekund.
|
||||
|
||||
**Opsætning:** Udkørsel-fanen → Nyt run → afkryds kilder → sæt politik pr. vare → Ja på traktorer
|
||||
→ Start. AD-tilstand pr. traktor: PickupAndDeliver, 1./2. markør = første kilde (pladsholder,
|
||||
sættes pr. tur), rotation "kun pålæsning". Runs gemmes i `ADSmartPickup_runs.xml` version 2;
|
||||
ældre filer læses som forsynings-runs.
|
||||
|
||||
## v1.9 — læs-bytte fra Wait
|
||||
|
||||
Venter en traktor ved et Wait-punkt med en vare målet er fuldt af, mens målet **samtidig mangler
|
||||
en anden vare** på traktorens liste (run eller AD's multi-valg) — kan der ellers gå i hårdknude:
|
||||
alle traktorer venter med den ene vare, mens ingen henter den anden. Hvert 5. sekund tjekkes derfor:
|
||||
|
||||
1. Er der en anden vare i listen, målet mangler et **fuldt** læs af (og som kan hentes fra en
|
||||
silo)? Ellers venter traktoren som hidtil.
|
||||
2. Findes en **ren lager-silo** (ikke stald, ikke fabrik, og ikke målet selv) med en
|
||||
aflæsningsstation ved en AD-markør, der tager varen og har fri plads ≥ resten, samt vejnet
|
||||
både til siloen og videre fra siloen til målet? Siloer der **allerede har varen** foretrækkes,
|
||||
ellers den nærmeste.
|
||||
3. Findes en silo: Wait-punktet frigives, traktoren kører derhen og læsser resten af. Når vognen
|
||||
er tom, henter næste tur den vare målet mangler.
|
||||
4. Findes ingen egnet silo: traktoren bliver ved Wait-punktet (som før v1.9).
|
||||
|
||||
Loglinjer: `'<navn>' bytter læs: N l STRAW tilbage i <markør> (markør n), målet mangler FORAGE` ved
|
||||
bytte, og én gang pr. venteperiode `'<navn>' venter med N l STRAW, og målet mangler FORAGE — ingen
|
||||
silo med plads og vejnet til/fra tager resten, så den bliver ved Wait-punktet` når ingen silo findes.
|
||||
|
||||
Kræver at traktoren har **begge** varer i sin liste (run's varer eller AD's multi-valg) — kun én
|
||||
vare valgt betyder ingen bytte-mulighed, og traktoren venter som hidtil. Wait-logikken gælder desuden
|
||||
kun mens aflæsningsopgaven peger på AD's rigtige aflæsningsmål, så en silo-omvej (fx efter et bytte)
|
||||
aldrig fejlagtigt bliver opfattet som et fuldt mål.
|
||||
|
||||
## v1.8 — Runs i ESC-menuen
|
||||
|
||||
Et **run** er en navngivet opskrift: mål + varer + Wait-pulje + ture, samt en liste traktorer der skal
|
||||
køre den. I stedet for at sætte AutoDrive op manuelt på hver traktor, defineres run'et ét sted og
|
||||
startes/stoppes samlet for alle traktorer i det.
|
||||
|
||||
**Oprettelse:** ESC → fanen "Smart Pickup" → "+ Nyt run". Runs vælges i Run-selectoren øverst;
|
||||
"Omdøb" og "Slet run" findes som knapper nederst sammen med Start/Stop.
|
||||
|
||||
**Felter:**
|
||||
|
||||
- **Mål** — dropdown med aflæsningsmål (markører med aflæsningsstation, fx en stald eller fabrik).
|
||||
- **Varer** — Auto (følger målets behov, som ved almindelig behovsstyret forsyning) eller Vælg
|
||||
(afkrydsning af bestemte varer).
|
||||
- **Wait-pulje** — hvilken `Wait`-mappe rest-læs skal vente i ("Alle (nærmeste ledige)" eller en
|
||||
navngivet pulje som fx "Cow Wait").
|
||||
- **Ture** — loop-tælleren (0–99, 0 = uendeligt), som normalt i AutoDrives Pickup&Deliver.
|
||||
- **Traktorer** — rækker med Ja/Nej (er traktoren med i run'et), kapacitet og et ✔/✖-ikon. En
|
||||
**✖-traktor springes over ved Start**, med en grund vist ved siden af (fx "ingen vogn", "vognen
|
||||
kan ikke bære run'ets varer", "for langt fra vejnettet" eller "intet mål"). Sættes en traktor til
|
||||
Nej (fjernet fra run'et), rører det IKKE dens AutoDrive-vareliste — run'ets varer forbliver valgt
|
||||
i traktorens AD-vareliste (synlig/redigerbar i AD's eget HUD) indtil de ændres dér manuelt.
|
||||
|
||||
**Start/Stop:** "Start run" sætter AutoDrive op på hver ✔-traktor: mode Pickup&Deliver, målmarkør,
|
||||
nærmeste pålæsningsmarkør som start, run'ets varer valgt, run'ets tur-tæller, mapper ("Brug mapper")
|
||||
slået til, og rotation sat til "kun pålæsning" (bemærk: "Brug mapper" er en global
|
||||
AutoDrive-indstilling og slås til for hele spillet, ikke kun run'ets traktorer). **Dette overskriver
|
||||
traktorens hidtidige manuelle AutoDrive-indstillinger.** "Stop run" stopper AutoDrive på hver traktor
|
||||
i run'et (medlemskabet i run'et bevares, så det kan startes igen). Traktorer der ikke er del af noget
|
||||
run, kører som før — bortset fra at "Brug mapper" kan være blevet slået til globalt af et run.
|
||||
|
||||
**Wait-puljer-afsnittet** nederst på siden viser hver Wait-mappe skrivebeskyttet: antal punkter og
|
||||
hvilken traktor der holder hvor.
|
||||
|
||||
**Gemning:** Runs gemmes i `savegameN/ADSmartPickup_runs.xml` (ved gem og ved Start/Stop). De
|
||||
**genstartes ikke automatisk** ved indlæsning af et savegame — kør "Start run" igen efter load, hvis
|
||||
traktorerne skal køre videre.
|
||||
|
||||
## v1.1–v1.7 — Grundfunktionerne
|
||||
|
||||
### Vare-bevidst mappe-rotation (én vare valgt)
|
||||
|
||||
Når pålæsningsmål roteres i en AD-mappe, køres der direkte til den silo i mappen, der har den
|
||||
valgte vare — mindst fyldte først (mælk/gylle: fyldeste først) — i stedet for at prøve markørerne én efter én.
|
||||
|
||||
### Behovsstyret forsyning (flere varer valgt)
|
||||
|
||||
Én traktor holder alle input i en produktion fyldt — fx hø, græs, ensilage og sukkerroer til en
|
||||
TMR-blander — i stedet for én traktor pr. vare.
|
||||
|
||||
1. Aflæsningsmål = produktionens markør.
|
||||
2. Multi-vælg ingredienserne i AutoDrives vare-dropdown.
|
||||
3. Start Pickup&Deliver. Loop-tælleren virker som normalt (N læs → parkér, 0 = uendeligt).
|
||||
|
||||
Pr. tur læses målets lager; den ingrediens der mangler mest (relativt til kapacitet) hentes fra en
|
||||
silo **hvor som helst på kortet** (eget lager før nabo-lån, mindst fyldte først). Ingredienser uden
|
||||
plads i målet (< 1000 l fri) eller uden kilde springes over. Mens AD kører viser HUD'en kun turens
|
||||
vare (AutoDrive ville ellers selv rotere vare ved triggeren); når AD stopper, gendannes hele listen.
|
||||
Slå fra ved at vælge en anden enkelt vare.
|
||||
|
||||
"Mest brug for" defineres i `ADSupplyPlanner.scoreNeed` (`adSupplyPlanner.lua`).
|
||||
|
||||
### Fabriksstyret (v1.6): målet er et produktionspunkt
|
||||
|
||||
Er aflæsningsmålet en fabrik (produktionspunkt), **erstattes** ingredienslisten af inputs fra
|
||||
fabrikkens ønskede produktioner — slår du fx TMR til i en AIO-fabrik, begynder traktoren at hente
|
||||
ensilage/hø/halm/roer/mineralfoder af sig selv; slår du den fra, holder den op. Dit multi-valg er kun
|
||||
tændknappen (og det gendannes ved stop). Listen genudledes hver tur, begrænset til det vognen kan bære.
|
||||
|
||||
- **Ønsket produktion** = tændt, ELLER slukket af kortet med status "mangler input" (Dakotas
|
||||
`ExtendedProductionPoint` slår produktioner fra automatisk, når de løber tør — dem skal vi netop
|
||||
fodre). Manuelt slukket = ikke ønsket.
|
||||
- **Prioritet = tid til tom:** `lager / (Σ amount × cyclesPerMinute)` over ønskede produktioner —
|
||||
den vare der løber tør først hentes først, uanset relativ fyldning. Loggen viser forbrug og minutter.
|
||||
- Fabrik uden ønskede produktioner, eller inputs vognen ikke kan bære → dit multi-valg som hidtil.
|
||||
|
||||
### Kilder: en fabriks inputs stjæles aldrig (v1.6.1)
|
||||
|
||||
En fabriks læssestation udleverer også dens **inputs** (halm i en TMR-fabrik). Sådan en vare hentes
|
||||
aldrig derfra — kun fabrikkens outputs og rigtige siloer er kilder. Gælder både mappe-logik og
|
||||
behovsstyring. Varer der både er input og output (kæde) må hentes.
|
||||
|
||||
### Stalde som mål (v1.6.1)
|
||||
|
||||
Kostald m.fl. svarer selv for niveau/kapacitet (`getHusbandryFillLevel/Capacity`), så halm+TMR til en
|
||||
stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder.
|
||||
|
||||
### Wait-punkter: rest-læs ved fuld stald (v1.7)
|
||||
|
||||
AutoDrive holder ellers en traktor med rest-læs stille ved aflæsningspunktet, til den er tømt — og
|
||||
spærrer for de næste i køen. Med Wait-punkter:
|
||||
|
||||
1. Læg markører i en **mappe med ordet `Wait` i navnet** (fx mappen `Cow Wait`) — så mange du har
|
||||
plads til; de er en fælles pulje for alle traktorer. Markørernes egne navne er ligegyldige, så
|
||||
dine mark-ventepladser ("F91 Wait" i mark-mapperne) rører den ikke.
|
||||
2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt
|
||||
(modden holder styr på reservationer) og venter dér.
|
||||
3. Når målet igen har plads til **hele** resten — eller er mindst **halvt tomt** (kæmpe vogne kan aldrig
|
||||
tømme hele resten i en lille stald) — kører den tilbage og læsser af. Derefter normal tur.
|
||||
4. Wait-punkterne SKAL være forbundet til vejnettet både fra målets udkørsel og tilbage til målet.
|
||||
Modden tjekker ruten begge veje før den omdirigerer; uden rute prøves næste punkt (AutoDrive
|
||||
ville ellers vise "cannot reach" og slukke sig selv).
|
||||
|
||||
Kun den aktuelle aflæsningsopgaves destination ændres — mål, rotation og loop-tæller røres ikke.
|
||||
Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet.
|
||||
Ingen ledige Wait-punkter → AutoDrive venter ved målet som i dag (én advarsel i loggen).
|
||||
Seks markører må gerne hedde det samme (fx `Cow Wait`); loggen viser markørnummeret.
|
||||
|
||||
**Læs der passer, foretrækkes (v1.7):** ved valg af næste tur går varer, hvor et fuldt læs kan komme af
|
||||
i målet, forud; passer intet fuldt læs, tages den mest trængende alligevel (og Wait-punktet bruges).
|
||||
|
||||
**Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*.
|
||||
Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet,
|
||||
så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en
|
||||
fabrik ("kun aflæsning") — så kalder AutoDrive aldrig modden, og alt valgt læsses dér.
|
||||
|
||||
### Fælles tekniske noter (v1.2–v1.5)
|
||||
|
||||
- Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en.
|
||||
- Siloens eget lager rangeres før nabo-lån (udvidelses-siloer deler lager inden for `storageRadius`).
|
||||
- Intet fund, intet behov eller fejl i et spil-API → AutoDrives originale rotation. Valg logges som `ADSmartPickup:` i log.txt.
|
||||
- Kræver i AutoDrive: "Brug mapper" (global AutoDrive-indstilling, gælder hele spillet) +
|
||||
"Rotér mål: pålæsning" på køretøjet (pr. køretøj).
|
||||
- **Markør ↔ silo (v1.3):** markøren er dér hvor trækkerens næse stopper; vognen holder et vogntog
|
||||
bagved. Markørens silo er derfor nærmeste trigger **bag næsen** set i ankomstretningen (forrige
|
||||
waypoint), højst 2 m foran. I tætte rækker (15 m mellem siloerne) er nabosiloens trigger ellers
|
||||
nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før.
|
||||
- **Kildestrategi (v1.5):** lagervarer hentes fra den **mindst fyldte** silo først (rester ryddes,
|
||||
siloer frigives). Varer hvis tanke *løber fulde* — mælk, gylle, digestat — hentes fra den **fyldeste**
|
||||
kilde først, så produktionen ikke går i stå. Listen er `ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES`
|
||||
(`adSupplyPlanner.lua`). En trigger foran næsen tæller aldrig som markørens egen.
|
||||
- **Spor-regel (v1.4):** triggeren skal ligge i trækkerens spor (højst 6 m til siden). Kendes retningen,
|
||||
men ligger intet i sporet (kurve ind til markøren), accepteres kun en trigger inden for 12 m.
|
||||
- **Kun fremme ved markøren (v1.4):** rotationen skjules først når målet er nået — undervejs ruller
|
||||
vognen gennem nabosiloers triggere, og dér skal AutoDrive køre videre ved tom trigger.
|
||||
- **Sikkerhedsventil (v1.4):** 30 s ved markøren uden at vognen fyldes → grebet slippes (advarsel i
|
||||
log), og AutoDrive kører videre som med almindelig rotation.
|
||||
- **Står og læsser (v1.2):** når moden har valgt siloen, ser AutoDrive under
|
||||
`LoadAtDestinationTask.update` rotationen som slået fra for netop det køretøj, så den holder ved
|
||||
markøren og læsser som uden rotation. Løber kilden tør, slippes grebet og turen fortsætter med det der
|
||||
er i vognen.
|
||||
@@ -10,7 +10,8 @@ ADBuildings.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADBuildings.MAX_MARKER_DISTANCE = 40
|
||||
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
|
||||
ADBuildings.MAX_DROP_DISTANCE = 25
|
||||
ADBuildings.CACHE_MS = 1000
|
||||
-- bygninger ændrer sig sjældent; genopbygningen (bygninger x markører) er dyr — 1 s gav et hak hvert sekund
|
||||
ADBuildings.CACHE_MS = 30000
|
||||
|
||||
local cache = {list = nil, farmId = nil, time = -math.huge}
|
||||
|
||||
@@ -30,6 +31,8 @@ local function getNodePosition(node)
|
||||
end
|
||||
|
||||
local function getName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then return tostring(name) end
|
||||
@@ -118,7 +121,10 @@ local function collectNodes(placeable, loadStations, unloadStations)
|
||||
for _, place in ipairs(spawner.spawnPlaces or {}) do
|
||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||
end
|
||||
if #nodes > 0 then table.insert(loadGroups, nodes) end
|
||||
if #nodes > 0 then
|
||||
nodes.isSpawner = true
|
||||
table.insert(loadGroups, nodes)
|
||||
end
|
||||
end
|
||||
-- stald uden aflæsningsnoder i stationen (hønsehus: kun foder-triggeren) -> søg i staldens foderdata
|
||||
if #unloadNodes == 0 and placeable.getHusbandryFillLevel ~= nil then
|
||||
@@ -139,7 +145,10 @@ local function collectNodes(placeable, loadStations, unloadStations)
|
||||
for _, place in ipairs(palletSpawner.spawnPlaces or {}) do
|
||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||
end
|
||||
if #nodes > 0 then table.insert(loadGroups, nodes) end
|
||||
if #nodes > 0 then
|
||||
nodes.isSpawner = true
|
||||
table.insert(loadGroups, nodes)
|
||||
end
|
||||
end
|
||||
end
|
||||
if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then
|
||||
@@ -218,6 +227,82 @@ end
|
||||
-- Aflæsningsmarkør: nærmeste ikke-Wait-markør ≤ MAX_MARKER_DISTANCE til aflæsningsnoderne — men ikke
|
||||
-- bygningens egne LÆSSEmarkører, når der findes en anden (Sugarbeet Crusher: pickup står tættere på
|
||||
-- aflæsningstriggeren end drop). Kun én fælles markør (AIO Load / Unload) bruges til begge.
|
||||
-- Alle ikke-Wait-markører ≤ MAX_MARKER_DISTANCE til nodes, nærmeste først: {{id, name, distance}}.
|
||||
local function markerCandidates(adEnv, nodes)
|
||||
local positions, found = {}, {}
|
||||
for _, node in ipairs(nodes) do
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||
if wayPoint ~= nil then
|
||||
local nearest = nil
|
||||
for _, position in ipairs(positions) do
|
||||
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||
if nearest == nil or distance < nearest then nearest = distance end
|
||||
end
|
||||
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then
|
||||
table.insert(found, {id = marker.id, name = tostring(marker.name), distance = nearest})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||
return found
|
||||
end
|
||||
|
||||
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
||||
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
||||
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
||||
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
||||
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
||||
if not isUnloads then
|
||||
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Diagnose (én gang pr. stald pr. session): hvilke markører ligger nærmest læsse- og aflæsningspunkterne.
|
||||
-- (in-game 27/9: Goat Food fik "Sheep Milk"-markørerne som mål og Goat Milk Pickup "Sheep Food" som kilde)
|
||||
local diagnosed = {}
|
||||
local function closestMarkers(adEnv, nodes, count)
|
||||
local positions, found = {}, {}
|
||||
for _, node in ipairs(nodes) do
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||
if wayPoint ~= nil then
|
||||
local nearest = nil
|
||||
for _, position in ipairs(positions) do
|
||||
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||
if nearest == nil or distance < nearest then nearest = distance end
|
||||
end
|
||||
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then table.insert(found, {name = tostring(marker.name), distance = nearest}) end
|
||||
end
|
||||
end
|
||||
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||
local parts = {}
|
||||
for index = 1, math.min(count, #found) do table.insert(parts, string.format("%s %.0f m", found[index].name, found[index].distance)) end
|
||||
return #parts > 0 and table.concat(parts, ", ") or "ingen markør"
|
||||
end
|
||||
|
||||
local function diagnoseHusbandry(adEnv, id, name, loadGroups, unloadNodes, loadMarkers, unloadMarker)
|
||||
if diagnosed[id] then return end
|
||||
diagnosed[id] = true
|
||||
local groups = {}
|
||||
for index, nodes in ipairs(loadGroups) do table.insert(groups, string.format("læs%d: %s", index, closestMarkers(adEnv, nodes, 2))) end
|
||||
local loads = {}
|
||||
for _, marker in ipairs(loadMarkers) do table.insert(loads, tostring(marker.name)) end
|
||||
Logging.info("ADSmartPickup: stald '%s': %s | aflæsning nærmest: %s | valgt læs=[%s] aflæs=%s", tostring(name),
|
||||
#groups > 0 and table.concat(groups, "; ") or "ingen læssegrupper", closestMarkers(adEnv, unloadNodes, 3),
|
||||
table.concat(loads, ", "), unloadMarker ~= nil and tostring(unloadMarker.name) or "-")
|
||||
end
|
||||
|
||||
local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
if #nodes == 0 then
|
||||
return nil
|
||||
@@ -332,8 +417,9 @@ local function build(adEnv, farmId)
|
||||
local marker = siloLoadMarker(adEnv, farmId, placeable, loadStations)
|
||||
if marker ~= nil then table.insert(loadMarkers, marker) end
|
||||
else
|
||||
local unloadBest = markerCandidates(adEnv, unloadNodes)[1]
|
||||
for _, nodes in ipairs(loadGroups) do
|
||||
local marker = nearestMarker(adEnv, nodes)
|
||||
local marker = ADBuildings.pickLoadMarker(markerCandidates(adEnv, nodes), unloadBest, nodes.isSpawner == true)
|
||||
if marker ~= nil and not seen[marker.id] then
|
||||
seen[marker.id] = true
|
||||
table.insert(loadMarkers, marker)
|
||||
@@ -343,6 +429,7 @@ local function build(adEnv, farmId)
|
||||
end
|
||||
-- siloer og palleskure læsser og aflæsser samme sted: altid nærmeste markør
|
||||
local unloadMarker = unloadMarkerFor(adEnv, unloadNodes, loadMarkers, kind ~= "silo" and kind ~= "objectStorage")
|
||||
if kind == "husbandry" then pcall(diagnoseHusbandry, adEnv, id, getName(placeable, id), loadGroups, unloadNodes, loadMarkers, unloadMarker) end
|
||||
-- silo der kun kan FYLDES (fx drive-on-bunker der fodrer en biogas-fermentor): aflæsning,
|
||||
-- ingen læssestation -> et forsyningsmål, aldrig en kilde
|
||||
local isFillOnlySilo = kind == "silo" and loadMarkers[1] == nil and #loadGroups == 0 and unloadMarker ~= nil
|
||||
@@ -363,6 +450,7 @@ local function build(adEnv, farmId)
|
||||
end
|
||||
|
||||
function ADBuildings.invalidate()
|
||||
if ADCache ~= nil then ADCache.clear() end
|
||||
cache.list, cache.time = nil, -math.huge
|
||||
end
|
||||
|
||||
@@ -372,6 +460,11 @@ function ADBuildings.list(adEnv, farmId)
|
||||
return cache.list
|
||||
end
|
||||
cache.list, cache.farmId, cache.time = build(adEnv, farmId), farmId, time
|
||||
-- nye bygninger uden eget navn navngives (tilføjes navnefilen; eksisterende navne røres ikke)
|
||||
if ADNames ~= nil and ADNames.autoName ~= nil then
|
||||
local ok, err = pcall(ADNames.autoName, adEnv, cache.list)
|
||||
if not ok then Logging.warning("ADSmartPickup: automatiske bygningsnavne fejlede: %s", tostring(err)) end
|
||||
end
|
||||
return cache.list
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- AD Cache
|
||||
-- Fælles korttids-cache for dyre opslag (markør -> station, salgssteder, identify, Wait-punkter, lagre).
|
||||
-- Holdende vogne genplanlægger hvert 5. s og slog de samme ting op igen og igen (markører x stationer x
|
||||
-- triggere) -> hak (in-game 26/9). Verden ændrer sig ikke så hurtigt. Uden spillets tid (tests) caches
|
||||
-- intet, så adfærden er som før. "Intet fundet" gemmes også.
|
||||
|
||||
ADCache = {}
|
||||
|
||||
ADCache.buckets = {}
|
||||
-- tændes af ADSmartPickup når hooksene er installeret (i spillet); tests kører uden cache
|
||||
ADCache.enabled = false
|
||||
local NOTHING = {}
|
||||
|
||||
-- Til moduler der indlæses uden adCache (tests): kald fn direkte.
|
||||
function ADCacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCache == nil or not ADCache.enabled then return fn() end
|
||||
return ADCache.get(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
local function now()
|
||||
local mission = g_currentMission
|
||||
if mission ~= nil and type(mission.time) == "number" then return mission.time end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Resultatet af fn() for (bucket, key), genbrugt i ttlMs.
|
||||
function ADCache.get(bucket, key, ttlMs, fn)
|
||||
local time = now()
|
||||
if not ADCache.enabled or time == nil or key == nil then return fn() end
|
||||
local store = ADCache.buckets[bucket]
|
||||
if store == nil then
|
||||
store = {size = 0, entries = {}}
|
||||
ADCache.buckets[bucket] = store
|
||||
end
|
||||
local entry = store.entries[key]
|
||||
if entry ~= nil and time - entry.at < ttlMs then
|
||||
if entry.value == NOTHING then return nil end
|
||||
return entry.value
|
||||
end
|
||||
local value = fn()
|
||||
if entry == nil then
|
||||
store.size = store.size + 1
|
||||
if store.size > 5000 then store.entries, store.size = {}, 1 end
|
||||
end
|
||||
-- spredt udløb (levetid +0-50 %): poster der blev lavet i samme kald, udløber ikke i samme frame
|
||||
-- (simulering 26/9: 600 markør-opslag udløb samtidig -> hak hvert 10. s)
|
||||
store.entries[key] = {value = value == nil and NOTHING or value, at = time + math.random() * ttlMs * 0.5}
|
||||
return value
|
||||
end
|
||||
|
||||
function ADCache.clear(bucket)
|
||||
if bucket == nil then ADCache.buckets = {} else ADCache.buckets[bucket] = nil end
|
||||
end
|
||||
@@ -0,0 +1,238 @@
|
||||
-- AD Course Cache
|
||||
-- Genbrug af Courseplay-kurser. At lade CP beregne et kursus tager sekunder på store marker (Dakota 16x, 70+ ha)
|
||||
-- og giver hak — men CP gemmer selv hver maskines seneste kursus i savegamet. Registret husker hvad kurset blev
|
||||
-- lavet TIL: mark, bredde, markens form (ADFields.shapeSignature) og indstillingerne der former kurset
|
||||
-- (værktøjsforskydning, multi-tool-sæt/bane, vin-offset/-skip). Passer alt, startes CP på maskinens eget kursus;
|
||||
-- ellers kopieres en anden maskines matchende kursus; ellers beregnes et nyt, som så registreres.
|
||||
-- Rene regler øverst (testet i tests/test_adCourseCache.lua); spil-nær lim nederst (alt pcall'et).
|
||||
|
||||
ADCourseCache = {}
|
||||
|
||||
-- ---- rene regler ----
|
||||
|
||||
local function round1(value)
|
||||
local rounded = math.floor((tonumber(value) or 0) * 10 + 0.5) / 10
|
||||
if rounded == 0 then rounded = 0 end -- ingen "-0.0"
|
||||
return string.format("%.1f", rounded)
|
||||
end
|
||||
|
||||
local function optional(value, format)
|
||||
if value == nil then return "-" end
|
||||
return format(value)
|
||||
end
|
||||
|
||||
-- Indstillingerne der former kurset, som én streng (afrundet, så float-støj ikke tæller).
|
||||
function ADCourseCache.settingsKey(s)
|
||||
s = s or {}
|
||||
return string.format("off=%s;lane=%s;multi=%d;vine=%s;skip=%s",
|
||||
round1(s.toolOffset), optional(s.lane, tostring), tonumber(s.multiTools) or 1,
|
||||
optional(s.vineOffset, round1), optional(s.vineSkip, tostring))
|
||||
end
|
||||
|
||||
-- Ny post (kopi af felterne; nil uden køretøj).
|
||||
function ADCourseCache.newRecord(values)
|
||||
if values == nil or values.vehicle == nil then return nil end
|
||||
return {vehicle = tostring(values.vehicle), field = tonumber(values.field), width = tonumber(values.width),
|
||||
sig = values.sig, settings = values.settings, waypoints = tonumber(values.waypoints)}
|
||||
end
|
||||
|
||||
-- Hvad kurset skal passe til: mark|bredde|form|indstillinger. nil hvis noget er ukendt (så genbruges intet).
|
||||
function ADCourseCache.key(entry)
|
||||
if entry == nil or entry.field == nil or entry.width == nil or entry.sig == nil or entry.settings == nil then return nil end
|
||||
return string.format("%s|%s|%s|%s", tostring(entry.field), round1(entry.width), entry.sig, entry.settings)
|
||||
end
|
||||
|
||||
function ADCourseCache.matches(record, request)
|
||||
local key = ADCourseCache.key(record)
|
||||
return key ~= nil and key == ADCourseCache.key(request)
|
||||
end
|
||||
|
||||
-- Hvorfor maskinens gemte kursus ikke kan bruges (til loggen).
|
||||
function ADCourseCache.mismatchReason(record, request)
|
||||
if request == nil or request.sig == nil then return "markens form ukendt" end
|
||||
if record == nil then return "intet kursus gemt" end
|
||||
if record.field ~= request.field then return "kursus til en anden mark" end
|
||||
if record.sig ~= request.sig then return "markens form ændret" end
|
||||
if record.width == nil or round1(record.width) ~= round1(request.width) then return "ny bredde" end
|
||||
if record.settings ~= request.settings then return "andre indstillinger" end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function copyMap(records)
|
||||
local result = {}
|
||||
for id, record in pairs(records or {}) do result[id] = record end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADCourseCache.withRecord(records, record)
|
||||
local result = copyMap(records)
|
||||
if record ~= nil then result[record.vehicle] = record end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADCourseCache.without(records, vehicleId)
|
||||
local result = copyMap(records)
|
||||
if vehicleId ~= nil then result[tostring(vehicleId)] = nil end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Kursus der kan genbruges: egen maskines først ("own"), ellers en anden maskines ("copy", laveste id først).
|
||||
-- isValid(record) (valgfri) tjekker at kurset stadig sidder på maskinen. Intet: nil + grunden.
|
||||
function ADCourseCache.findReusable(records, request, vehicleId, isValid)
|
||||
records = records or {}
|
||||
local ownId = vehicleId ~= nil and tostring(vehicleId) or nil
|
||||
local own = ownId ~= nil and records[ownId] or nil
|
||||
local anyStale = false
|
||||
local function usable(record)
|
||||
if not ADCourseCache.matches(record, request) then return false end
|
||||
if isValid ~= nil and not isValid(record) then
|
||||
anyStale = true
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
if own ~= nil and usable(own) then return own, "own" end
|
||||
local ids = {}
|
||||
for id in pairs(records) do
|
||||
if id ~= ownId then table.insert(ids, id) end
|
||||
end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
if usable(records[id]) then return records[id], "copy" end
|
||||
end
|
||||
if anyStale then return nil, "kurset på maskinen er ændret" end
|
||||
return nil, ADCourseCache.mismatchReason(own, request)
|
||||
end
|
||||
|
||||
-- Marken har fået en anden form: dens poster (med anden signatur) slettes. Returnerer nyt register + antal.
|
||||
-- Ukendt ny form (nil) sletter intet; intet slettet = samme tabel tilbage.
|
||||
function ADCourseCache.invalidateField(records, fieldId, newSig)
|
||||
if records == nil or newSig == nil then return records or {}, 0 end
|
||||
local result, removed = {}, 0
|
||||
for id, record in pairs(records) do
|
||||
if record.field == fieldId and record.sig ~= newSig then
|
||||
removed = removed + 1
|
||||
else
|
||||
result[id] = record
|
||||
end
|
||||
end
|
||||
if removed == 0 then return records, 0 end
|
||||
return result, removed
|
||||
end
|
||||
|
||||
-- ---- spil-nær lim ----
|
||||
|
||||
local function log(...) ADFieldJobs.log(...) end
|
||||
|
||||
local function getRecords() return ADFieldJobs.state.courseRecords or {} end
|
||||
local function setRecords(records)
|
||||
ADFieldJobs.state.courseRecords = records
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
local function waypointCount(course)
|
||||
if course == nil or course.getNumberOfWaypoints == nil then return nil end
|
||||
local ok, n = pcall(course.getNumberOfWaypoints, course)
|
||||
return ok and tonumber(n) or nil
|
||||
end
|
||||
|
||||
-- Kurset sidder stadig på maskinen og har samme antal punkter som da det blev registreret (spilleren kan
|
||||
-- have lavet et nyt kursus i CP's HUD).
|
||||
local function courseStillThere(vehicle, record)
|
||||
local course = ADCpBridge.getCourse(vehicle)
|
||||
if course == nil then return nil end
|
||||
local n = waypointCount(course)
|
||||
if record.waypoints ~= nil and n ~= record.waypoints then return nil end
|
||||
return course
|
||||
end
|
||||
|
||||
local function vehicleById(adEnv, id)
|
||||
local ok, found = pcall(function()
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
if ADRunsController.getVehicleId(vehicle) == id then return vehicle end
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
return ok and found or nil
|
||||
end
|
||||
|
||||
-- Hvad kurset til denne start skal passe til.
|
||||
function ADCourseCache.requestFor(vehicle, fieldId, info, role, opts)
|
||||
local vineOffset = ADCpBridge.vineOffsetFor(info.fruitName, role)
|
||||
return {
|
||||
field = fieldId, width = ADCpBridge.getWorkWidth(vehicle), sig = info.shapeSig,
|
||||
settings = ADCourseCache.settingsKey({
|
||||
toolOffset = ADCpBridge.getToolOffset(vehicle), lane = opts.lane, multiTools = opts.multiTools or 1,
|
||||
vineOffset = vineOffset, vineSkip = vineOffset ~= nil and ADCpBridge.vineSkipFor(info.fruitName, role) or nil,
|
||||
}),
|
||||
}
|
||||
end
|
||||
|
||||
-- Før ADCpBridge.start: genbrug/kopi/ny beregning. Returnerer (nye) opts + kontekst til afterStart.
|
||||
-- Har opts allerede et kursus (slåmaskinens/rivens, lederens multi-tool-kursus), bruges registret ikke.
|
||||
local function prepareUnsafe(adEnv, field, info, assignment, opts)
|
||||
if opts.course ~= nil then return opts, nil end
|
||||
local vehicle = assignment.vehicle
|
||||
local ok, request = pcall(ADCourseCache.requestFor, vehicle, field.id, info, assignment.role, opts)
|
||||
if not ok then return opts, nil end
|
||||
local vehicles = {}
|
||||
local function isValid(record)
|
||||
local owner = record.vehicle == assignment.vehicleId and vehicle or vehicleById(adEnv, record.vehicle)
|
||||
local course = owner ~= nil and courseStillThere(owner, record) or nil
|
||||
vehicles[record.vehicle] = course ~= nil and {vehicle = owner, course = course} or nil
|
||||
return course ~= nil
|
||||
end
|
||||
local record, how = ADCourseCache.findReusable(getRecords(), request, assignment.vehicleId, isValid)
|
||||
local result = {}
|
||||
for k, v in pairs(opts) do result[k] = v end
|
||||
if how == "own" then
|
||||
result.reuse = true
|
||||
log(field.id, "kursus genbrugt ('%s', %.1f m) — ingen ny beregning", ADRuns.vehicleLabel(vehicle), request.width or 0)
|
||||
elseif how == "copy" then
|
||||
local source = vehicles[record.vehicle]
|
||||
result.course = source.course
|
||||
log(field.id, "kursus kopieret fra '%s' til '%s' — ingen ny beregning", ADRuns.vehicleLabel(source.vehicle), ADRuns.vehicleLabel(vehicle))
|
||||
else
|
||||
log(field.id, "nyt kursus beregnes til '%s' (%s)", ADRuns.vehicleLabel(vehicle), tostring(how))
|
||||
end
|
||||
return result, {request = request, how = how}
|
||||
end
|
||||
|
||||
function ADCourseCache.prepare(adEnv, field, info, assignment, opts)
|
||||
local ok, result, context = pcall(prepareUnsafe, adEnv, field, info, assignment, opts)
|
||||
if ok then return result, context end
|
||||
Logging.warning("ADSmartPickup: kursus-register (Courseplay laver nyt kursus): %s", tostring(result))
|
||||
return opts, nil
|
||||
end
|
||||
|
||||
-- Efter CP er startet: maskinens nuværende kursus registreres (nyt, genbrugt eller kopieret fra registret).
|
||||
-- Uden kontekst (kursus kopieret fra slåmaskinen/lederen) passer det ikke til maskinens egen anmodning: glem.
|
||||
function ADCourseCache.afterStart(vehicle, vehicleId, context)
|
||||
if vehicleId == nil then return end
|
||||
local ok, err = pcall(function()
|
||||
local records = getRecords()
|
||||
local course = context ~= nil and ADCpBridge.getCourse(vehicle) or nil
|
||||
if course == nil or ADCourseCache.key(context.request) == nil then
|
||||
if records[tostring(vehicleId)] ~= nil then setRecords(ADCourseCache.without(records, vehicleId)) end
|
||||
return
|
||||
end
|
||||
local r = context.request
|
||||
local record = ADCourseCache.newRecord({vehicle = vehicleId, field = r.field, width = r.width, sig = r.sig,
|
||||
settings = r.settings, waypoints = waypointCount(course)})
|
||||
local existing = records[record.vehicle]
|
||||
if existing ~= nil and ADCourseCache.matches(existing, r) and existing.waypoints == record.waypoints then return end
|
||||
setRecords(ADCourseCache.withRecord(records, record))
|
||||
end)
|
||||
if not ok then Logging.warning("ADSmartPickup: kursus-register: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Markens form er (måske) ændret: poster med anden signatur glemmes (scanning og markguidens Gem).
|
||||
function ADCourseCache.checkField(fieldId, sig)
|
||||
local records = getRecords()
|
||||
local kept, removed = ADCourseCache.invalidateField(records, fieldId, sig)
|
||||
if removed > 0 then
|
||||
setRecords(kept)
|
||||
log(fieldId, "markens form er ændret — %d gemt(e) kursus(er) glemt, næste start beregner nyt", removed)
|
||||
end
|
||||
return removed
|
||||
end
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
ADCpBridge = {}
|
||||
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine",
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine", "spec_baler",
|
||||
"spec_sowingMachine", "spec_cultivator", "spec_plow", "spec_roller", "spec_weeder", "spec_mulcher"}
|
||||
|
||||
-- Sprøjtens/sprederens fyld -> rolle (tom sprøjte har ingen rolle; den kan ikke sendes ud).
|
||||
@@ -62,16 +62,92 @@ local function collect(vehicle, list)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Sprøjtens/sprederens rolle ud fra det fyld den har på (tom -> nil).
|
||||
-- Hvilke varer der hører til en rolle (påfyldning af en tom spreder/sprøjte), i foretrukken rækkefølge.
|
||||
ADCpBridge.ROLE_FILLTYPES = {
|
||||
fertilizer = {"FERTILIZER", "LIQUIDFERTILIZER", "MANURE", "LIQUIDMANURE", "DIGESTATE"},
|
||||
limer = {"LIME"},
|
||||
weeder = {"HERBICIDE"},
|
||||
}
|
||||
ADCpBridge.ROLE_ORDER = {"fertilizer", "limer", "weeder"}
|
||||
|
||||
-- Sprøjtens/sprederens roller: alle roller dens fyldenhed kan tage (Bredal-spreder: gødning OG kalk).
|
||||
-- Primær rolle = fyldets rolle (har den kalk på, er den kalkspreder), ellers den første den kan tage.
|
||||
-- Returnerer primær, sæt {[rolle] = true}, fyldets rolle (nil = tom); nil hvis ingen roller.
|
||||
function ADCpBridge.sprayRolesFor(filledName, supportedNames)
|
||||
local roles, first = {}, nil
|
||||
for _, role in ipairs(ADCpBridge.ROLE_ORDER) do
|
||||
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role]) do
|
||||
if supportedNames[name] then
|
||||
roles[role] = true
|
||||
first = first or role
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local filledRole = filledName ~= nil and ADCpBridge.sprayRoleForFillType(filledName) or nil
|
||||
if filledRole ~= nil then roles[filledRole] = true end
|
||||
local primary = filledRole or first
|
||||
return primary, primary ~= nil and roles or nil, filledRole
|
||||
end
|
||||
|
||||
local function sprayerNames(object)
|
||||
local unitIndex = object:getSprayerFillUnitIndex()
|
||||
local filledName = nil
|
||||
local level = object:getFillUnitFillLevel(unitIndex) or 0
|
||||
local capacity = object.getFillUnitCapacity ~= nil and (object:getFillUnitCapacity(unitIndex) or 0) or 0
|
||||
if level > 0 then
|
||||
filledName = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unitIndex))
|
||||
end
|
||||
local supported = {}
|
||||
local unit = object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits ~= nil and object.spec_fillUnit.fillUnits[unitIndex] or nil
|
||||
for fillType, isSupported in pairs(unit ~= nil and unit.supportedFillTypes or {}) do
|
||||
if isSupported then
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||
if name ~= nil then supported[name] = true end
|
||||
end
|
||||
end
|
||||
return filledName, supported, level, capacity
|
||||
end
|
||||
|
||||
local function sprayRole(object)
|
||||
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
|
||||
local ok, role = pcall(function()
|
||||
local unit = object:getSprayerFillUnitIndex()
|
||||
if (object:getFillUnitFillLevel(unit) or 0) <= 0 then return nil end
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unit))
|
||||
return ADCpBridge.sprayRoleForFillType(name)
|
||||
local ok, role, roles, filledRole, wrongFillDumpable = pcall(function()
|
||||
local filledName, supported, level, capacity = sprayerNames(object)
|
||||
local r, rs, fr = ADCpBridge.sprayRolesFor(filledName, supported)
|
||||
return r, rs, fr, ADCpBridge.isDumpable(level, capacity)
|
||||
end)
|
||||
return ok and role or nil
|
||||
if not ok then return nil end
|
||||
return role, roles, filledRole, wrongFillDumpable
|
||||
end
|
||||
|
||||
-- Må en forkert vare hældes ud? Kun en lille rest: højst 10 % af tanken og aldrig over 2.000 l
|
||||
-- (in-game 26/9: 100.000 l flydende gødning og 4.000 l sprøjtemiddel blev hældt ud for at skifte opgave).
|
||||
ADCpBridge.MAX_DUMP_LITERS = 2000
|
||||
ADCpBridge.MAX_DUMP_SHARE = 0.10
|
||||
function ADCpBridge.isDumpable(level, capacity)
|
||||
level = level or 0
|
||||
if level <= 0 then return true end
|
||||
if level > ADCpBridge.MAX_DUMP_LITERS then return false end
|
||||
return capacity == nil or capacity <= 0 or level <= capacity * ADCpBridge.MAX_DUMP_SHARE
|
||||
end
|
||||
|
||||
-- Tøm sprøjten/sprederen for en vare der ikke hører til opgaven (kalk på, skal gøde). FS25 kan ikke
|
||||
-- tippe gødning/kalk af, så resten kasseres. Returnerer liter og varenavn (0 hvis intet blev tømt).
|
||||
function ADCpBridge.emptySprayerIfWrong(vehicle, role)
|
||||
local liters, name = 0, nil
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
pcall(function()
|
||||
local filledName, _, level, capacity = sprayerNames(object)
|
||||
if filledName == nil or ADCpBridge.sprayRoleForFillType(filledName) == role then return end
|
||||
if not ADCpBridge.isDumpable(level, capacity) then return end
|
||||
local unit = object:getSprayerFillUnitIndex()
|
||||
object:addFillUnitFillLevel(object:getOwnerFarmId(), unit, -level, object:getFillUnitFillType(unit), ToolType.UNDEFINED, nil)
|
||||
liters, name = liters + level, filledName
|
||||
end)
|
||||
end
|
||||
end
|
||||
return liters, name
|
||||
end
|
||||
|
||||
function ADCpBridge.getSpecs(vehicle)
|
||||
@@ -80,21 +156,56 @@ function ADCpBridge.getSpecs(vehicle)
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if object[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
specs.sprayRole = specs.sprayRole or sprayRole(object)
|
||||
if specs.sprayRole == nil then specs.sprayRole, specs.sprayRoles, specs.filledRole, specs.dumpable = sprayRole(object) end
|
||||
end
|
||||
if vehicle.getCanStartCpCombineUnloader ~= nil then
|
||||
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
|
||||
specs.canUnload = ok and can == true or nil
|
||||
end
|
||||
if vehicle.getCanStartCpBaleFinder ~= nil then
|
||||
local ok, can = pcall(vehicle.getCanStartCpBaleFinder, vehicle)
|
||||
specs.canBaleCollect = ok and can == true or nil
|
||||
end
|
||||
return specs
|
||||
end
|
||||
|
||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
|
||||
-- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter
|
||||
-- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere).
|
||||
function ADCpBridge.vineFruitSet(spec)
|
||||
local fruits = {}
|
||||
if spec == nil then return fruits end
|
||||
for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do
|
||||
if type(spec[key]) == "number" then fruits[spec[key]] = true end
|
||||
end
|
||||
if type(spec.fruitTypes) == "table" then
|
||||
for key, value in pairs(spec.fruitTypes) do
|
||||
if type(value) == "number" then fruits[value] = true
|
||||
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
|
||||
local vineLogged = setmetatable({}, {__mode = "k"})
|
||||
|
||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere i kæden). nil = ikke en høster.
|
||||
function ADCpBridge.getHarvestFruits(vehicle)
|
||||
if vehicle.spec_combine == nil then return nil end
|
||||
local fruits = {}
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
||||
if object.spec_vineCutter ~= nil then
|
||||
local vine = ADCpBridge.vineFruitSet(object.spec_vineCutter)
|
||||
for fruit in pairs(vine) do fruits[fruit] = true end
|
||||
if next(vine) == nil and not vineLogged[object] then
|
||||
-- ukendt feltnavn i denne spilversion: vis nøglerne, så reglen kan rettes
|
||||
vineLogged[object] = true
|
||||
local keys = {}
|
||||
for key, value in pairs(object.spec_vineCutter) do table.insert(keys, tostring(key) .. "=" .. type(value)) end
|
||||
table.sort(keys)
|
||||
Logging.info("ADSmartPickup: vinhøster uden afgrøde fundet — spec_vineCutter: %s", table.concat(keys, ", "))
|
||||
end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
@@ -160,8 +271,9 @@ function ADCpBridge.isPlantableNow(fruitIndex)
|
||||
end
|
||||
|
||||
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||
-- nuværende fyld (gødning, kalk, sprøjtemiddel). nil = intet at fylde på (strigle, plov ...).
|
||||
function ADCpBridge.getRefillFillType(vehicle)
|
||||
-- fyld; en TOM spreder får rollens vare (gødning ved gødskning, kalk ved kalkning), hvis dens fyldenhed
|
||||
-- kan tage den. nil = intet at fylde på (strigle, plov ...).
|
||||
function ADCpBridge.getRefillFillType(vehicle, role)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
||||
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||
@@ -169,7 +281,16 @@ function ADCpBridge.getRefillFillType(vehicle)
|
||||
end
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
local ok, fillType = pcall(function() return object:getFillUnitFillType(object:getSprayerFillUnitIndex()) end)
|
||||
local ok, fillType = pcall(function()
|
||||
local filledName, supported = sprayerNames(object)
|
||||
if filledName ~= nil and (role == nil or ADCpBridge.sprayRoleForFillType(filledName) == role) then
|
||||
return g_fillTypeManager:getFillTypeIndexByName(filledName)
|
||||
end
|
||||
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role] or {}) do
|
||||
if supported[name] then return g_fillTypeManager:getFillTypeIndexByName(name) end
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
||||
end
|
||||
end
|
||||
@@ -189,6 +310,58 @@ function ADCpBridge.getRefillLevel(vehicle, fillType)
|
||||
return total
|
||||
end
|
||||
|
||||
-- Halmskår bag mejetærskeren (Courseplays køretøjsindstilling strawSwath: 1 = fra/hakkes, 2 = til).
|
||||
function ADCpBridge.setStrawSwath(vehicle, on)
|
||||
return (pcall(function() vehicle:getCpSettings().strawSwath:setValue(on and 2 or 1) end))
|
||||
end
|
||||
|
||||
-- Ballesamler: Courseplays BALE_FINDER_CP (ballevogn eller Universal Autoload) samler markens baller.
|
||||
function ADCpBridge.startBaleFinder(vehicle, fieldX, fieldZ, onDone)
|
||||
if AIJobType == nil or AIJobType.BALE_FINDER_CP == nil then
|
||||
return nil, "Courseplay (BALE_FINDER_CP) findes ikke"
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.BALE_FINDER_CP)
|
||||
if job == nil then return nil, "kunne ikke oprette ballesamler-job" end
|
||||
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
|
||||
local params = job.cpJobParameters
|
||||
if params ~= nil and params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
||||
if params ~= nil and params.startPosition ~= nil then
|
||||
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
|
||||
pcall(function() params.startPosition:setPosition(vx, vz) end)
|
||||
end
|
||||
pcall(function() job:setValues() end)
|
||||
local handle = {cancelled = false, fired = false, finished = false}
|
||||
local function finish(success, reason)
|
||||
if handle.finished then return end
|
||||
handle.finished = true
|
||||
if not handle.cancelled then onDone(success, reason) end
|
||||
end
|
||||
local function startJob()
|
||||
if handle.cancelled then return end
|
||||
local valid, err = job:validate(farmId)
|
||||
if not valid then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
if handle.fired then return end
|
||||
handle.fired = true
|
||||
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
|
||||
startJob()
|
||||
end
|
||||
if job.registerFieldBoundaryDetectionCallback ~= nil then
|
||||
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
|
||||
end
|
||||
local isValid, isRunning, message = job:detectFieldBoundary()
|
||||
if isValid and not isRunning then
|
||||
startJob()
|
||||
elseif not isRunning then
|
||||
return nil, "Courseplay: " .. tostring(message)
|
||||
end
|
||||
return handle
|
||||
end
|
||||
|
||||
-- Stop køretøjets AI-job (Courseplay/hjælper).
|
||||
function ADCpBridge.stop(vehicle)
|
||||
pcall(function()
|
||||
@@ -218,9 +391,21 @@ local function courseplayWidth(vehicle)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Vinhøster uden AI-markører: bredden er vinrækken (3 m = Courseplays vin-gitter for druer).
|
||||
ADCpBridge.VINE_ROW_WIDTH = 3
|
||||
function ADCpBridge.widthOrVine(width, hasVineCutter)
|
||||
-- under markarbejdets minimum (1,5 m) — også en lille bredde fra Courseplay (in-game 29/9: stadig widthUnknown)
|
||||
if hasVineCutter and (width or 0) < 1.5 then return ADCpBridge.VINE_ROW_WIDTH end
|
||||
return width
|
||||
end
|
||||
|
||||
function ADCpBridge.getWorkWidth(vehicle)
|
||||
local hasVine = false
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_vineCutter ~= nil then hasVine = true end
|
||||
end
|
||||
local cpWidth = courseplayWidth(vehicle)
|
||||
if cpWidth ~= nil then return cpWidth end
|
||||
if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) end
|
||||
local best = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
local variable = variableWidth(object)
|
||||
@@ -232,11 +417,15 @@ function ADCpBridge.getWorkWidth(vehicle)
|
||||
if ok and type(width) == "number" and width > best then best = width end
|
||||
end
|
||||
end
|
||||
return best
|
||||
return ADCpBridge.widthOrVine(best, hasVine)
|
||||
end
|
||||
|
||||
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
|
||||
-- med AD aktiv blev meldt 'Courseplay kører' og ventede på sig selv).
|
||||
function ADCpBridge.isWorking(vehicle)
|
||||
if vehicle.getIsCpFieldWorkActive ~= nil and vehicle:getIsCpFieldWorkActive() then return true end
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if module ~= nil and module.isActive ~= nil and module:isActive() then return false end
|
||||
return vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() or false
|
||||
end
|
||||
|
||||
@@ -309,6 +498,27 @@ function ADCpBridge.getCourse(vehicle)
|
||||
return ok and course or nil
|
||||
end
|
||||
|
||||
-- Forager: nye kurser får altid HEADLANDS runder og starter på forageren, så der ikke køres ind i træer, hegn
|
||||
-- o.l. i hjørnerne (brugerkrav 29/9). Kan kurset ikke laves med forager (smal mark), prøves én gang uden.
|
||||
ADCpBridge.HEADLANDS = 2
|
||||
|
||||
function ADCpBridge.setHeadlands(vehicle, n)
|
||||
if vehicle.getCourseGeneratorSettings == nil then return false end
|
||||
return (pcall(function()
|
||||
local settings = vehicle:getCourseGeneratorSettings()
|
||||
settings.numberOfHeadlands:setValue(n)
|
||||
if settings.startOnHeadland ~= nil then settings.startOnHeadland:setValue(n > 0) end
|
||||
end))
|
||||
end
|
||||
|
||||
-- Har kurset forager? (ukendt API = antag ja, så intet genberegnes unødigt)
|
||||
function ADCpBridge.courseHasHeadlands(course)
|
||||
if course == nil then return false end
|
||||
if course.getNumberOfHeadlands == nil then return true end
|
||||
local ok, n = pcall(course.getNumberOfHeadlands, course)
|
||||
return not ok or type(n) ~= "number" or n > 0
|
||||
end
|
||||
|
||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||
local function setMultiTools(vehicle, n)
|
||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||
@@ -328,7 +538,36 @@ end
|
||||
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
|
||||
-- multiTools = antal sæt når kurset laves her (multi-tool-leder)
|
||||
-- lane = køretøjets bane på et multi-tool-kursus (laneOffset -2..2)
|
||||
-- onDone(ok, reason) kaldes én gang; reason "copied" ved succes med kopieret kursus.
|
||||
-- reuse = start på køretøjets eget nuværende kursus uden ny beregning (ADCourseCache fandt det passende)
|
||||
-- onDone(ok, reason) kaldes én gang; reason "copied"/"reused" ved succes med kopieret/genbrugt kursus.
|
||||
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
||||
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
||||
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
||||
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||
function ADCpBridge.vineOffsetFor(fruitName, role)
|
||||
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
||||
if role == "harvester" then return 0 end
|
||||
return -1
|
||||
end
|
||||
|
||||
-- Vinrækker der springes over: sprøjter/spredere arbejder på begge sider af gangen på én gang, så de kører kun
|
||||
-- i hver anden gang (1). Andre redskaber (harve, mulcher) arbejder mod én side (0). nil = ikke en vinmark.
|
||||
ADCpBridge.VINE_BOTH_SIDES = {fertilizer = true, weeder = true, limer = true}
|
||||
function ADCpBridge.vineSkipFor(fruitName, role)
|
||||
if ADCpBridge.vineOffsetFor(fruitName, role) == nil then return nil end
|
||||
return ADCpBridge.VINE_BOTH_SIDES[role] and 1 or 0
|
||||
end
|
||||
|
||||
function ADCpBridge.setVineSkip(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||
return (pcall(function() vehicle:getCpVineSettings().vineRowsToSkip:setValue(value) end))
|
||||
end
|
||||
|
||||
function ADCpBridge.setVineOffset(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||
return (pcall(function() vehicle:getCpVineSettings().vineCenterOffset:setValue(value) end))
|
||||
end
|
||||
|
||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
opts = opts or {}
|
||||
if not ADCpBridge.isAvailable() then
|
||||
@@ -342,6 +581,12 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
local params = job.cpJobParameters
|
||||
if params ~= nil and params.fieldPosition ~= nil then params.fieldPosition:setPosition(fieldX, fieldZ) end
|
||||
if params ~= nil and params.startPosition ~= nil then params.startPosition:setPosition(vx, vz) end
|
||||
-- startpunkt i kurset: CP's standard er "first" (forfra) — "nearest"/"last" på en påbegyndt mark
|
||||
local startAtValues = CpFieldWorkJobParameters ~= nil and {first = CpFieldWorkJobParameters.START_AT_FIRST_POINT,
|
||||
nearest = CpFieldWorkJobParameters.START_AT_NEAREST_POINT, last = CpFieldWorkJobParameters.START_AT_LAST_POINT} or {}
|
||||
if opts.startAt ~= nil and startAtValues[opts.startAt] ~= nil and params ~= nil and params.startAt ~= nil then
|
||||
pcall(function() params.startAt:setValue(startAtValues[opts.startAt]) end)
|
||||
end
|
||||
job:setValues()
|
||||
local handle = {cancelled = false, fired = false, finished = false}
|
||||
local function finish(ok, reason)
|
||||
@@ -355,10 +600,16 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
local ok, err = job:validate(farmId)
|
||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true, handle.copiedCourse and "copied" or nil)
|
||||
finish(true, handle.copiedCourse and "copied" or (handle.reusedCourse and "reused" or nil))
|
||||
end
|
||||
local function generateAndStart()
|
||||
if handle.cancelled then return end
|
||||
-- genbrug: maskinens eget kursus (CP's validate kræver kun hasCpCourse) — ingen ny beregning
|
||||
-- eget kursus genbruges kun med forager (ellers nyt kursus med 2 runder)
|
||||
if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil and ADCpBridge.courseHasHeadlands(ADCpBridge.getCourse(vehicle)) then
|
||||
handle.reusedCourse = true
|
||||
return startWithCourse()
|
||||
end
|
||||
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
||||
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
||||
if copied then
|
||||
@@ -368,11 +619,22 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||
end
|
||||
setMultiTools(vehicle, opts.multiTools or 1)
|
||||
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||
if handle.cancelled then return end
|
||||
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
startWithCourse()
|
||||
end)
|
||||
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
|
||||
local function generate(headlands)
|
||||
ADCpBridge.setHeadlands(vehicle, headlands)
|
||||
handle.generating = true
|
||||
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||
handle.generating = false
|
||||
if handle.cancelled then return end
|
||||
if not generated and headlands > 0 then
|
||||
Logging.info("ADSmartPickup: kursus med %d forager-runder kunne ikke laves — prøver uden forager", headlands)
|
||||
return generate(0)
|
||||
end
|
||||
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
startWithCourse()
|
||||
end)
|
||||
end
|
||||
generate(ADCpBridge.HEADLANDS)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
if handle.fired then return end
|
||||
@@ -406,6 +668,21 @@ function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station
|
||||
if params ~= nil then
|
||||
if params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
||||
if params.startPosition ~= nil then pcall(function() params.startPosition:setPosition(waitX, waitZ) end) end
|
||||
-- positioner med retning: CP's applyCurrentState sætter retningen, men fejler den (pcall ovenfor) bliver den
|
||||
-- aldrig sat, og spillet afviser jobbet med "Angle missing" (in-game 27/9 F15, hvert 5. min i en time)
|
||||
pcall(function()
|
||||
local dirX, _, dirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1)
|
||||
local angle = MathUtil.getYRotationFromDirection(dirX, dirZ)
|
||||
for _, position in ipairs({params.startPosition, params.fieldUnloadPosition}) do
|
||||
if position ~= nil and position.getAngle ~= nil and position:getAngle() == nil then
|
||||
if position.getPosition ~= nil and select(1, position:getPosition()) == nil then
|
||||
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
|
||||
position:setPosition(vx, vz)
|
||||
end
|
||||
position:setAngle(angle)
|
||||
end
|
||||
end
|
||||
end)
|
||||
if params.useGiantsUnload ~= nil then pcall(function() params.useGiantsUnload:setValue(true) end) end
|
||||
end
|
||||
pcall(function() job:setValues() end)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
-- AD CP Resume
|
||||
-- Spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (AutoDrive gør selv). Ved hvert gem
|
||||
-- skrives de maskiner hvor CP kører (uden AD) til ADSmartPickup_cpresume.xml; efter indlæsning startes netop
|
||||
-- dem igen fra nærmeste waypoint med CP's egen HUD-start (samme job som spilleren sidst valgte i HUD'en).
|
||||
|
||||
ADCpResume = {}
|
||||
ADCpResume.FILE_NAME = "ADSmartPickup_cpresume.xml"
|
||||
ADCpResume.ROOT = "ADSmartPickupCpResume"
|
||||
ADCpResume.LOG_PREFIX = "ADSmartPickup: CP-genstart: "
|
||||
ADCpResume.pending = nil -- {[vehicleId] = true} fra seneste gem; nil = ingen fil (gammel regel bruges)
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- vehicles = {{id, cpActive, adActive}} -> id'er der skal huskes
|
||||
function ADCpResume.activeIds(vehicles)
|
||||
local ids = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if vehicle.cpActive and not vehicle.adActive then table.insert(ids, vehicle.id) end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
function ADCpResume.fromRecords(ids)
|
||||
local set = {}
|
||||
for _, id in ipairs(ids or {}) do
|
||||
if type(id) == "string" and id ~= "" then set[id] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
-- vehicles = {{id, aiActive, controlled, fieldWork}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste).
|
||||
-- fieldWork = false: CP's HUD-job er ikke markarbejde (fx bunkersilo) — HUD-start ville starte det forkerte job
|
||||
-- (in-game 29/9: "CP: No silo found"); nil = ukendt (ældre CP) -> startes som før.
|
||||
function ADCpResume.toStart(pending, vehicles)
|
||||
if pending == nil then return nil end
|
||||
local ids = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled and vehicle.fieldWork ~= false then
|
||||
table.insert(ids, vehicle.id)
|
||||
end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
-- true/false fra CP's HUD; nil når CP-versionen ikke har funktionen
|
||||
local function hudFieldWork(vehicle)
|
||||
if vehicle.cpIsHudFieldWorkJobSelected == nil then return nil end
|
||||
return vehicle:cpIsHudFieldWorkJobSelected() == true
|
||||
end
|
||||
|
||||
local function vehicleState(adEnv, vehicle)
|
||||
local ad = vehicle.ad
|
||||
return {
|
||||
id = ADRunsController.getVehicleId(vehicle),
|
||||
cpActive = vehicle.getIsCpActive ~= nil and vehicle:getIsCpActive() == true,
|
||||
adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(),
|
||||
aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true,
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true,
|
||||
fieldWork = hudFieldWork(vehicle),
|
||||
}
|
||||
end
|
||||
|
||||
local function farmVehicles(adEnv)
|
||||
local list, byId = {}, {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local ok, state = pcall(vehicleState, adEnv, vehicle)
|
||||
if ok and state.id ~= nil then
|
||||
table.insert(list, state)
|
||||
byId[state.id] = vehicle
|
||||
end
|
||||
end
|
||||
return list, byId
|
||||
end
|
||||
|
||||
-- Starter CP fra nærmeste waypoint (HUD-startpunktet sættes som spillerens "start ved nærmeste"-tast).
|
||||
function ADCpResume.startNearest(vehicle)
|
||||
if vehicle.cpStartStopDriver == nil or (vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive()) then return false end
|
||||
if vehicle.getCpStartingPointSetting ~= nil and CpFieldWorkJobParameters ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse() then
|
||||
local setting = vehicle:getCpStartingPointSetting()
|
||||
if setting ~= nil then setting:setValue(CpFieldWorkJobParameters.START_AT_NEAREST_POINT) end
|
||||
end
|
||||
vehicle:cpStartStopDriver(true)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Id'er der skal startes nu (nil = ingen gemt liste). Listen bruges kun én gang.
|
||||
function ADCpResume.takeToStart(adEnv)
|
||||
local list, byId = farmVehicles(adEnv)
|
||||
local ids = ADCpResume.toStart(ADCpResume.pending, list)
|
||||
ADCpResume.pending = nil
|
||||
return ids, byId
|
||||
end
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADCpResume.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADCpResume.save(adEnv)
|
||||
local path = getPath()
|
||||
if path == nil or adEnv == nil then return false end
|
||||
local ids = ADCpResume.activeIds((farmVehicles(adEnv)))
|
||||
local xmlFile = XMLFile.create("ADSmartPickupCpResume", path, ADCpResume.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
for index, id in ipairs(ids) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", ADCpResume.ROOT, index - 1), id)
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
-- gemmes ved hver runs-gem (flere gange i minuttet): log kun når listen ændrer sig
|
||||
local signature = table.concat(ids, ",")
|
||||
if signature ~= ADCpResume.lastLogged then
|
||||
ADCpResume.lastLogged = signature
|
||||
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADCpResume.load()
|
||||
ADCpResume.pending = nil
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupCpResume", path)
|
||||
if xmlFile == nil then return end
|
||||
local ids = {}
|
||||
xmlFile:iterate(ADCpResume.ROOT .. ".vehicle", function(_, key) table.insert(ids, xmlFile:getString(key .. "#id")) end)
|
||||
xmlFile:delete()
|
||||
ADCpResume.pending = ADCpResume.fromRecords(ids)
|
||||
Logging.info(ADCpResume.LOG_PREFIX .. "%d maskine(r) kørte Courseplay ved sidste gem", #ids)
|
||||
end
|
||||
@@ -8,6 +8,7 @@ ADFieldFleet = {}
|
||||
-- plov, ikke aflæsningsvogn). sprayRole kommer fra sprøjtens/sprederens fyld (gødning/kalk/ukrudt).
|
||||
ADFieldFleet.ROLE_SPECS = {
|
||||
{role = "harvester", spec = "spec_combine"},
|
||||
{role = "baler", spec = "spec_baler"},
|
||||
{role = "collector", spec = "spec_forageWagon"},
|
||||
{role = "rake", spec = "spec_windrower"},
|
||||
{role = "tedder", spec = "spec_tedder"},
|
||||
@@ -17,8 +18,10 @@ ADFieldFleet.ROLE_SPECS = {
|
||||
{role = "weeder", spec = "spec_weeder"},
|
||||
{role = "sprayRole"},
|
||||
{role = "mulcher", spec = "spec_mulcher"},
|
||||
{role = "cultivator", spec = "spec_cultivator"},
|
||||
-- plov før kultivator: FS25-plove har også spec_cultivator (in-game 28/9: POV 5 XL blev "kultivator")
|
||||
{role = "plow", spec = "spec_plow"},
|
||||
{role = "cultivator", spec = "spec_cultivator"},
|
||||
{role = "baleCollector", spec = "canBaleCollect"},
|
||||
{role = "unloader", spec = "canUnload"},
|
||||
}
|
||||
|
||||
@@ -33,12 +36,23 @@ function ADFieldFleet.roleFromSpecs(specs)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Ekstra opgaver en rolle kan tage: pløjning klargør jorden lige så godt som kultivering (ikke omvendt).
|
||||
ADFieldFleet.EXTRA_ROLES = {plow = {plow = true, cultivator = true}}
|
||||
|
||||
function ADFieldFleet.rolesFor(role)
|
||||
local roles = ADFieldFleet.EXTRA_ROLES[role]
|
||||
if roles == nil then return nil end
|
||||
local copy = {}
|
||||
for key, value in pairs(roles) do copy[key] = value end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Kandidater med rollen, sorteret nærmest først, delt i brugbare og afviste (med første grund).
|
||||
local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, sawRole, sawAccepted = {}, false, false
|
||||
local widthReason, widthArgs = nil, nil
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if candidate.role == role and candidate.enabled then
|
||||
if (candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])) and candidate.enabled then
|
||||
sawRole = true
|
||||
if accept == nil or accept(candidate) then
|
||||
sawAccepted = true
|
||||
@@ -46,7 +60,12 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz})
|
||||
-- spreder med en anden vare på skal tømmes først: kun hvis ingen anden kan
|
||||
local wrongFill = candidate.filledRole ~= nil and candidate.filledRole ~= role
|
||||
-- fuld af en anden vare: bruges slet ikke til denne opgave (resten hældes kun ud når den er lille)
|
||||
if not (wrongFill and candidate.dumpable == false) then
|
||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz, wrongFill = wrongFill})
|
||||
end
|
||||
elseif widthReason == nil then
|
||||
widthReason, widthArgs = reason, args
|
||||
end
|
||||
@@ -54,7 +73,10 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(usable, function(a, b) return a.distance < b.distance end)
|
||||
table.sort(usable, function(a, b)
|
||||
if a.wrongFill ~= b.wrongFill then return not a.wrongFill end
|
||||
return a.distance < b.distance
|
||||
end)
|
||||
local reason, args = nil, nil
|
||||
if not sawRole then
|
||||
reason = "noRig"
|
||||
@@ -71,6 +93,43 @@ end
|
||||
-- Nærmeste brugbare sæt. Grund når intet passer: noRig (ingen med flueben og rollen), cannotDo (ingen kan
|
||||
-- tage opgaven, fx afgrøden), allBusy, tooWide/tooNarrow (bredden, med tal fra den første).
|
||||
-- accept (valgfri): candidate -> bool, fx "høsteren kan tage hvede".
|
||||
-- Faste maskiner: rigIdsByField = {[fieldId] = {id, ...}}. En maskine der er fast på en ANDEN mark må
|
||||
-- ikke bruges her. Har marken selv faste maskiner med rollen, bruges kun de (marken venter på sin egen);
|
||||
-- ellers de frie. (in-game 26/9: sprøjten "til F22" blev sendt til F109.)
|
||||
function ADFieldFleet.filterDedicated(candidates, fieldId, role, rigIdsByField)
|
||||
local ownerOf, own = {}, {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
for _, id in ipairs(ids or {}) do
|
||||
ownerOf[id] = otherId
|
||||
if otherId == fieldId then own[id] = true end
|
||||
end
|
||||
end
|
||||
local ownWithRole = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local hasRole = candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])
|
||||
if own[candidate.id] and hasRole then table.insert(ownWithRole, candidate) end
|
||||
end
|
||||
if #ownWithRole > 0 then return ownWithRole end
|
||||
local free = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
if ownerOf[candidate.id] == nil then table.insert(free, candidate) end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
-- Gør en maskine fast på én mark (fjernes fra andre marker). Returnerer ny tabel {[fieldId] = ids}.
|
||||
function ADFieldFleet.dedicate(rigIdsByField, fieldId, vehicleId, isOn)
|
||||
local result = {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
local copy = {}
|
||||
for _, id in ipairs(ids or {}) do if id ~= vehicleId then table.insert(copy, id) end end
|
||||
result[otherId] = copy
|
||||
end
|
||||
result[fieldId] = result[fieldId] or {}
|
||||
if isOn then table.insert(result[fieldId], vehicleId) end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADFieldFleet.pick(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
if usable[1] ~= nil then return usable[1].candidate end
|
||||
@@ -89,9 +148,39 @@ function ADFieldFleet.pickMany(candidates, role, n, x, z, requiredWidth, accept)
|
||||
return nil, reason, args
|
||||
end
|
||||
|
||||
-- Må sættet sendes ud? "all" (standard): alle ledige maskiner; "ticked": kun med flueben.
|
||||
function ADFieldFleet.isEnabled(mode, enabledIds, id)
|
||||
if mode == "ticked" then return (enabledIds or {})[id] == true end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldFleet.toggle(enabledIds, id)
|
||||
local copy = {}
|
||||
for key, value in pairs(enabledIds or {}) do copy[key] = value end
|
||||
copy[id] = (not copy[id]) or nil
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Til menuen: skjul maskiner der er faste på en ANDEN mark end den valgte. Returnerer synlige, antal
|
||||
-- skjulte og de marker de er faste på (sorteret). selectedFieldId nil = vis alle.
|
||||
function ADFieldFleet.visibleForField(rigs, selectedFieldId, rigIdsByField)
|
||||
if selectedFieldId == nil then return rigs, 0, {} end
|
||||
local ownerOf = {}
|
||||
for fieldId, ids in pairs(rigIdsByField or {}) do
|
||||
for _, id in ipairs(ids or {}) do ownerOf[id] = fieldId end
|
||||
end
|
||||
local visible, hidden, fieldSet = {}, 0, {}
|
||||
for _, rig in ipairs(rigs or {}) do
|
||||
local owner = ownerOf[rig.id]
|
||||
if owner ~= nil and owner ~= selectedFieldId then
|
||||
hidden = hidden + 1
|
||||
fieldSet[owner] = true
|
||||
else
|
||||
table.insert(visible, rig)
|
||||
end
|
||||
end
|
||||
local fields = {}
|
||||
for fieldId in pairs(fieldSet) do table.insert(fields, fieldId) end
|
||||
table.sort(fields, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return visible, hidden, fields
|
||||
end
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
-- AD Field Guide
|
||||
-- "Opsæt mark": markguidens trin oven på ADGuide (ren Lua, ingen spil-API). Kladden er en kopi af
|
||||
-- markens plan (+ faste maskiner), så intet ændres før [Gem]. Alt spil-afhængigt kommer i `context`:
|
||||
-- {info, sowable = {{name, title}}, mowable = {[name] = true}, straw = {[name] = true} | nil,
|
||||
-- rigs = {{id, text, role}}, rigIdsByField = {[fieldId] = ids}, fieldSilos = {{id, text}},
|
||||
-- stores = {{id, text}} | nil (nil = markens vare kendes ikke), storeHidden, fields = {{id, text}}}
|
||||
-- Rækker er data: {type = "choice"|"toggle"|"check"|"text"|"link", id, title/label, ...}; GUI'et tegner dem.
|
||||
|
||||
ADFieldGuide = {}
|
||||
|
||||
ADFieldGuide.ORDER = {"field", "crop", "cropChoice", "prep", "care", "machines", "store", "summary"}
|
||||
ADFieldGuide.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
||||
ADFieldGuide.STRAW_VALUES = {"chop", "collect", "bale"}
|
||||
|
||||
local function L(key, args) return {key = key, args = args} end
|
||||
local function T(text) return {text = text} end
|
||||
local YES_NO = {off = L("ui_no"), on = L("ui_yes")}
|
||||
|
||||
-- ---- afledte egenskaber ----
|
||||
|
||||
local function target(draft)
|
||||
return draft.plan ~= nil and draft.plan.targetFruit or ""
|
||||
end
|
||||
|
||||
local function isMowable(context, name)
|
||||
return name ~= nil and name ~= "" and (context.mowable or {})[name] == true
|
||||
end
|
||||
|
||||
-- Ukendt halm-tabel (tom/nil): alle høstafgrøder regnes med halm (som før guiden).
|
||||
local function hasStraw(context, name)
|
||||
if context.straw == nil or next(context.straw) == nil then return true end
|
||||
return name ~= nil and context.straw[name] == true
|
||||
end
|
||||
|
||||
local function info(guide) return guide.context.info or {} end
|
||||
|
||||
local function mowOptions(guide)
|
||||
local t = target(guide.draft)
|
||||
return info(guide).kind == "mowable" or isMowable(guide.context, t)
|
||||
end
|
||||
|
||||
local function strawOptions(guide)
|
||||
local t, i = target(guide.draft), info(guide)
|
||||
if t ~= "" then
|
||||
if isMowable(guide.context, t) then return false end
|
||||
return hasStraw(guide.context, t)
|
||||
end
|
||||
return i.kind == "harvest" and hasStraw(guide.context, i.fruitName)
|
||||
end
|
||||
|
||||
-- Der høstes (mejetærsker + aflæsningsvogne) nu eller senere.
|
||||
local function harvestInvolved(guide)
|
||||
local t = target(guide.draft)
|
||||
return info(guide).kind == "harvest" or (t ~= "" and not isMowable(guide.context, t))
|
||||
end
|
||||
|
||||
local function prepVisible(guide)
|
||||
local t, i = target(guide.draft), info(guide)
|
||||
return i.kind ~= "mowable" or (t ~= "" and t ~= i.fruitName)
|
||||
end
|
||||
|
||||
local function careVisible(guide)
|
||||
return info(guide).kind ~= "bare" or target(guide.draft) ~= ""
|
||||
end
|
||||
|
||||
-- Ukrudt giver kun mening på afgrøder der ikke slås (samme regel som før).
|
||||
local function weedVisible(guide)
|
||||
local t = target(guide.draft)
|
||||
if t ~= "" then return not isMowable(guide.context, t) end
|
||||
return info(guide).kind ~= "mowable"
|
||||
end
|
||||
|
||||
local VISIBLE = {
|
||||
field = function(guide) return guide.draft.pickField == true end,
|
||||
crop = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
cropChoice = function(guide) return guide.draft.fieldId ~= nil and (mowOptions(guide) or strawOptions(guide)) end,
|
||||
prep = function(guide) return guide.draft.fieldId ~= nil and prepVisible(guide) end,
|
||||
care = function(guide) return guide.draft.fieldId ~= nil and careVisible(guide) end,
|
||||
machines = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
store = function(guide) return guide.draft.fieldId ~= nil and guide.context.stores ~= nil and info(guide).kind ~= "bare" end,
|
||||
summary = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
}
|
||||
|
||||
function ADFieldGuide.isVisible(step, guide)
|
||||
local rule = VISIBLE[step]
|
||||
return rule ~= nil and rule(guide) == true
|
||||
end
|
||||
|
||||
ADFieldGuide.DEF = {order = ADFieldGuide.ORDER, isVisible = ADFieldGuide.isVisible}
|
||||
|
||||
function ADFieldGuide.stepKey(step) return "spu_guide_field_step_" .. tostring(step) end
|
||||
function ADFieldGuide.hintKey(step) return "spu_guide_field_hint_" .. tostring(step) end
|
||||
|
||||
-- ---- kladde ----
|
||||
|
||||
local function isPrepAuto(plan)
|
||||
return (plan.soil or "auto") == "auto" and plan.mulch ~= true and plan.lime ~= false and plan.roll ~= true
|
||||
end
|
||||
|
||||
-- Pleje "Automatisk" = al pleje slået til (gødning, ukrudt, kalk). Kalk tæller kun her når den ikke hører
|
||||
-- under klargøring — ellers afgør klargøringens valg den.
|
||||
local function isCareAuto(plan)
|
||||
return plan.fertilize ~= false and plan.weed ~= false and plan.lime ~= false
|
||||
end
|
||||
|
||||
local function storeModeOf(ids, fieldSilos)
|
||||
if #ids == 0 then return "auto" end
|
||||
local silo = {}
|
||||
for _, entry in ipairs(fieldSilos or {}) do silo[entry.id] = true end
|
||||
for _, id in ipairs(ids) do
|
||||
if not silo[id] then return "manual" end
|
||||
end
|
||||
return "fieldSilo"
|
||||
end
|
||||
|
||||
local function draftFrom(field, context)
|
||||
local plan = ADGuide.deepCopy(field.plan or {})
|
||||
plan.storeBuildingIds = plan.storeBuildingIds or {}
|
||||
plan.rigIds = plan.rigIds or {}
|
||||
local mode = storeModeOf(plan.storeBuildingIds, context.fieldSilos)
|
||||
return {
|
||||
fieldId = field.id, plan = plan, originalTarget = plan.targetFruit or "",
|
||||
prepAuto = isPrepAuto(plan), careAuto = isCareAuto(plan), useDedicated = #plan.rigIds > 0,
|
||||
storeMode = mode, storeIds = mode == "manual" and ADGuide.deepCopy(plan.storeBuildingIds) or {},
|
||||
}
|
||||
end
|
||||
|
||||
function ADFieldGuide.open(field, context, options)
|
||||
return ADGuide.new(ADFieldGuide.DEF, draftFrom(field, context), context, options)
|
||||
end
|
||||
|
||||
-- Guiden åbnet uden valgt mark: trin 1 "Mark".
|
||||
function ADFieldGuide.openPicker(context)
|
||||
return ADGuide.new(ADFieldGuide.DEF, {pickField = true}, context, {step = "field"})
|
||||
end
|
||||
|
||||
-- Mark valgt i trin 1: kladden bygges fra markens plan; trin 1 tæller stadig med.
|
||||
function ADFieldGuide.pickField(guide, field, context)
|
||||
local draft = draftFrom(field, context)
|
||||
draft.pickField = true
|
||||
local picked = ADGuide.withContext(ADGuide.withDraft(guide, draft), context)
|
||||
return (ADGuide.next(picked))
|
||||
end
|
||||
|
||||
-- ---- valg ----
|
||||
|
||||
local PLAN_VALUES = {
|
||||
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
|
||||
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
|
||||
}
|
||||
|
||||
function ADFieldGuide.choose(guide, id, value)
|
||||
local draft = ADGuide.deepCopy(guide.draft)
|
||||
if PLAN_VALUES[id] then
|
||||
draft.plan[id] = value
|
||||
elseif id == "product" then
|
||||
draft.plan.product = value and "hay" or "silage"
|
||||
elseif id == "prepAuto" or id == "careAuto" or id == "useDedicated" then
|
||||
draft[id] = value == true
|
||||
elseif id == "rigIds" then
|
||||
draft.plan.rigIds = ADGuide.toggleItem(draft.plan.rigIds, value.item, value.on)
|
||||
elseif id == "useFieldSilo" then
|
||||
draft.storeMode = value and "fieldSilo" or "auto"
|
||||
elseif id == "storeAuto" then
|
||||
draft.storeMode = value and "auto" or "manual"
|
||||
elseif id == "storeBuildingIds" then
|
||||
draft.storeIds = ADGuide.toggleItem(draft.storeIds, value.item, value.on)
|
||||
else
|
||||
return guide
|
||||
end
|
||||
return ADGuide.withDraft(guide, draft)
|
||||
end
|
||||
|
||||
-- ---- rækker pr. trin ----
|
||||
|
||||
local function numberOptions(from, to)
|
||||
local options = {}
|
||||
for value = from, to do table.insert(options, {label = T(tostring(value)), value = value}) end
|
||||
return options
|
||||
end
|
||||
|
||||
local function choice(id, title, options, value)
|
||||
return {type = "choice", id = id, title = title, options = options, value = value}
|
||||
end
|
||||
|
||||
local function toggle(id, title, value, off, on)
|
||||
return {type = "toggle", id = id, title = title, off = off or YES_NO.off, on = on or YES_NO.on, value = value == true}
|
||||
end
|
||||
|
||||
local function rowsField(guide)
|
||||
local rows = {}
|
||||
for _, entry in ipairs(guide.context.fields or {}) do
|
||||
table.insert(rows, {type = "link", id = "field", value = entry.id, label = T(entry.text)})
|
||||
end
|
||||
if #rows == 0 then table.insert(rows, {type = "text", label = L("spu_field_scanning")}) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCrop(guide)
|
||||
local plan = guide.draft.plan
|
||||
local options = {{label = L("spu_guide_crop_keep"), value = ""}}
|
||||
for _, fruit in ipairs(guide.context.sowable or {}) do
|
||||
table.insert(options, {label = T(fruit.title), value = fruit.name})
|
||||
end
|
||||
local rows = {choice("targetFruit", L("spu_guide_crop"), options, plan.targetFruit or "")}
|
||||
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
|
||||
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCropChoice(guide)
|
||||
local plan, rows = guide.draft.plan, {}
|
||||
if mowOptions(guide) then
|
||||
table.insert(rows, toggle("product", L("spu_field_plan"), ADFieldModel.product(plan, "mowable") == "hay",
|
||||
L("spu_field_plan_silage"), L("spu_field_plan_hay")))
|
||||
table.insert(rows, toggle("rake", L("spu_field_rake"), plan.rake))
|
||||
end
|
||||
if strawOptions(guide) then
|
||||
local options = {}
|
||||
for _, value in ipairs(ADFieldGuide.STRAW_VALUES) do
|
||||
table.insert(options, {label = L("spu_field_straw_" .. value), value = value})
|
||||
end
|
||||
table.insert(rows, choice("straw", L("spu_field_straw"), options, plan.straw or "chop"))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsPrep(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("prepAuto", L("spu_guide_prep"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.prepAuto)}
|
||||
if draft.prepAuto then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_prep_autoText")})
|
||||
return rows
|
||||
end
|
||||
local soil = {}
|
||||
for _, value in ipairs(ADFieldGuide.SOIL_VALUES) do
|
||||
table.insert(soil, {label = L("spu_field_soil_" .. value), value = value})
|
||||
end
|
||||
table.insert(rows, choice("soil", L("spu_field_soil"), soil, plan.soil or "auto"))
|
||||
table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false))
|
||||
table.insert(rows, toggle("mulch", L("spu_field_mulch"), plan.mulch))
|
||||
if target(draft) ~= "" or plan.sow then
|
||||
table.insert(rows, toggle("roll", L("spu_field_roll"), plan.roll))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsCare(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("careAuto", L("spu_guide_care"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.careAuto)}
|
||||
if draft.careAuto then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_care_autoText")})
|
||||
return rows
|
||||
end
|
||||
table.insert(rows, toggle("fertilize", L("spu_field_fertilize"), plan.fertilize ~= false))
|
||||
if weedVisible(guide) then table.insert(rows, toggle("weed", L("spu_field_weed"), plan.weed ~= false)) end
|
||||
if not prepVisible(guide) then table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false)) end
|
||||
return rows
|
||||
end
|
||||
|
||||
-- Faste maskiner: kun ledige og dem der allerede er faste her; faste på andre marker skjules med en linje.
|
||||
local function dedicatedRows(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local byField = {}
|
||||
for fieldId, ids in pairs(guide.context.rigIdsByField or {}) do byField[fieldId] = ids end
|
||||
byField[draft.fieldId] = draft.plan.rigIds
|
||||
local own = {}
|
||||
for _, id in ipairs(draft.plan.rigIds) do own[id] = true end
|
||||
local visible, hidden, fields = ADFieldFleet.visibleForField(guide.context.rigs or {}, draft.fieldId, byField)
|
||||
local sorted = {}
|
||||
for _, rig in ipairs(visible) do table.insert(sorted, rig) end
|
||||
table.sort(sorted, function(a, b)
|
||||
if (own[a.id] == true) ~= (own[b.id] == true) then return own[a.id] == true end
|
||||
if (a.role or "") ~= (b.role or "") then return (a.role or "") < (b.role or "") end
|
||||
return ADFlowView.naturalLess(tostring(a.text), tostring(b.text))
|
||||
end)
|
||||
for _, rig in ipairs(sorted) do
|
||||
table.insert(rows, {type = "check", id = "rigIds", item = rig.id, title = T(rig.text), value = own[rig.id] == true})
|
||||
end
|
||||
if #sorted == 0 then table.insert(rows, {type = "text", label = L("spu_field_noRigs")}) end
|
||||
if hidden > 0 then
|
||||
local names = {}
|
||||
for _, fieldId in ipairs(fields) do table.insert(names, "F" .. tostring(fieldId)) end
|
||||
table.insert(rows, {type = "text", label = L("spu_field_hiddenDedicated", {hidden, table.concat(names, ", ")})})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsMachines(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local rows = {choice("rigs", L("spu_guide_rigs"), numberOptions(1, ADFieldModel.MAX_RIGS), plan.rigs or 1)}
|
||||
if harvestInvolved(guide) then
|
||||
table.insert(rows, choice("unloaders", L("spu_field_unloaders"), numberOptions(1, ADFieldModel.MAX_UNLOADERS), plan.unloaders or 1))
|
||||
end
|
||||
table.insert(rows, toggle("useDedicated", L("spu_guide_dedicated"), draft.useDedicated))
|
||||
if draft.useDedicated then
|
||||
for _, row in ipairs(dedicatedRows(guide)) do table.insert(rows, row) end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsStore(guide)
|
||||
local draft, context, rows = guide.draft, guide.context, {}
|
||||
local silos = context.fieldSilos or {}
|
||||
if #silos > 0 then
|
||||
local names = {}
|
||||
for _, silo in ipairs(silos) do table.insert(names, silo.text) end
|
||||
table.insert(rows, toggle("useFieldSilo", L("spu_guide_useFieldSilo"), draft.storeMode == "fieldSilo"))
|
||||
table.insert(rows, {type = "text", label = T(" " .. table.concat(names, ", "))})
|
||||
if draft.storeMode == "fieldSilo" then return rows end
|
||||
end
|
||||
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
|
||||
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeMode ~= "manual"))
|
||||
if draft.storeMode ~= "manual" then
|
||||
table.insert(rows, {type = "text", label = L("spu_guide_store_autoText")})
|
||||
return rows
|
||||
end
|
||||
local chosen = {}
|
||||
for _, id in ipairs(draft.storeIds) do chosen[id] = true end
|
||||
for _, store in ipairs(context.stores or {}) do
|
||||
table.insert(rows, {type = "check", id = "storeBuildingIds", item = store.id, title = T(store.text), value = chosen[store.id] == true})
|
||||
end
|
||||
if #(context.stores or {}) == 0 then table.insert(rows, {type = "text", label = L("spu_field_noStore")}) end
|
||||
if (context.storeHidden or 0) > 0 then
|
||||
table.insert(rows, {type = "text", label = L("spu_field_storeHidden", {context.storeHidden})})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsSummary(guide)
|
||||
local rows = {}
|
||||
for _, line in ipairs(ADFieldGuide.summary(guide)) do
|
||||
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local ROWS = {
|
||||
field = rowsField, crop = rowsCrop, cropChoice = rowsCropChoice, prep = rowsPrep, care = rowsCare,
|
||||
machines = rowsMachines, store = rowsStore, summary = rowsSummary,
|
||||
}
|
||||
|
||||
function ADFieldGuide.rows(guide)
|
||||
local builder = ROWS[guide.step]
|
||||
return builder ~= nil and builder(guide) or {}
|
||||
end
|
||||
|
||||
-- ---- resumé ----
|
||||
|
||||
local function fruitTitle(context, name)
|
||||
for _, fruit in ipairs(context.sowable or {}) do
|
||||
if fruit.name == name then return fruit.title end
|
||||
end
|
||||
return tostring(name)
|
||||
end
|
||||
|
||||
local function storeText(guide)
|
||||
local draft, context = guide.draft, guide.context
|
||||
if draft.storeMode == "fieldSilo" then return {L("spu_guide_sum_fieldSilo")} end
|
||||
if draft.storeMode ~= "manual" or #draft.storeIds == 0 then return {L("spu_guide_sum_auto")} end
|
||||
local names = {}
|
||||
for _, id in ipairs(draft.storeIds) do
|
||||
for _, store in ipairs(context.stores or {}) do
|
||||
if store.id == id then table.insert(names, store.text) end
|
||||
end
|
||||
end
|
||||
if #names == 0 or #names > 2 or #names ~= #draft.storeIds then
|
||||
return {L("spu_guide_sum_stores", {#draft.storeIds})}
|
||||
end
|
||||
return {T(table.concat(names, " + "))}
|
||||
end
|
||||
|
||||
local SUMMARY = {
|
||||
crop = function(guide)
|
||||
local plan, t = guide.draft.plan, target(guide.draft)
|
||||
if t ~= "" then return {L("spu_guide_sum_target", {fruitTitle(guide.context, t)})} end
|
||||
local parts = {L("spu_guide_crop_keep")}
|
||||
if plan.sow and info(guide).kind ~= "mowable" then table.insert(parts, L("spu_guide_sum_sowAgain")) end
|
||||
return parts
|
||||
end,
|
||||
cropChoice = function(guide)
|
||||
local plan, parts = guide.draft.plan, {}
|
||||
if mowOptions(guide) then
|
||||
table.insert(parts, L(ADFieldModel.product(plan, "mowable") == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage"))
|
||||
table.insert(parts, L(plan.rake and "spu_guide_sum_rake" or "spu_guide_sum_noRake"))
|
||||
end
|
||||
if strawOptions(guide) then table.insert(parts, L("spu_guide_sum_straw_" .. (plan.straw or "chop"))) end
|
||||
return parts
|
||||
end,
|
||||
prep = function(guide)
|
||||
local plan = guide.draft.plan
|
||||
if guide.draft.prepAuto then return {L("spu_guide_sum_auto")} end
|
||||
local parts = {L("spu_field_soil_" .. (plan.soil or "auto"))}
|
||||
if plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
|
||||
if plan.mulch then table.insert(parts, L("spu_guide_sum_mulch")) end
|
||||
if plan.roll and (plan.sow or target(guide.draft) ~= "") then table.insert(parts, L("spu_guide_sum_roll")) end
|
||||
return parts
|
||||
end,
|
||||
care = function(guide)
|
||||
if guide.draft.careAuto then return {L("spu_guide_sum_auto")} end
|
||||
local plan, parts = guide.draft.plan, {}
|
||||
if plan.fertilize ~= false then table.insert(parts, L("spu_guide_sum_fert")) end
|
||||
if weedVisible(guide) and plan.weed ~= false then table.insert(parts, L("spu_guide_sum_weed")) end
|
||||
if not prepVisible(guide) and plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
|
||||
if #parts == 0 then parts = {L("spu_guide_sum_noCare")} end
|
||||
return parts
|
||||
end,
|
||||
machines = function(guide)
|
||||
local draft, plan = guide.draft, guide.draft.plan
|
||||
local parts = {}
|
||||
if draft.useDedicated and #plan.rigIds > 0 then
|
||||
table.insert(parts, L("spu_guide_sum_dedicated", {#plan.rigIds}))
|
||||
else
|
||||
table.insert(parts, L("spu_guide_sum_freeRigs"))
|
||||
end
|
||||
table.insert(parts, L("spu_guide_sum_rigs", {plan.rigs or 1}))
|
||||
if harvestInvolved(guide) then table.insert(parts, L("spu_guide_sum_unloaders", {plan.unloaders or 1})) end
|
||||
return parts
|
||||
end,
|
||||
store = storeText,
|
||||
}
|
||||
|
||||
-- Resumé-linjer {step, title, parts} for de synlige trin (ikke "Mark" og "Oversigt").
|
||||
function ADFieldGuide.summary(guide)
|
||||
local lines = {}
|
||||
for _, step in ipairs(ADGuide.visibleSteps(guide)) do
|
||||
local build = SUMMARY[step]
|
||||
if build ~= nil then
|
||||
table.insert(lines, {step = step, title = L(ADFieldGuide.stepKey(step)), parts = build(guide)})
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- gem ----
|
||||
|
||||
-- start: true = [Kør automatisk], false = [Gem uden at starte], nil = [Ret] (tilstanden bevares).
|
||||
-- Mål-afgrøde kører altid automatisk (ADFieldModel.setTarget).
|
||||
function ADFieldGuide.result(guide, start)
|
||||
local draft = guide.draft
|
||||
local plan = ADGuide.deepCopy(draft.plan)
|
||||
local t = plan.targetFruit or ""
|
||||
if t ~= "" then
|
||||
plan.mode = "auto"
|
||||
elseif start ~= nil then
|
||||
plan.mode = start and "auto" or "manual"
|
||||
end
|
||||
if draft.prepAuto and ADFieldGuide.isVisible("prep", guide) then
|
||||
plan.soil, plan.mulch, plan.lime, plan.roll = "auto", false, true, false
|
||||
end
|
||||
if draft.careAuto and ADFieldGuide.isVisible("care", guide) then
|
||||
plan.fertilize, plan.weed = true, true
|
||||
if not prepVisible(guide) then plan.lime = true end
|
||||
end
|
||||
if ADFieldGuide.isVisible("store", guide) then
|
||||
if draft.storeMode == "fieldSilo" then
|
||||
local ids = {}
|
||||
for _, silo in ipairs(guide.context.fieldSilos or {}) do table.insert(ids, silo.id) end
|
||||
plan.storeBuildingIds = ids
|
||||
elseif draft.storeMode == "manual" then
|
||||
plan.storeBuildingIds = ADGuide.deepCopy(draft.storeIds)
|
||||
else
|
||||
plan.storeBuildingIds = {}
|
||||
end
|
||||
end
|
||||
local rigIds = draft.useDedicated and ADGuide.deepCopy(plan.rigIds) or {}
|
||||
plan.rigIds = nil
|
||||
return {fieldId = draft.fieldId, targetFruit = t, targetChanged = t ~= (draft.originalTarget or ""), plan = plan, rigIds = rigIds}
|
||||
end
|
||||
|
||||
-- Faste maskiner for alle marker efter gem: marken får rigIds, og de fjernes fra andre marker.
|
||||
function ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds)
|
||||
local mine = {}
|
||||
for _, id in ipairs(rigIds or {}) do mine[id] = true end
|
||||
local result = {}
|
||||
for otherId, ids in pairs(rigIdsByField or {}) do
|
||||
if otherId ~= fieldId then
|
||||
local copy = {}
|
||||
for _, id in ipairs(ids or {}) do if not mine[id] then table.insert(copy, id) end end
|
||||
result[otherId] = copy
|
||||
end
|
||||
end
|
||||
result[fieldId] = ADGuide.deepCopy(rigIds or {})
|
||||
return result
|
||||
end
|
||||
@@ -7,28 +7,139 @@ ADFieldJobs = {}
|
||||
|
||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADFieldJobs.TICK_MS = 1000
|
||||
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
||||
ADFieldJobs.PASSIVE_SLICES = 10
|
||||
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
||||
ADFieldJobs.SLOW_TICK_MS = 3
|
||||
ADFieldJobs.tickCount = 0
|
||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
||||
ADFieldJobs.MAX_PASSES = 2
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
||||
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
||||
-- courseRecords: vehicleId -> post i kursus-registret (ADCourseCache)
|
||||
-- manual: {[vehicleId] = true} = høstere spilleren selv kører til marken (for brede til vejene)
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}, manual = {}}
|
||||
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
||||
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||
ADFieldJobs.courses = {}
|
||||
ADFieldJobs.finished = {}
|
||||
ADFieldJobs.timer = 0
|
||||
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
|
||||
ADFieldJobs.timer = 500
|
||||
ADFieldJobs.now = 0
|
||||
ADFieldJobs.loaded = false
|
||||
|
||||
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
||||
-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang)
|
||||
ADFieldJobs.ACTIVE_PHASES = {fetching = true, driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true}
|
||||
|
||||
-- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente.
|
||||
ADFieldJobs.REGROW_RATIO = 0.1
|
||||
|
||||
-- Trin efter høsten der ødelægger/ignorerer en stående afgrøde.
|
||||
-- Klargøring der kan måles på jorden: trin -> info-felt (andel af marken). Kultivering er gjort når jorden er
|
||||
-- klargjort (kultiveret/pløjet/såbed/stubbearbejdet); pløjning først når den er pløjet.
|
||||
ADFieldJobs.GROUND_PREP_STAGES = {cultivating = "prepared", plowing = "plowed"}
|
||||
ADFieldJobs.PREPARED_DONE_RATIO = 0.95
|
||||
|
||||
ADFieldJobs.PREP_STAGES = {strawCollecting = true, baling = true, baleCollecting = true, mulching = true, liming = true,
|
||||
plowing = true, cultivating = true, sowing = true, rolling = true}
|
||||
|
||||
-- ---- ren beslutning ----
|
||||
|
||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar, eller under 5 % af HELE
|
||||
-- marken står (rester i hjørnerne). readyRatio regner kun med den afgrøde der er tilbage, så en halvt høstet mark
|
||||
-- lignede "væk" (in-game 27/9 F111 gik til kultivering med halvdelen stående).
|
||||
ADFieldJobs.HARVEST_DONE_RATIO = 0.05
|
||||
local function cropGone(stage, info)
|
||||
if info == nil then return false end
|
||||
if stage == "harvesting" and info.kind == "bare" then return true end
|
||||
if stage == "harvesting" and type(info.standingRatio) == "number" then
|
||||
return info.standingRatio < ADFieldJobs.HARVEST_DONE_RATIO
|
||||
end
|
||||
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
||||
end
|
||||
|
||||
-- Ligger (x, z) inden for omridset? (lige-ulige-reglen; kanten tæller som inde)
|
||||
function ADFieldJobs.insidePolygon(x, z, verts)
|
||||
local inside, j = false, #verts
|
||||
for i = 1, #verts do
|
||||
local a, b = verts[i], verts[j]
|
||||
if (a.z > z) ~= (b.z > z) and x <= (b.x - a.x) * (z - a.z) / (b.z - a.z) + a.x then inside = not inside end
|
||||
j = i
|
||||
end
|
||||
return inside
|
||||
end
|
||||
|
||||
-- Efter genindlæsning: marken en CP-maskine står på, hvis markens aktuelle trin er maskinens rolle (ellers nil).
|
||||
-- fields = {{id, stage, verts = {{x, z}}}}
|
||||
function ADFieldJobs.resumeField(x, z, role, fields)
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if #(field.verts or {}) >= 3 and ADFieldModel.roleFor(field.stage) == role and ADFieldJobs.insidePolygon(x, z, field.verts) then
|
||||
return field.id
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvor Courseplay skal starte i kurset: "last" (eget kursus + husket waypoint = fortsæt præcis), "nearest"
|
||||
-- (marken er allerede delvist høstet/slået), ellers "first". CP's eget standardvalg er "first" — også på en
|
||||
-- halvt høstet mark (in-game 27/9: mejetærskeren startede F111 forfra).
|
||||
-- s = {reuse, remembered, stage, standingRatio}
|
||||
ADFieldJobs.PARTLY_DONE_RATIO = 0.95
|
||||
function ADFieldJobs.startAtFor(s)
|
||||
if s.reuse and s.remembered then return "last" end
|
||||
local cropStage = s.stage == "harvesting" or s.stage == "mowing"
|
||||
if cropStage and type(s.standingRatio) == "number" and s.standingRatio < ADFieldJobs.PARTLY_DONE_RATIO then return "nearest" end
|
||||
return "first"
|
||||
end
|
||||
|
||||
-- Skal en maskine med husket CP-waypoint genstartes efter indlæsning? Kun hvis den stadig er midt i jobbet: den
|
||||
-- står på en mark der er i gang med dens trin (fieldId), eller den står lige ved sit huskede waypoint. Et husket
|
||||
-- waypoint fra et gammelt job sendte ellers maskinen ud ad vejene (in-game 27/9: to ROPA'er). s = {fieldId, distance}
|
||||
ADFieldJobs.RESUME_NEAR_M = 30
|
||||
function ADFieldJobs.shouldResume(s)
|
||||
if s.fieldId ~= nil then return true end
|
||||
return type(s.distance) == "number" and s.distance <= ADFieldJobs.RESUME_NEAR_M
|
||||
end
|
||||
|
||||
-- Højst én maskine sendt ud pr. DISPATCH_GAP_MS på tværs af alle marker (in-game 27/9: 6 maskiner kørte fra
|
||||
-- parkeringen i samme sekund efter Start alle).
|
||||
ADFieldJobs.DISPATCH_GAP_MS = 10000
|
||||
function ADFieldJobs.mayDispatch(now, lastAt)
|
||||
return lastAt == nil or now - lastAt >= ADFieldJobs.DISPATCH_GAP_MS
|
||||
end
|
||||
|
||||
function ADFieldJobs.assignmentCount()
|
||||
local count = 0
|
||||
for _ in pairs(ADFieldJobs.state.assignments) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Har Courseplay-starten taget for lang tid? Mens CP beregner kursus (store marker), må det tage op til
|
||||
-- COURSE_GENERATE_MS; ellers START_MS (in-game 27/9 F90: 140 ha tog over 60 s, og moden opgav midt i beregningen).
|
||||
ADFieldJobs.START_MS = 60000
|
||||
ADFieldJobs.COURSE_GENERATE_MS = 300000
|
||||
function ADFieldJobs.startTimedOut(elapsed, generating)
|
||||
return elapsed > (generating and ADFieldJobs.COURSE_GENERATE_MS or ADFieldJobs.START_MS)
|
||||
end
|
||||
|
||||
-- Høsten er færdig, men høsteren har korn i tanken: tøm den på marken før marken går videre (én gang pr. mark).
|
||||
-- s = {fill, near, busy, controlled, tried}
|
||||
ADFieldJobs.emptyAttempts = {}
|
||||
function ADFieldJobs.shouldEmptyHarvester(s)
|
||||
return (s.fill or 0) > 0 and s.near == true and not s.busy and not s.controlled and not s.tried
|
||||
end
|
||||
|
||||
-- Markens punkt nærmest (x, z) — CP bruger det som kursets startsted. Uden position: første punkt.
|
||||
function ADFieldJobs.nearestPoint(points, x, z)
|
||||
if points == nil or points[1] == nil then return nil end
|
||||
if x == nil or z == nil then return points[1] end
|
||||
local best, bestD = nil, nil
|
||||
for _, point in ipairs(points) do
|
||||
local d = (point.x - x) ^ 2 + (point.z - z) ^ 2
|
||||
if bestD == nil or d < bestD then best, bestD = point, d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
||||
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
||||
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
||||
@@ -46,6 +157,20 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
end
|
||||
return {action = "start"}
|
||||
end
|
||||
-- mål-afgrøde på en bar mark: klargør straks; såningen venter selv på såtiden
|
||||
if info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
||||
if ADFieldModel.shouldStartBare(field, "bare") then
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
return {action = "start"}
|
||||
end
|
||||
end
|
||||
-- klargøring på en mark hvor afgrøden står uhøstet: tilbage til klar (in-game 27/9 F16: kartofler
|
||||
-- blev regnet som høstet, og marken stod til kultivering med afgrøden i jorden)
|
||||
local standing = info ~= nil and type(info.standingRatio) == "number" and info.standingRatio >= ADFieldJobs.HARVEST_DONE_RATIO
|
||||
if ADFieldJobs.PREP_STAGES[field.stage] and info ~= nil and info.kind == "harvest" and info.phase == "ready"
|
||||
and (standing or (info.readyRatio or 0) >= ADFieldModel.READY_RATIO) then
|
||||
return {action = "rewind"}
|
||||
end
|
||||
if not ADFieldModel.isWorking(field) then
|
||||
local care = ADFieldModel.careNeed(field, info)
|
||||
if care == nil then return {action = "none"} end
|
||||
@@ -53,7 +178,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if not hasMarker then return {action = "wait", care = care} end
|
||||
return {action = "care", stage = care}
|
||||
end
|
||||
local active, finished, helpers = 0, 0, 0
|
||||
local active, finished, helpers, awaiting = 0, 0, 0, 0
|
||||
for _, assignment in ipairs(assignments) do
|
||||
if assignment.stage == nil or assignment.stage == field.stage then
|
||||
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||
@@ -61,6 +186,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if isActive then helpers = helpers + 1 end
|
||||
else
|
||||
if isActive then active = active + 1 end
|
||||
if assignment.phase == "awaitingPlayer" then awaiting = awaiting + 1 end
|
||||
if assignment.phase == "finished" then finished = finished + 1 end
|
||||
end
|
||||
end
|
||||
@@ -69,10 +195,21 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
if active == 0 then
|
||||
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
||||
if isCropStage and cropGone(field.stage, info) then return {action = "advance"} end
|
||||
-- klargøringen er allerede gjort (målt på jorden): videre — også efter genindlæsning, hvor
|
||||
-- opgaven ikke er husket (in-game 28/9: F17 blev kultiveret 3 gange)
|
||||
local groundKey = ADFieldJobs.GROUND_PREP_STAGES[field.stage]
|
||||
if groundKey ~= nil and info ~= nil and type(info[groundKey]) == "number" and info[groundKey] >= ADFieldJobs.PREPARED_DONE_RATIO then
|
||||
return {action = "advance"}
|
||||
end
|
||||
if finished > 0 then
|
||||
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
||||
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
||||
end
|
||||
-- slåning uden maskine på en halvt slået mark: vent til genvæksten er fuldt udvokset igen
|
||||
-- (et hjørne Courseplay efterlod i samme omgang, finished > 0, slås straks)
|
||||
if field.stage == "mowing" and finished == 0 and info ~= nil and (info.immatureRatio or 0) > ADFieldJobs.REGROW_RATIO then
|
||||
return {action = "wait", reason = "regrowing"}
|
||||
end
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
||||
@@ -82,7 +219,8 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
end
|
||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||
-- tømmevogne først når en høster faktisk er i gang (ikke mens spilleren kører den derud)
|
||||
if helperRole ~= nil and active > awaiting and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
||||
end
|
||||
return {action = "none"}
|
||||
@@ -124,6 +262,10 @@ function ADFieldJobs.load()
|
||||
local ok, loaded = pcall(ADFieldStorage.load)
|
||||
if ok then
|
||||
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
||||
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
||||
ADFieldJobs.state.courseRecords = loaded.courseRecords or {}
|
||||
ADFieldJobs.state.manual = loaded.manual or {}
|
||||
ADFieldJobs.state.paused = loaded.paused == true
|
||||
else
|
||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
||||
end
|
||||
@@ -135,6 +277,30 @@ function ADFieldJobs.setPlan(id, changes)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- "Start nu" på en mark hvor moden venter på spilleren (manuel transport): overtag uden at vente
|
||||
ADFieldJobs.takeoverRequested = {}
|
||||
function ADFieldJobs.requestTakeover(id)
|
||||
ADFieldJobs.takeoverRequested[id] = true
|
||||
end
|
||||
|
||||
-- Venter moden på at spilleren kører en høster til marken? (assignment eller nil)
|
||||
function ADFieldJobs.awaitingPlayer(fieldId)
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
if assignment.fieldId == fieldId and assignment.phase == "awaitingPlayer" then return assignment end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Under global pause: kun denne mark genoptages (de andre venter på Start alle). nil = pause igen.
|
||||
function ADFieldJobs.setFieldResumed(id, isOn)
|
||||
ADFieldJobs.state.resumed = ADFieldJobs.state.resumed or {}
|
||||
ADFieldJobs.state.resumed[id] = isOn ~= false or nil
|
||||
end
|
||||
|
||||
function ADFieldJobs.isFieldResumed(id)
|
||||
return (ADFieldJobs.state.resumed or {})[id] == true
|
||||
end
|
||||
|
||||
function ADFieldJobs.requestStart(id)
|
||||
ADFieldJobs.note(id, {startRequested = true})
|
||||
end
|
||||
@@ -143,6 +309,30 @@ function ADFieldJobs.forceReady(id)
|
||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
end
|
||||
|
||||
function ADFieldJobs.setTarget(id, fruitName)
|
||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
||||
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.setFleetMode(mode)
|
||||
ADFieldJobs.state.fleetMode = mode
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- Pause (Stop alt og parkér): intet nyt sendes ud, ingen genstart efter indlæsning. Gemmes.
|
||||
function ADFieldJobs.setPaused(value)
|
||||
ADFieldJobs.state.paused = value == true
|
||||
ADFieldJobs.state.resumed = {}
|
||||
ADFieldJobs.save()
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde %s", value and "på pause" or "genoptaget")
|
||||
end
|
||||
|
||||
function ADFieldJobs.toggleManual(vehicleId)
|
||||
ADFieldJobs.state.manual = ADFieldFleet.toggle(ADFieldJobs.state.manual or {}, vehicleId)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.toggleFleet(vehicleId)
|
||||
ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
|
||||
ADFieldJobs.save()
|
||||
@@ -192,32 +382,56 @@ function ADFieldJobs.environment()
|
||||
local weather = g_currentMission.environment.weather
|
||||
return weather ~= nil and weather.getIsRaining ~= nil and weather:getIsRaining() or false
|
||||
end)
|
||||
return {raining = ok and raining == true}
|
||||
return {
|
||||
raining = ok and raining == true,
|
||||
-- kan afgrøden (navn) sås i den aktuelle periode?
|
||||
plantable = function(name)
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and ADCpBridge.isPlantableNow(fruit.index)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||
ADFieldJobs.lastDecision = {}
|
||||
|
||||
local function stepField(adEnv, info)
|
||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
||||
ADFieldJobs.setField(field)
|
||||
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||
ADFieldJobs.waitingSince = {}
|
||||
|
||||
local function stepField(adEnv, info, env)
|
||||
local current = ADFieldJobs.getField(info.id)
|
||||
local field = ADFieldModel.onReadiness(current, info.readyRatio or 0)
|
||||
if field ~= current then ADFieldJobs.setField(field) end
|
||||
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment())
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
|
||||
ADFieldJobs.lastDecision[info.id] = decision
|
||||
if decision.action == "start" then
|
||||
local started = ADFieldModel.start(field, info.kind, info.needs)
|
||||
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
|
||||
started.lastFruit = info.kind == "harvest" and info.fruitName or field.lastFruit
|
||||
ADFieldJobs.setField(started)
|
||||
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "rewind" then
|
||||
ADFieldJobs.setField(ADFieldModel.with(field, {stage = "ready", retryAt = 0, reason = false}))
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||
ADFieldJobs.log(info.id, "%s -> ready: afgrøden står stadig uhøstet (%.0f %% klar)", field.stage, (info.readyRatio or 0) * 100)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "care" then
|
||||
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
||||
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "advance" then
|
||||
-- høsteren har stadig korn i tanken: CP genstartes og venter ved kursets ende, tømmevognene kommer
|
||||
-- (marken bliver på høst til tanken er tom) — in-game 27/9 F15
|
||||
if field.stage == "harvesting" and ADFieldWork.tryEmptyHarvester ~= nil then
|
||||
local okEmpty, emptying = pcall(ADFieldWork.tryEmptyHarvester, adEnv, field, info)
|
||||
if okEmpty and emptying then return end
|
||||
end
|
||||
local nextField = ADFieldModel.advance(field, info.needs)
|
||||
ADFieldJobs.setField(nextField)
|
||||
ADFieldJobs.waitingSince[info.id] = nil
|
||||
ADFieldJobs.forgetFinished(info.id)
|
||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||
@@ -225,23 +439,58 @@ local function stepField(adEnv, info)
|
||||
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
||||
end
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "wait" and decision.reason == "rain" then
|
||||
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
||||
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
||||
elseif decision.action == "wait" and decision.reason ~= nil then
|
||||
local key = "spu_field_reason_" .. decision.reason
|
||||
if field.reason == nil or field.reason.key ~= key then
|
||||
ADFieldJobs.note(info.id, {reason = {key = key}})
|
||||
end
|
||||
elseif decision.action == "dispatch" then
|
||||
-- én udsendelse ad gangen på tværs af markerne: marken prøver igen ved næste tick
|
||||
if not ADFieldJobs.mayDispatch(ADFieldJobs.now, ADFieldJobs.lastDispatchAt) then return end
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
-- Realtid i ms: netGetTime (bruges overalt i FS), ellers getTimeSec.
|
||||
local function clockMs()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
local okSec, seconds = pcall(function() return getTimeSec() end)
|
||||
return okSec and type(seconds) == "number" and seconds * 1000 or nil
|
||||
end
|
||||
|
||||
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
|
||||
local function reportSlow(total, scan, follow, steps, stepped)
|
||||
if total == nil or total <= ADFieldJobs.SLOW_TICK_MS then return end
|
||||
if ADFieldJobs.lastSlowReport ~= nil and ADFieldJobs.now - ADFieldJobs.lastSlowReport < 60000 then return end
|
||||
ADFieldJobs.lastSlowReport = ADFieldJobs.now
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde-tick %.1f ms (scan %.1f, sæt %.1f, marker %.1f for %d marker)",
|
||||
total, scan or 0, follow or 0, steps or 0, stepped)
|
||||
end
|
||||
|
||||
ADFieldJobs.STARTUP_GRACE_MS = 120000
|
||||
|
||||
function ADFieldJobs.tick(adEnv, dt)
|
||||
ADFieldJobs.now = ADFieldJobs.now + dt
|
||||
ADFieldJobs.timer = ADFieldJobs.timer + dt
|
||||
if ADFieldJobs.loaded then
|
||||
-- markscanningen kører et lille stykke HVERT frame (arbejdskø), aldrig en hel mark i ét frame
|
||||
local okScan, errScan = pcall(ADFields.scanStep, adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
|
||||
if not okScan then Logging.warning(ADFieldJobs.LOG_PREFIX .. "markscanning: %s", tostring(errScan)) end
|
||||
end
|
||||
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
||||
ADFieldJobs.timer = 0
|
||||
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
||||
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
ADFields.scanNext(adEnv, farmId)
|
||||
local t0 = clockMs()
|
||||
local t1 = t0
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||
if not ok then
|
||||
@@ -249,8 +498,34 @@ function ADFieldJobs.tick(adEnv, dt)
|
||||
ADFieldWork.release(assignment)
|
||||
end
|
||||
end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
local ok, err = pcall(stepField, adEnv, info)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
local t2 = clockMs()
|
||||
-- de første minutter efter indlæsning sendes intet ud: Courseplay/AutoDrive genoptager deres egne jobs
|
||||
-- lidt efter start, og indtil da ligner de maskiner ledige (in-game 26/9: kultivatoren blev taget midt
|
||||
-- i spillerens eget CP-job 45 s efter indlæsning)
|
||||
if ADFieldJobs.now < ADFieldJobs.STARTUP_GRACE_MS then return end
|
||||
-- Stop alt og parkér: intet sendes ud og intet genstartes, før spilleren trykker Start alle — undtagen
|
||||
-- marker spilleren har genoptaget enkeltvis ([Start] i Fields-listen, ADFieldJobs.setFieldResumed)
|
||||
local paused, resumed = ADFieldJobs.state.paused, ADFieldJobs.state.resumed or {}
|
||||
if paused and next(resumed) == nil then return end
|
||||
if not paused and not ADFieldJobs.resumedAfterLoad then
|
||||
-- én gang, før noget nyt sendes ud: CP-maskiner der var midt i et job, fortsætter (og tæller som aktive)
|
||||
ADFieldJobs.resumedAfterLoad = true
|
||||
local okResume, errResume = pcall(ADFieldWork.resumeAfterLoad, adEnv)
|
||||
if not okResume then Logging.warning(ADFieldJobs.LOG_PREFIX .. "genstart efter indlæsning fejlede: %s", tostring(errResume)) end
|
||||
end
|
||||
local env = ADFieldJobs.environment()
|
||||
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
|
||||
local stepped = 0
|
||||
local infos = ADFields.list()
|
||||
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
||||
for index, info in ipairs(infos) do
|
||||
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
|
||||
if (not paused or resumed[info.id]) and (not ADFieldModel.isPassive(ADFieldJobs.getField(info.id)) or index % ADFieldJobs.PASSIVE_SLICES == slice) then
|
||||
stepped = stepped + 1
|
||||
local ok, err = pcall(stepField, adEnv, info, env)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
end
|
||||
end
|
||||
local t3 = clockMs()
|
||||
if t0 ~= nil and t3 ~= nil then reportSlow(t3 - t0, t1 - t0, t2 - t1, t3 - t2, stepped) end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ ADFieldModel.ROLES = {
|
||||
mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector",
|
||||
harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow",
|
||||
cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder",
|
||||
fertilizing = "fertilizer",
|
||||
fertilizing = "fertilizer", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector",
|
||||
}
|
||||
ADFieldModel.HELPERS = {harvesting = "unloader"}
|
||||
ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true}
|
||||
@@ -42,8 +42,11 @@ KNOWN_STAGES.careLiming = true
|
||||
local DEFAULT_PLAN = {
|
||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||
fertilize = true, weed = true,
|
||||
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
||||
}
|
||||
local STRAW_VALUES = {chop = true, collect = true, bale = true}
|
||||
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
||||
local STRAW_STAGES = {collect = {"strawCollecting"}, bale = {"baling", "baleCollecting"}}
|
||||
local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true}
|
||||
local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true}
|
||||
|
||||
@@ -58,6 +61,8 @@ local function copyPlan(plan)
|
||||
for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end
|
||||
for key, value in pairs(plan or {}) do copy[key] = value end
|
||||
copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil)
|
||||
-- faste maskiner (køretøjs-id'er) til marken; en fast maskine bruges kun her
|
||||
copy.rigIds = copyList(plan ~= nil and plan.rigIds or nil)
|
||||
return copy
|
||||
end
|
||||
|
||||
@@ -84,6 +89,8 @@ end
|
||||
|
||||
-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst.
|
||||
function ADFieldModel.product(plan, kind)
|
||||
-- silage/hø gælder kun slåafgrøder: et gemt produkt fra en fejlklassificeret mark (vinmark F22) ignoreres
|
||||
if kind ~= nil and kind ~= "mowable" then return "harvest" end
|
||||
if plan.product ~= nil and plan.product ~= "auto" then return plan.product end
|
||||
return kind == "mowable" and "silage" or "harvest"
|
||||
end
|
||||
@@ -116,6 +123,7 @@ function ADFieldModel.workStages(plan, kind, needs)
|
||||
local product = ADFieldModel.product(plan, kind)
|
||||
if product == "harvest" then
|
||||
local stages = {"harvesting"}
|
||||
for _, stage in ipairs(STRAW_STAGES[plan.straw or "chop"] or {}) do table.insert(stages, stage) end
|
||||
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
|
||||
return stages
|
||||
end
|
||||
@@ -123,12 +131,58 @@ function ADFieldModel.workStages(plan, kind, needs)
|
||||
if product == "hay" then table.insert(stages, "tedding") end
|
||||
if plan.rake then table.insert(stages, "raking") end
|
||||
table.insert(stages, "collecting")
|
||||
-- mål-afgrøde på en slåmark med en anden afgrøde: efter sidste slæt klargøres og sås målet
|
||||
if plan.prepAfterMow then
|
||||
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
|
||||
end
|
||||
return stages
|
||||
end
|
||||
|
||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner.
|
||||
-- Mål-afgrøde ("vælg mark + afgrøde, glem den"): planen udfyldes ud fra hvad der står på marken nu.
|
||||
-- Står målet der allerede som slåafgrøde (græs -> græs): bare slå igen og igen. Ellers: høst/slå det der
|
||||
-- står, klargør og så målet. Kører altid automatisk. Uden mål returneres planen uændret.
|
||||
function ADFieldModel.applyTarget(plan, kind, currentFruit)
|
||||
local copy = copyPlan(plan)
|
||||
if copy.targetFruit == nil or copy.targetFruit == "" then return copy end
|
||||
copy.mode = "auto"
|
||||
copy.sowFruit = copy.targetFruit
|
||||
if kind == "mowable" then
|
||||
copy.prepAfterMow = currentFruit ~= copy.targetFruit
|
||||
copy.sow = copy.prepAfterMow
|
||||
else
|
||||
copy.prepAfterMow = false
|
||||
copy.sow = true
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
|
||||
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
|
||||
function ADFieldModel.setTarget(field, fruitName)
|
||||
local copy = copyField(field)
|
||||
copy.plan.targetFruit = fruitName or ""
|
||||
copy.held = nil
|
||||
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
||||
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
||||
copy.stage = "idle"
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Bar mark med mål-afgrøde starter selv klargøring + såning med det samme: klargøringen (kalk, plov,
|
||||
-- kultivator) kører uanset årstid, og kun såningen venter på såtiden (preflight: notPlantable). Med mange
|
||||
-- marker nås det ellers ikke inden såvinduet lukker (brugerønske 28/9).
|
||||
function ADFieldModel.shouldStartBare(field, kind)
|
||||
if field.plan.targetFruit == nil or field.plan.targetFruit == "" then return false end
|
||||
-- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning
|
||||
if field.held and not field.startRequested then return false end
|
||||
return kind == "bare" and field.stage == "idle"
|
||||
end
|
||||
|
||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
|
||||
-- høsterens spor — der er breddekravet ligegyldigt).
|
||||
function ADFieldModel.requiredWidth(field, role)
|
||||
if role == "collector" and not field.plan.rake then
|
||||
if role == "collector" and not field.plan.rake and field.stage ~= "strawCollecting" then
|
||||
return field.mowWidth
|
||||
end
|
||||
return nil
|
||||
@@ -136,6 +190,8 @@ end
|
||||
|
||||
-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget.
|
||||
function ADFieldModel.courseSource(field, role)
|
||||
-- halm: læssevogn og presser kører høsterens kursus (skåret ligger i mejetærskerens spor)
|
||||
if field.stage == "strawCollecting" or role == "baler" then return "harvester" end
|
||||
if role ~= "collector" then return nil end
|
||||
return field.plan.rake and "rake" or "mower"
|
||||
end
|
||||
@@ -174,16 +230,27 @@ function ADFieldModel.lanePositions(n)
|
||||
end
|
||||
|
||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os.
|
||||
-- Uændret -> samme tabel (kaldes for hver mark hvert sekund; nye kopier giver affald og GC-hak).
|
||||
function ADFieldModel.onReadiness(field, ratio)
|
||||
local copy = copyField(field)
|
||||
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
||||
local copy = copyField(field)
|
||||
copy.stage = "ready"
|
||||
return copy
|
||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
|
||||
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
|
||||
local copy = copyField(field)
|
||||
copy.stage = "idle"
|
||||
copy.startRequested = false
|
||||
return copy
|
||||
end
|
||||
return copy
|
||||
return field
|
||||
end
|
||||
|
||||
-- Passiv mark: manuel, uden mål, venter/færdig, ingen Start nu — der sker intet uden brugeren.
|
||||
function ADFieldModel.isPassive(field)
|
||||
local plan = field.plan
|
||||
return plan.mode ~= "auto" and (plan.targetFruit == nil or plan.targetFruit == "") and not field.startRequested
|
||||
and (field.stage == "idle" or field.stage == "done")
|
||||
end
|
||||
|
||||
function ADFieldModel.shouldStart(field)
|
||||
@@ -205,14 +272,18 @@ local function freshCycle(copy)
|
||||
copy.mowWidth = nil
|
||||
copy.mowVehicleId = nil
|
||||
copy.rakeVehicleId = nil
|
||||
copy.harvestVehicleId = nil
|
||||
copy.windrowOffset = nil
|
||||
copy.careRuns = {}
|
||||
end
|
||||
|
||||
-- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig.
|
||||
function ADFieldModel.start(field, kind, needs)
|
||||
-- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den.
|
||||
function ADFieldModel.start(field, kind, needs, currentFruit)
|
||||
local copy = copyField(field)
|
||||
local stages = ADFieldModel.workStages(field.plan, kind, needs)
|
||||
copy.held = nil
|
||||
copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit)
|
||||
local stages = ADFieldModel.workStages(copy.plan, kind, needs)
|
||||
copy.stage = stages[1] or "done"
|
||||
copy.kind = kind or "mowable"
|
||||
freshCycle(copy)
|
||||
@@ -232,7 +303,7 @@ function ADFieldModel.advance(field, needs)
|
||||
local stages = ADFieldModel.workStages(field.plan, field.kind, needs)
|
||||
copy.stage = "done"
|
||||
local passed = false
|
||||
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
|
||||
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "strawCollecting", "baling", "baleCollecting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
|
||||
local rank = {}
|
||||
for index, stage in ipairs(order) do rank[stage] = index end
|
||||
for _, stage in ipairs(stages) do
|
||||
@@ -254,12 +325,14 @@ function ADFieldModel.stop(field)
|
||||
end
|
||||
copy.startRequested = false
|
||||
copy.reason = nil
|
||||
-- stoppet af spilleren: en bar mark med mål-afgrøde starter ikke selv igen (Start eller nyt mål slipper)
|
||||
copy.held = true
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling).
|
||||
-- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn.
|
||||
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true}
|
||||
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true, strawCollecting = true, baling = true}
|
||||
function ADFieldModel.isWeatherSensitive(stage, plan, kind)
|
||||
if WEATHER_STAGES[stage] then return true end
|
||||
return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay"
|
||||
@@ -297,7 +370,16 @@ function ADFieldModel.startCare(field, stage)
|
||||
end
|
||||
|
||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde.
|
||||
-- Under dette er bredden ikke læst rigtigt (spreder uden AI-markører gav 0,5 m): Courseplay laver så et
|
||||
-- kursus med rækker hver halve meter — på en 72 ha mark fryser spillet (in-game 26/9, F108).
|
||||
ADFieldModel.MIN_WORK_WIDTH = 1.5
|
||||
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
||||
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
||||
|
||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
||||
return false, "widthUnknown", {width}
|
||||
end
|
||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||
end
|
||||
@@ -376,13 +458,16 @@ function ADFieldModel.toRecord(field)
|
||||
return {
|
||||
id = field.id, stage = field.stage, mode = plan.mode, product = plan.product,
|
||||
rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "),
|
||||
dedicated = table.concat(plan.rigIds or {}, " "),
|
||||
rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime),
|
||||
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
||||
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||
mowWidth = field.mowWidth or 0,
|
||||
mowVehicle = field.mowVehicleId or "",
|
||||
rakeVehicle = field.rakeVehicleId or "",
|
||||
harvestVehicle = field.harvestVehicleId or "",
|
||||
windrowOffset = field.windrowOffset ~= nil and string.format("%.2f", field.windrowOffset) or "",
|
||||
}
|
||||
end
|
||||
@@ -405,6 +490,7 @@ function ADFieldModel.fromRecord(record)
|
||||
plan.product = PRODUCTS[record.product] and record.product or "auto"
|
||||
plan.rake = readFlag(record.rake, false)
|
||||
for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end
|
||||
for id in string.gmatch(record.dedicated or "", "%S+") do table.insert(plan.rigIds, id) end
|
||||
plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||
plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||
plan.mulch = readFlag(record.mulch, false)
|
||||
@@ -415,12 +501,16 @@ function ADFieldModel.fromRecord(record)
|
||||
plan.roll = readFlag(record.roll, false)
|
||||
plan.fertilize = readFlag(record.fertilize, true)
|
||||
plan.weed = readFlag(record.weed, true)
|
||||
plan.targetFruit = record.targetFruit or ""
|
||||
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
||||
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
||||
field.kind = readText(record.kind)
|
||||
field.lastFruit = readText(record.lastFruit)
|
||||
for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end
|
||||
field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil
|
||||
field.mowVehicleId = readText(record.mowVehicle)
|
||||
field.rakeVehicleId = readText(record.rakeVehicle)
|
||||
field.harvestVehicleId = readText(record.harvestVehicle)
|
||||
field.windrowOffset = tonumber(record.windrowOffset)
|
||||
return field
|
||||
end
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
-- AD Field Silo Config
|
||||
-- Marksilo-flowets opsætning som ren model (ny tabel ud, input muteres ikke) + flade records til XML-laget.
|
||||
|
||||
ADFieldSiloConfig = {}
|
||||
|
||||
ADFieldSiloConfig.MAX_VEHICLES = 10
|
||||
ADFieldSiloConfig.DEFAULT_MAX_VEHICLES = 2
|
||||
ADFieldSiloConfig.MIN_LITERS_CHOICES = {5000, 10000, 25000, 50000}
|
||||
ADFieldSiloConfig.DEFAULT_MIN_LITERS = 10000
|
||||
-- foderreserve: staldenes forbrug i så mange døgn bliver på gården, før overskud køres til fabrik
|
||||
ADFieldSiloConfig.RESERVE_DAYS_CHOICES = {3, 7, 14, 30}
|
||||
ADFieldSiloConfig.DEFAULT_RESERVE_DAYS = 7
|
||||
|
||||
local function copy(cfg)
|
||||
local ids = {}
|
||||
for index, id in ipairs(cfg.storeIds or {}) do ids[index] = id end
|
||||
return {enabled = cfg.enabled == true, storeIds = ids, maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters,
|
||||
toProduction = cfg.toProduction == true, reserveDays = cfg.reserveDays}
|
||||
end
|
||||
|
||||
local function clampVehicles(value)
|
||||
value = tonumber(value) or ADFieldSiloConfig.DEFAULT_MAX_VEHICLES
|
||||
return math.max(1, math.min(ADFieldSiloConfig.MAX_VEHICLES, math.floor(value)))
|
||||
end
|
||||
|
||||
local function snap(value, choices, default)
|
||||
value = tonumber(value)
|
||||
if value == nil then return default end
|
||||
local best, bestDiff = nil, nil
|
||||
for _, choice in ipairs(choices) do
|
||||
local diff = math.abs(choice - value)
|
||||
if bestDiff == nil or diff < bestDiff then best, bestDiff = choice, diff end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function snapLiters(value)
|
||||
return snap(value, ADFieldSiloConfig.MIN_LITERS_CHOICES, ADFieldSiloConfig.DEFAULT_MIN_LITERS)
|
||||
end
|
||||
|
||||
local function snapDays(value)
|
||||
return snap(value, ADFieldSiloConfig.RESERVE_DAYS_CHOICES, ADFieldSiloConfig.DEFAULT_RESERVE_DAYS)
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.new()
|
||||
return {enabled = false, storeIds = {}, maxVehicles = ADFieldSiloConfig.DEFAULT_MAX_VEHICLES, minLiters = ADFieldSiloConfig.DEFAULT_MIN_LITERS,
|
||||
toProduction = false, reserveDays = ADFieldSiloConfig.DEFAULT_RESERVE_DAYS}
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.with(cfg, changes)
|
||||
local new = copy(cfg)
|
||||
if changes.enabled ~= nil then new.enabled = changes.enabled == true end
|
||||
if changes.maxVehicles ~= nil then new.maxVehicles = clampVehicles(changes.maxVehicles) end
|
||||
if changes.minLiters ~= nil then new.minLiters = snapLiters(changes.minLiters) end
|
||||
if changes.toProduction ~= nil then new.toProduction = changes.toProduction == true end
|
||||
if changes.reserveDays ~= nil then new.reserveDays = snapDays(changes.reserveDays) end
|
||||
return new
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.hasStore(cfg, id)
|
||||
for _, storeId in ipairs(cfg.storeIds or {}) do
|
||||
if storeId == id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.toggleStore(cfg, id)
|
||||
local new = copy(cfg)
|
||||
local kept, found = {}, false
|
||||
for _, storeId in ipairs(new.storeIds) do
|
||||
if storeId == id then found = true else table.insert(kept, storeId) end
|
||||
end
|
||||
if not found then table.insert(kept, id) end
|
||||
new.storeIds = kept
|
||||
return new
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.toRecord(cfg)
|
||||
return {enabled = cfg.enabled and 1 or 0, stores = table.concat(cfg.storeIds or {}, " "),
|
||||
maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters, toProduction = cfg.toProduction and 1 or 0,
|
||||
reserveDays = cfg.reserveDays}
|
||||
end
|
||||
|
||||
function ADFieldSiloConfig.fromRecord(record)
|
||||
local cfg = ADFieldSiloConfig.new()
|
||||
if record == nil then return cfg end
|
||||
for token in string.gmatch(tostring(record.stores or ""), "%S+") do table.insert(cfg.storeIds, token) end
|
||||
cfg.enabled = tonumber(record.enabled) == 1
|
||||
cfg.maxVehicles = clampVehicles(record.maxVehicles)
|
||||
cfg.minLiters = snapLiters(record.minLiters)
|
||||
cfg.toProduction = tonumber(record.toProduction) == 1
|
||||
cfg.reserveDays = snapDays(record.reserveDays)
|
||||
return cfg
|
||||
end
|
||||
@@ -0,0 +1,195 @@
|
||||
-- AD Field Silo Planner
|
||||
-- Ren planlægning for marksilo-flowet: hvilke (silo, vare) skal hjem, hvor mange lånte vogne sendes, og til
|
||||
-- hvilket gårdlager eller (med "kør til produktion") hvilken fabrik. Ingen spil-kald; input muteres ikke.
|
||||
|
||||
ADFieldSiloPlanner = {}
|
||||
|
||||
function ADFieldSiloPlanner.key(siloId, fillType)
|
||||
return tostring(siloId) .. ":" .. tostring(fillType)
|
||||
end
|
||||
local key = ADFieldSiloPlanner.key
|
||||
|
||||
local function dist2(a, b)
|
||||
return ((a.x or 0) - (b.x or 0)) ^ 2 + ((a.z or 0) - (b.z or 0)) ^ 2
|
||||
end
|
||||
|
||||
local function freeIn(store, fillType, reserved)
|
||||
return ((store.free or {})[fillType] or 0) - (reserved[store.id .. ":" .. tostring(fillType)] or 0)
|
||||
end
|
||||
|
||||
local function anyStoreTakes(stores, fillType, minLiters, reserved)
|
||||
for _, store in ipairs(stores or {}) do
|
||||
if store.markerId ~= nil and freeIn(store, fillType, reserved) >= minLiters then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function isProduction(store)
|
||||
return store ~= nil and store.kind == "production"
|
||||
end
|
||||
|
||||
-- Nærmeste lager af den ønskede slags (fabrik/gård) med plads til hele turen; ellers det med mest plads.
|
||||
local function chooseStore(stores, silo, fillType, liters, minLiters, reserved, production)
|
||||
local best, bestD, most, mostFree = nil, nil, nil, nil
|
||||
for _, store in ipairs(stores or {}) do
|
||||
local free = freeIn(store, fillType, reserved)
|
||||
if store.markerId ~= nil and free >= minLiters and isProduction(store) == production then
|
||||
local d = dist2(store, silo)
|
||||
if free >= liters and (bestD == nil or d < bestD) then best, bestD = store, d end
|
||||
if mostFree == nil or free > mostFree then most, mostFree = store, free end
|
||||
end
|
||||
end
|
||||
-- fabrikker kun med plads til hele læsset: en fabrik tager kun det den forbruger, så en vogn med resten
|
||||
-- holder ved den i timevis (in-game 28/9: 252.000 l kartofler ved en fuld snackfabrik)
|
||||
if production then return best end
|
||||
return best or most
|
||||
end
|
||||
|
||||
-- Tom silo: en lånt vogn med last, der har stået stille ved en tom marksilo i REST_IDLE_MS, kører resten videre
|
||||
-- (AutoDrive venter ellers til vognen er fuld). s = {atSilo, stillMs, siloStock, cargo}
|
||||
ADFieldSiloPlanner.REST_IDLE_MS = 30000
|
||||
ADFieldSiloPlanner.REST_MIN_LITERS = 100
|
||||
function ADFieldSiloPlanner.restDue(s)
|
||||
return s.atSilo == true and (s.stillMs or 0) >= ADFieldSiloPlanner.REST_IDLE_MS
|
||||
and (s.cargo or 0) >= ADFieldSiloPlanner.REST_MIN_LITERS and (s.siloStock or 0) < ADFieldSiloPlanner.REST_MIN_LITERS
|
||||
end
|
||||
|
||||
-- Fuld fabrik/lager: vognen med last har holdt ved sit lager i STORE_FULL_MS, og lageret har ikke plads.
|
||||
-- s = {atStore, stillMs, cargo, storeFree}
|
||||
ADFieldSiloPlanner.STORE_FULL_MS = 60000
|
||||
function ADFieldSiloPlanner.storeFullDue(s)
|
||||
return s.atStore == true and (s.stillMs or 0) >= ADFieldSiloPlanner.STORE_FULL_MS
|
||||
and (s.cargo or 0) >= ADFieldSiloPlanner.REST_MIN_LITERS and (s.storeFree or 0) < ADFieldSiloPlanner.REST_MIN_LITERS
|
||||
end
|
||||
|
||||
-- Lager til en rest: fabrik kun med plads til hele resten, ellers gårdlager (nærmeste med plads, ellers mest plads).
|
||||
-- excludeId: lageret vognen holder fast ved (fuldt).
|
||||
function ADFieldSiloPlanner.restStore(stores, silo, fillType, liters, excludeId)
|
||||
local candidates = {}
|
||||
for _, store in ipairs(stores or {}) do
|
||||
if store.id ~= excludeId then table.insert(candidates, store) end
|
||||
end
|
||||
return chooseStore(candidates, silo, fillType, liters, 1, {}, true) or chooseStore(candidates, silo, fillType, liters, 1, {}, false)
|
||||
end
|
||||
|
||||
local function canCarry(vehicle, fillType)
|
||||
if (vehicle.capacity or 0) <= 0 then return false end
|
||||
if vehicle.cargo ~= nil and vehicle.cargo ~= fillType then return false end
|
||||
return (vehicle.carries or {})[fillType] == true
|
||||
end
|
||||
|
||||
local function nearestVehicle(vehicles, used, silo, fillType)
|
||||
local best, bestD = nil, nil
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not used[vehicle.id] and canCarry(vehicle, fillType) then
|
||||
local d = dist2(vehicle, silo)
|
||||
if bestD == nil or d < bestD then best, bestD = vehicle, d end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Foder først: mangler gården stadig foderreserve for varen (reserve − beholdning − læs på vej hjem), køres til
|
||||
-- gårdlager; ellers til fabrik. Findes kun den ene slags, bruges den.
|
||||
local function pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||||
local fillType = task.fillType
|
||||
local short = ((input.feedReserve or {})[fillType] or 0) - ((input.farmStock or {})[fillType] or 0) - (farmIncoming[fillType] or 0)
|
||||
local wantProduction = short <= 0
|
||||
return chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, wantProduction)
|
||||
or chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, not wantProduction)
|
||||
end
|
||||
|
||||
-- Høsten først: for hvert behov {fillType, count} (høstere i arbejde, der mangler tømmevogne) holdes så mange
|
||||
-- vogne der kan bære afgrøden tilbage; resten må marksilo-flowet låne. Ny liste; input muteres ikke.
|
||||
function ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
|
||||
local held = {}
|
||||
for _, need in ipairs(needs or {}) do
|
||||
local left = need.count or 0
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if left <= 0 then break end
|
||||
if not held[vehicle.id] and canCarry(vehicle, need.fillType) then
|
||||
held[vehicle.id] = true
|
||||
left = left - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local free = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not held[vehicle.id] then table.insert(free, vehicle) end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
function ADFieldSiloPlanner.plan(input)
|
||||
local minLiters = input.minLiters or 10000
|
||||
local storesById = {}
|
||||
for _, store in ipairs(input.stores or {}) do storesById[store.id] = store end
|
||||
local reserved, onWay, farmIncoming = {}, {}, {}
|
||||
for _, loan in ipairs(input.loans or {}) do
|
||||
local k = key(loan.siloId, loan.fillType)
|
||||
onWay[k] = (onWay[k] or 0) + (loan.liters or 0)
|
||||
if loan.storeId ~= nil and not isProduction(storesById[loan.storeId]) then
|
||||
farmIncoming[loan.fillType] = (farmIncoming[loan.fillType] or 0) + (loan.liters or 0)
|
||||
end
|
||||
-- læs på vej til et lager optager pladsen, til de er aflæsset (også i senere runder)
|
||||
if loan.storeId ~= nil then
|
||||
local reserveKey = loan.storeId .. ":" .. tostring(loan.fillType)
|
||||
reserved[reserveKey] = (reserved[reserveKey] or 0) + (loan.liters or 0)
|
||||
end
|
||||
end
|
||||
local status, tasks, statusByKey = {}, {}, {}
|
||||
for _, silo in ipairs(input.silos or {}) do
|
||||
for fillType, liters in pairs(silo.stock or {}) do
|
||||
if (liters or 0) >= minLiters then
|
||||
local k = key(silo.id, fillType)
|
||||
local entry = {siloId = silo.id, siloName = silo.name, fillType = fillType, liters = liters, onWay = onWay[k] or 0}
|
||||
if silo.markerId == nil then
|
||||
entry.state = "noMarker"
|
||||
elseif not anyStoreTakes(input.stores, fillType, minLiters, reserved) then
|
||||
entry.state = "noFarmStore"
|
||||
elseif (input.paused or {})[k] then
|
||||
entry.state = "paused"
|
||||
else
|
||||
entry.state = "ok"
|
||||
local rest = liters - entry.onWay
|
||||
if rest >= minLiters then table.insert(tasks, {silo = silo, fillType = fillType, rest = rest}) end
|
||||
end
|
||||
table.insert(status, entry)
|
||||
statusByKey[k] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(tasks, function(a, b)
|
||||
if a.rest ~= b.rest then return a.rest > b.rest end
|
||||
if tostring(a.silo.id) ~= tostring(b.silo.id) then return tostring(a.silo.id) < tostring(b.silo.id) end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
table.sort(status, function(a, b)
|
||||
if tostring(a.siloId) ~= tostring(b.siloId) then return tostring(a.siloId) < tostring(b.siloId) end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
local freeSlots = math.max(0, (input.maxVehicles or 2) - #(input.loans or {}))
|
||||
local used, dispatch = {}, {}
|
||||
for _, task in ipairs(tasks) do
|
||||
while freeSlots > 0 and task.rest >= minLiters do
|
||||
local vehicle = nearestVehicle(input.vehicles, used, task.silo, task.fillType)
|
||||
if vehicle == nil then break end
|
||||
local liters = math.min(vehicle.capacity, task.rest)
|
||||
local store = pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||||
if store == nil then break end
|
||||
if isProduction(store) then
|
||||
statusByKey[key(task.silo.id, task.fillType)].toProduction = true
|
||||
else
|
||||
farmIncoming[task.fillType] = (farmIncoming[task.fillType] or 0) + liters
|
||||
end
|
||||
used[vehicle.id] = true
|
||||
local reserveKey = store.id .. ":" .. tostring(task.fillType)
|
||||
reserved[reserveKey] = (reserved[reserveKey] or 0) + liters
|
||||
table.insert(dispatch, {vehicleId = vehicle.id, siloId = task.silo.id, siloMarkerId = task.silo.markerId,
|
||||
fillType = task.fillType, storeId = store.id, storeMarkerId = store.markerId, liters = liters})
|
||||
task.rest = task.rest - liters
|
||||
freeSlots = freeSlots - 1
|
||||
end
|
||||
end
|
||||
return {dispatch = dispatch, status = status, freeSlots = freeSlots}
|
||||
end
|
||||
@@ -0,0 +1,720 @@
|
||||
-- AD Field Silos
|
||||
-- Marksilo-flowet i spillet: samler marksiloer, gårdlagre og ledige vogne, lader ADFieldSiloPlanner vælge,
|
||||
-- med "kør til produktion" går overskud ud over gårdens foderreserve til gårdens fabrikker;
|
||||
-- låner vogne (AutoDrive "Hent og aflever", én tur, parkering bagefter) og frigiver dem igen.
|
||||
-- Lån ligger kun i hukommelsen; opsætningen gemmes i ADSmartPickup_fieldsilos.xml.
|
||||
|
||||
ADFieldSilos = {}
|
||||
|
||||
ADFieldSilos.PLAN_INTERVAL_MS = 60000
|
||||
ADFieldSilos.CHECK_INTERVAL_MS = 2000
|
||||
ADFieldSilos.STUCK_MS = 120000
|
||||
ADFieldSilos.PAUSE_MS = 600000
|
||||
ADFieldSilos.FAIL_MS = 30000 -- AD slukket inden for 30 s uden fremdrift = turen kunne ikke køres
|
||||
ADFieldSilos.CARGO_MIN_LITERS = 100 -- rest under dette tæller ikke som last
|
||||
-- Silo så tæt på et gårdlager er en del af gården (Farma-siloer deler lager inden for 50 m): ikke marksilo.
|
||||
-- (in-game 27/9: sukkerroer blev kørt fra én Farma-silo til nabosiloen i samme klynge)
|
||||
ADFieldSilos.FARM_RADIUS = 100
|
||||
-- Siloer højst så langt fra en nabo hører til samme klynge (Farma-rækker: 15 m mellem siloer, 18 m mellem rækker)
|
||||
ADFieldSilos.CLUSTER_LINK = 60
|
||||
ADFieldSilos.REST_SILO_DISTANCE = 40 -- m fra marksiloens markør: vognen holder ved siloen
|
||||
ADFieldSilos.FETCH_TIMEOUT_MS = 300000 -- så længe må en traktor være om at hente en puljevogn
|
||||
ADFieldSilos.MOVING_SPEED = 0.00028 -- m/ms, samme grænse som AutoDrive
|
||||
ADFieldSilos.FILE_NAME = "ADSmartPickup_fieldsilos.xml"
|
||||
ADFieldSilos.ROOT = "ADSmartPickupFieldSilos"
|
||||
ADFieldSilos.LOG_PREFIX = "ADSmartPickup: marksilo: "
|
||||
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
ADFieldSilos.lastPlan = nil
|
||||
local loans = setmetatable({}, {__mode = "k"}) -- vehicle -> {siloId, siloName, fillType, liters, startedMs, lastProgressMs, lastFill}
|
||||
local paused = {} -- key -> untilMs
|
||||
local planTimerMs, checkTimerMs, nowMs = ADFieldSilos.PLAN_INTERVAL_MS, 0, 0
|
||||
|
||||
-- ---- ren frigivelsesregel ----
|
||||
|
||||
-- vehicleState = {adActive, controlled, fill, moving}. Fremdrift = kører, eller fyldningen ændrer sig (læsser
|
||||
-- eller aflæsser). Vagten gælder kun før turen er kommet i gang — kø ved siloen/lageret er normalt.
|
||||
function ADFieldSilos.releaseReason(loan, vehicleState, now)
|
||||
if vehicleState.controlled then return "player" end
|
||||
local progressing = vehicleState.moving or (vehicleState.fill or 0) ~= (loan.lastFill or 0)
|
||||
local hasProgressed = loan.hasProgressed or progressing
|
||||
local age = now - (loan.startedMs or 0)
|
||||
if not vehicleState.adActive then
|
||||
return (not hasProgressed and age < ADFieldSilos.FAIL_MS) and "failed" or "done"
|
||||
end
|
||||
if hasProgressed then return nil end
|
||||
if age > ADFieldSilos.STUCK_MS then return "stuck" end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ---- opsætning ----
|
||||
|
||||
function ADFieldSilos.setConfig(cfg)
|
||||
local wasEnabled = ADFieldSilos.config.enabled
|
||||
ADFieldSilos.config = cfg
|
||||
if wasEnabled and not cfg.enabled then ADFieldSilos.stopAll(ADFieldSilos.adEnv) end
|
||||
planTimerMs = ADFieldSilos.PLAN_INTERVAL_MS -- planlæg straks ved næste tick
|
||||
end
|
||||
|
||||
function ADFieldSilos.isLoaned(vehicle)
|
||||
return loans[vehicle] ~= nil
|
||||
end
|
||||
|
||||
function ADFieldSilos.loanCount()
|
||||
local n = 0
|
||||
for _ in pairs(loans) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
-- ---- spil-input ----
|
||||
|
||||
local function farmId()
|
||||
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||||
end
|
||||
|
||||
local function position(node)
|
||||
if node == nil then return 0, 0 end
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function siloStock(placeable)
|
||||
local stock = {}
|
||||
local spec = placeable ~= nil and placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
for fillType, level in pairs(storage.fillLevels or {}) do
|
||||
if (level or 0) > 0 then stock[fillType] = (stock[fillType] or 0) + level end
|
||||
end
|
||||
end
|
||||
return stock
|
||||
end
|
||||
|
||||
local function siloFree(placeable)
|
||||
local free = {}
|
||||
local spec = placeable ~= nil and placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
for fillType in pairs(storage.fillTypes or {}) do
|
||||
local amount = storage.getFreeCapacity ~= nil and (storage:getFreeCapacity(fillType) or 0) or 0
|
||||
free[fillType] = (free[fillType] or 0) + amount
|
||||
end
|
||||
end
|
||||
return free
|
||||
end
|
||||
|
||||
local function firstMarker(building, role)
|
||||
local markers = ADBuildings.getMarkers(building, role)
|
||||
return markers[1] ~= nil and markers[1].id or nil
|
||||
end
|
||||
|
||||
-- Bygninger der er kilde, mål eller lager i et flow: dem tømmer marksilo-flowet ikke (de hører til den drift).
|
||||
local function runBuildingIds()
|
||||
local ids = {}
|
||||
local store = ADRunsController ~= nil and ADRunsController.store or nil
|
||||
for _, run in ipairs(store ~= nil and store.runs or {}) do
|
||||
for _, list in ipairs({run.sourceBuildingIds or {}, run.targetBuildingIds or {}, run.storeBuildingIds or {}}) do
|
||||
for _, id in ipairs(list) do ids[id] = true end
|
||||
end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- Alle siloer i samme klynge som et gårdlager (kæde af naboer ≤ CLUSTER_LINK): gårdens, aldrig marksiloer.
|
||||
-- (in-game 27/9: gårdens Farma-klynge er ~300 m lang; siloer over 100 m fra et valgt lager blev marksiloer)
|
||||
local function farmCluster(allSilos, storeIds)
|
||||
local farm, queue = {}, {}
|
||||
for _, silo in ipairs(allSilos) do
|
||||
if storeIds[silo.id] then farm[silo.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
local link2 = ADFieldSilos.CLUSTER_LINK ^ 2
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue)
|
||||
for _, silo in ipairs(allSilos) do
|
||||
if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= link2 then
|
||||
farm[silo.id] = true
|
||||
table.insert(queue, silo)
|
||||
end
|
||||
end
|
||||
end
|
||||
return farm
|
||||
end
|
||||
|
||||
local function nearStore(stores, x, z)
|
||||
for _, store in ipairs(stores) do
|
||||
if (store.x - x) ^ 2 + (store.z - z) ^ 2 <= ADFieldSilos.FARM_RADIUS ^ 2 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Fabrikkens input-varer (dem aflæsningen tager imod) med fri plads i dens lager.
|
||||
local function factoryFree(placeable)
|
||||
local spec = placeable ~= nil and placeable.spec_productionPoint or nil
|
||||
local point = spec ~= nil and spec.productionPoint or nil
|
||||
local storage = point ~= nil and point.storage or nil
|
||||
if storage == nil or storage.getFreeCapacity == nil then return {} end
|
||||
local inputs = {}
|
||||
for fillType in pairs(point.inputFillTypeIds or {}) do inputs[fillType] = true end
|
||||
for _, production in ipairs(point.productions or {}) do
|
||||
for _, input in ipairs(production.inputs or {}) do inputs[input.type] = true end
|
||||
end
|
||||
local free = {}
|
||||
for fillType in pairs(inputs) do free[fillType] = storage:getFreeCapacity(fillType) or 0 end
|
||||
return free
|
||||
end
|
||||
|
||||
-- "Kør til produktion": gårdens fabrikker med aflæsningsmarkør bliver mål ved siden af gårdlagrene.
|
||||
local function addFactories(stores, buildings)
|
||||
for _, building in ipairs(buildings) do
|
||||
local markerId = building.kind == "factory" and firstMarker(building, "unload") or nil
|
||||
if markerId ~= nil then
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(stores, {id = building.id, name = building.name, kind = "production", markerId = markerId,
|
||||
x = x, z = z, free = factoryFree(building.placeable)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Foderreserve (staldenes døgnforbrug × døgn) og gårdlagrenes beholdning pr. vare.
|
||||
function ADFieldSilos.collectFeed(stores, byId, fid, reserveDays)
|
||||
local reserve, stock = {}, {}
|
||||
local rates = ADSources ~= nil and ADSources.getFarmRates ~= nil and ADSources.getFarmRates(ADFieldSilos.adEnv, fid, true) or {}
|
||||
for fillType, rate in pairs(rates) do reserve[fillType] = (rate.consumption or 0) * reserveDays end
|
||||
local counted = {} -- Farma-siloer i samme klynge deler lager: tæl hvert lager én gang
|
||||
for _, store in ipairs(stores) do
|
||||
local building = byId[store.id]
|
||||
local spec = store.kind ~= "production" and building ~= nil and building.placeable ~= nil and building.placeable.spec_silo or nil
|
||||
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
|
||||
if not counted[storage] then
|
||||
counted[storage] = true
|
||||
for fillType, level in pairs(storage.fillLevels or {}) do stock[fillType] = (stock[fillType] or 0) + (level or 0) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return reserve, stock
|
||||
end
|
||||
|
||||
-- Gårdlagre fra opsætningen + marksiloer (alle markers marksiloer, uden dubletter og uden gårdlagre).
|
||||
function ADFieldSilos.collectBuildings(adEnv)
|
||||
local fid = farmId()
|
||||
local stores, byId, allSilos, storeIds = {}, {}, {}, {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, fid)) do
|
||||
byId[building.id] = building
|
||||
if building.kind == "silo" then
|
||||
local sx, sz = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(allSilos, {id = building.id, x = sx, z = sz})
|
||||
if ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then storeIds[building.id] = true end
|
||||
end
|
||||
if building.kind == "silo" and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
table.insert(stores, {id = building.id, name = building.name, markerId = firstMarker(building, "unload"),
|
||||
x = x, z = z, free = siloFree(building.placeable)})
|
||||
end
|
||||
end
|
||||
if ADFieldSilos.config.toProduction then addFactories(stores, ADBuildings.list(adEnv, fid)) end
|
||||
local silos, seen = {}, runBuildingIds()
|
||||
for id in pairs(farmCluster(allSilos, storeIds)) do seen[id] = true end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, fid, info)) do
|
||||
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
|
||||
if not seen[building.id] and not ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) and not nearStore(stores, x, z) then
|
||||
seen[building.id] = true
|
||||
-- marknummeret først: navnet skæres af i fanen, og mange siloer hedder ens ("Farma 500 …")
|
||||
-- marknummeret først (medmindre navnet allerede starter med det, fx "F22 marksilo")
|
||||
local siloName = tostring(building.name)
|
||||
if not siloName:match("^F" .. tostring(info.id) .. "%f[%D]") then siloName = string.format("F%s %s", tostring(info.id), siloName) end
|
||||
table.insert(silos, {id = building.id, name = siloName, markerId = firstMarker(building, "load"),
|
||||
x = x, z = z, stock = siloStock(building.placeable)})
|
||||
end
|
||||
end
|
||||
end
|
||||
return silos, stores, byId
|
||||
end
|
||||
|
||||
-- Marksiloernes placeables som sæt (til at udelukke dem som tømmested — de er til markens høst).
|
||||
function ADFieldSilos.fieldSiloPlaceableSet(adEnv)
|
||||
local set = {}
|
||||
local silos, _, byId = ADFieldSilos.collectBuildings(adEnv)
|
||||
for _, silo in ipairs(silos or {}) do
|
||||
local building = byId[silo.id]
|
||||
if building ~= nil and building.placeable ~= nil then set[building.placeable] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
local function trailerInfo(adEnv, vehicle)
|
||||
local capacity, carries, cargo = 0, {}, nil
|
||||
for _, unit in ipairs(adEnv.AutoDrive.getAllDischargeableUnits(vehicle) or {}) do
|
||||
local object, index = unit.object, unit.fillUnitIndex
|
||||
if object ~= nil and object.getFillUnitCapacity ~= nil then
|
||||
capacity = capacity + (object:getFillUnitCapacity(index) or 0)
|
||||
for fillType in pairs(object.getFillUnitSupportedFillTypes ~= nil and object:getFillUnitSupportedFillTypes(index) or {}) do
|
||||
carries[fillType] = true
|
||||
end
|
||||
if (object:getFillUnitFillLevel(index) or 0) >= ADFieldSilos.CARGO_MIN_LITERS then cargo = object:getFillUnitFillType(index) end
|
||||
end
|
||||
end
|
||||
return capacity, carries, cargo
|
||||
end
|
||||
|
||||
local function fillLevel(adEnv, vehicle)
|
||||
local ok, level = pcall(function() return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle))) end)
|
||||
return ok and tonumber(level) or 0
|
||||
end
|
||||
|
||||
function ADFieldSilos.collectVehicles(adEnv)
|
||||
local vehicles, byId = {}, {}
|
||||
-- hvorfor tømmevogne ikke kan lånes (til fanens statuslinje)
|
||||
local why = {notEnabled = 0, busy = 0, controlled = 0, noPark = 0}
|
||||
local rigs = ADFieldWork.getRigs(adEnv)
|
||||
for _, rig in ipairs(rigs) do
|
||||
-- uden AD-parkeringspunkt bliver vognen holdende på lagerets markør og spærrer for de næste
|
||||
local canPark = ADFieldWork.parkMarkerId == nil or ADFieldWork.parkMarkerId(adEnv, rig.vehicle) ~= nil
|
||||
if rig.role == "unloader" then
|
||||
if not rig.enabled then why.notEnabled = why.notEnabled + 1
|
||||
elseif rig.controlled then why.controlled = why.controlled + 1
|
||||
elseif rig.busy then why.busy = why.busy + 1
|
||||
elseif not canPark then why.noPark = why.noPark + 1 end
|
||||
end
|
||||
if rig.role == "unloader" and rig.enabled and not rig.busy and not rig.controlled and canPark then
|
||||
local capacity, carries, cargo = trailerInfo(adEnv, rig.vehicle)
|
||||
table.insert(vehicles, {id = rig.id, x = rig.x, z = rig.z, capacity = capacity, carries = carries, cargo = cargo})
|
||||
byId[rig.id] = rig.vehicle
|
||||
end
|
||||
end
|
||||
-- puljevogne: en ledig solo-traktor henter vognen først (maskinpulje etape 3)
|
||||
local poolById = {}
|
||||
if ADPool ~= nil then
|
||||
local okPool, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
||||
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
||||
end)
|
||||
if okPool then
|
||||
for _, pair in ipairs(ADPool.pairAll("unloader", implements, tractors, ADPool.blocked(ADFieldJobs.now or 0))) do
|
||||
local entryId = "pool:" .. tostring(pair.implement.id)
|
||||
table.insert(vehicles, {id = entryId, x = pair.implement.x, z = pair.implement.z,
|
||||
capacity = pair.implement.capacity or 0, carries = pair.implement.carries or {}})
|
||||
byId[entryId] = pair.tractor.vehicle
|
||||
poolById[entryId] = pair
|
||||
end
|
||||
else
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "maskinpuljen kunne ikke læses: %s", tostring(implements))
|
||||
end
|
||||
end
|
||||
local ok, needs = pcall(ADFieldSilos.harvestNeeds, adEnv, rigs)
|
||||
if ok and #needs > 0 then
|
||||
local before = #vehicles
|
||||
vehicles = ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
|
||||
why.harvest = before - #vehicles
|
||||
elseif not ok then
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "høstreserve kunne ikke beregnes: %s", tostring(needs))
|
||||
end
|
||||
why.eligible = #vehicles
|
||||
ADFieldSilos.lastVehicleReasons = why
|
||||
return vehicles, byId, poolById
|
||||
end
|
||||
|
||||
-- Høsten først: pr. høster der arbejder (CP kører) holdes dens planlagte antal tømmevogne tilbage, minus dem
|
||||
-- markarbejdet allerede har sendt til marken. (in-game 28/9: marksilo-flowet havde lånt begge kartoffelvogne,
|
||||
-- og ingen kom til roeoptagerne.)
|
||||
function ADFieldSilos.harvestNeeds(adEnv, rigs)
|
||||
local assignments = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.assignments or {}
|
||||
local sent = {}
|
||||
for _, assignment in pairs(assignments) do
|
||||
if assignment.role == "unloader" and assignment.fieldId ~= nil then sent[assignment.fieldId] = (sent[assignment.fieldId] or 0) + 1 end
|
||||
end
|
||||
local needs = {}
|
||||
for _, rig in ipairs(rigs) do
|
||||
if rig.role == "harvester" and ADCpBridge.isWorking(rig.vehicle) then
|
||||
local assignment = assignments[rig.id]
|
||||
local fieldId = assignment ~= nil and assignment.fieldId or nil
|
||||
local field = fieldId ~= nil and ADFieldJobs.getField(fieldId) or nil
|
||||
local info = fieldId ~= nil and ADFields.getInfo(fieldId) or nil
|
||||
local count = field ~= nil and ADFieldModel.maxHelpers("harvesting", field.plan) or 1
|
||||
count = count - (fieldId ~= nil and (sent[fieldId] or 0) or 0)
|
||||
local fillTypes = {}
|
||||
if info ~= nil and info.harvestFillType ~= nil then
|
||||
fillTypes[info.harvestFillType] = true
|
||||
else
|
||||
for fruitIndex in pairs(rig.fruits or {}) do
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
|
||||
if fruit ~= nil and fruit.fillType ~= nil then fillTypes[fruit.fillType.index] = true end
|
||||
end
|
||||
end
|
||||
for fillType in pairs(fillTypes) do
|
||||
if count > 0 then table.insert(needs, {fillType = fillType, count = count}) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return needs
|
||||
end
|
||||
|
||||
-- ---- lån ----
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
local desc = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||||
return desc ~= nil and (desc.title or desc.name) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function dispatchOne(adEnv, order, vehicle, silosById, storesById, isPool)
|
||||
local module = vehicle.ad.stateModule
|
||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||
module:setFirstMarkerByWayPointId(order.siloMarkerId)
|
||||
module:setSecondMarkerByWayPointId(order.storeMarkerId)
|
||||
module.selectedFillTypes = {order.fillType}
|
||||
module:setFillType(order.fillType)
|
||||
module.loopCounter = 1
|
||||
if module.setLoopsDone ~= nil then module:setLoopsDone(0) end
|
||||
-- puljevogn: ingen AD-parkering — ved turens slut sætter returnHome vognen af og kører traktoren hjem
|
||||
-- (med AD-parkering kørte den først hjem med vognen og så tilbage efter den)
|
||||
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
|
||||
-- rotation fra: ellers kan modens egne pickup-hooks sende vognen til en anden silo i mappen
|
||||
if ADRunsController ~= nil and ADRunsController.setSetting ~= nil then
|
||||
ADRunsController.setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_NONE)
|
||||
end
|
||||
module:raiseDirtyFlag()
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_PICKUPANDDELIVER, order.siloMarkerId, order.storeMarkerId, order.fillType)
|
||||
local silo, store = silosById[order.siloId], storesById[order.storeId]
|
||||
loans[vehicle] = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId, fillType = order.fillType,
|
||||
liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle)}
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter %d l %s i %s -> %s", tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId),
|
||||
tostring(store ~= nil and store.name or order.storeId))
|
||||
end
|
||||
|
||||
-- Puljevogn: traktoren henter vognen ved "Connect X" og kører derefter turen som en almindelig tømmevogn.
|
||||
local function dispatchPool(adEnv, order, pair, silosById, storesById)
|
||||
local tractor = pair.tractor.vehicle
|
||||
local silo = silosById[order.siloId]
|
||||
local owner = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId,
|
||||
fillType = order.fillType, liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = 0, fetching = true}
|
||||
loans[tractor] = owner
|
||||
ADHitch.fetch(adEnv, tractor, pair.implement.connectId, function(_, ok, detail)
|
||||
ADPool.release(owner)
|
||||
if loans[tractor] ~= owner then return end -- lånet blev afbrudt imens (Stop alt / flow slået fra)
|
||||
if not ok then
|
||||
loans[tractor] = nil
|
||||
local blamed = ADPool.failureBlames(detail) == "tractor" and pair.tractor.id or pair.implement.id
|
||||
ADPool.quarantine(blamed, ADFieldJobs.now or 0)
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke hente puljevognen '%s' (%s) — prøver en anden om lidt",
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(pair.implement.name), tostring(detail))
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, tractor)
|
||||
if park ~= nil then ADFieldWork.startAd(adEnv, tractor, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
return
|
||||
end
|
||||
pcall(ADHitch.foldForTransport, tractor)
|
||||
local okDispatch, err = pcall(dispatchOne, adEnv, order, tractor, silosById, storesById, true)
|
||||
if not okDispatch then
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke starte turen med puljevognen: %s", tostring(ADRuns.vehicleLabel(tractor)), tostring(err))
|
||||
ADFieldSilos.releaseVehicle(adEnv, tractor, "failed")
|
||||
end
|
||||
end)
|
||||
-- reservationen først når hentningen er sat i gang (fejler fetch, er der intet at frigive)
|
||||
ADPool.reserve(owner, {pair.implement.id, pair.tractor.id})
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter puljevognen '%s' til %d l %s i %s", tostring(ADRuns.vehicleLabel(tractor)),
|
||||
tostring(pair.implement.name), math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId))
|
||||
end
|
||||
|
||||
local function release(adEnv, vehicle, reason)
|
||||
local loan = loans[vehicle]
|
||||
loans[vehicle] = nil
|
||||
if loan == nil then return end
|
||||
if loan.fetching then
|
||||
-- henter stadig puljevognen: afbryd koblingen og frigiv reservationen
|
||||
if ADHitch ~= nil then ADHitch.cancel(vehicle) end
|
||||
if ADPool ~= nil then ADPool.release(loan) end
|
||||
end
|
||||
if reason == "stuck" or reason == "failed" then
|
||||
local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType)
|
||||
paused[k] = nowMs + ADFieldSilos.PAUSE_MS
|
||||
pcall(vehicle.stopAutoDrive, vehicle)
|
||||
local why = reason == "stuck" and "kom ikke i gang på 2 min — stoppet" or "AutoDrive stoppede turen med det samme (rute/læssested?)"
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' ved %s (%s): %s; siloen springes over i 10 min",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(loan.siloName), fillTypeName(loan.fillType), why)
|
||||
elseif reason == "stopped" then
|
||||
pcall(vehicle.stopAutoDrive, vehicle)
|
||||
end
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' frigivet (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(reason))
|
||||
-- puljevogn bag traktoren: sæt den af på sin plads og kør hjem (ikke når spilleren har overtaget)
|
||||
if reason ~= "player" and ADHitch ~= nil then
|
||||
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
|
||||
if not (okHome and pooled) and loan.fetching then
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldSilos.releaseVehicle(adEnv, vehicle, reason)
|
||||
release(adEnv, vehicle, reason)
|
||||
end
|
||||
|
||||
function ADFieldSilos.stopAll(adEnv)
|
||||
local list = {}
|
||||
for vehicle in pairs(loans) do table.insert(list, vehicle) end
|
||||
for _, vehicle in ipairs(list) do release(adEnv, vehicle, "stopped") end
|
||||
end
|
||||
|
||||
-- Efter genindlæsning genoptager AutoDrive selv "Hent og aflever" fra marksiloerne, men lånene er glemt: sådanne
|
||||
-- vogne overtages igen som lån (tæller i "højst x ude", og tom silo-vagten gælder).
|
||||
function ADFieldSilos.adoptOrphans(adEnv, silos)
|
||||
local byMarker = {}
|
||||
for _, silo in ipairs(silos or {}) do
|
||||
if silo.markerId ~= nil then byMarker[silo.markerId] = silo end
|
||||
end
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if loans[vehicle] == nil and module ~= nil and module:isActive() and module:getMode() == adEnv.AutoDrive.MODE_PICKUPANDDELIVER then
|
||||
local first = module:getFirstMarker()
|
||||
local silo = first ~= nil and byMarker[first.id] or nil
|
||||
if silo ~= nil then
|
||||
local fillType = module.getFillType ~= nil and module:getFillType() or nil
|
||||
local second = module.getSecondMarker ~= nil and module:getSecondMarker() or nil
|
||||
local storeId = nil
|
||||
for _, store in ipairs(ADFieldSilos.lastStores or {}) do
|
||||
if second ~= nil and store.markerId == second.id then storeId = store.id end
|
||||
end
|
||||
loans[vehicle] = {siloId = silo.id, storeId = storeId, siloName = silo.name, fillType = fillType, liters = 0, startedMs = nowMs,
|
||||
lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle), hasProgressed = true, adopted = true}
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' overtaget igen (AutoDrive kører fra %s efter genindlæsning)",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(silo.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Tom silo-vagt: en lånt vogn med last, der står stille ved en tom marksilo, kører resten videre — til en fabrik
|
||||
-- med plads til hele resten, ellers til et gårdlager (AutoDrive venter ellers til vognen er fuld).
|
||||
local warnedNoRestStore = {}
|
||||
local function isNear(adEnv, vehicle, markerId)
|
||||
local wayPoint = markerId ~= nil and adEnv.ADGraphManager:getWayPointById(markerId) or nil
|
||||
if wayPoint == nil or vehicle.rootNode == nil then return false end
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return (x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2 <= ADFieldSilos.REST_SILO_DISTANCE ^ 2
|
||||
end
|
||||
|
||||
local function findById(list, id)
|
||||
for _, item in ipairs(list or {}) do
|
||||
if item.id == id then return item end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function checkRest(adEnv, vehicle, loan, moving, fill)
|
||||
if loan.fetching then return end
|
||||
local silo = findById(ADFieldSilos.lastSilos, loan.siloId)
|
||||
if silo == nil then return end
|
||||
if moving then loan.stillSince = nil return end
|
||||
loan.stillSince = loan.stillSince or nowMs
|
||||
local stillMs = nowMs - loan.stillSince
|
||||
local fullStore = nil
|
||||
if not loan.delivering and isNear(adEnv, vehicle, silo.markerId) then
|
||||
-- tom marksilo med last på
|
||||
local state = {atSilo = true, stillMs = stillMs, siloStock = (silo.stock or {})[loan.fillType] or 0, cargo = fill}
|
||||
if not ADFieldSiloPlanner.restDue(state) then return end
|
||||
else
|
||||
-- fuldt lager (fabrik der kun tager sit forbrug) med last på
|
||||
local current = findById(ADFieldSilos.lastStores, loan.storeId)
|
||||
if current == nil or not isNear(adEnv, vehicle, current.markerId) then return end
|
||||
local state = {atStore = true, stillMs = stillMs, cargo = fill, storeFree = (current.free or {})[loan.fillType] or 0}
|
||||
if not ADFieldSiloPlanner.storeFullDue(state) then return end
|
||||
fullStore = current
|
||||
end
|
||||
local store = ADFieldSiloPlanner.restStore(ADFieldSilos.lastStores, silo, loan.fillType, fill, fullStore ~= nil and fullStore.id or nil)
|
||||
if store == nil then
|
||||
if not warnedNoRestStore[vehicle] then
|
||||
warnedNoRestStore[vehicle] = true
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' har %d l %s fra tom %s, men intet lager har plads",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(fill), fillTypeName(loan.fillType), tostring(silo.name))
|
||||
end
|
||||
return
|
||||
end
|
||||
warnedNoRestStore[vehicle] = nil
|
||||
loan.delivering, loan.storeId, loan.stillSince = true, store.id, nil
|
||||
loan.startedMs, loan.hasProgressed = nowMs, true
|
||||
local isPool = ADHitch ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, store.markerId, nil, loan.fillType)
|
||||
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s': %s — kører resten (%d l %s) til %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), fullStore ~= nil and (tostring(fullStore.name) .. " er fuld") or (tostring(silo.name) .. " er tom"),
|
||||
math.floor(fill), fillTypeName(loan.fillType), tostring(store.name))
|
||||
end
|
||||
|
||||
local function checkLoans(adEnv)
|
||||
local list = {}
|
||||
for vehicle in pairs(loans) do table.insert(list, vehicle) end
|
||||
for _, vehicle in ipairs(list) do
|
||||
if vehicle.isDeleted then
|
||||
loans[vehicle] = nil -- solgt/slettet mens den var lånt
|
||||
else
|
||||
local ok, err = pcall(function()
|
||||
local loan = loans[vehicle]
|
||||
-- henter puljevognen: AD holder stille under koblingen — ikke "færdig"; men spilleren kan overtage,
|
||||
-- og en hentning der ikke bliver færdig (trafik, rute), opgives
|
||||
if loan.fetching then
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false
|
||||
if controlled then
|
||||
release(adEnv, vehicle, "player")
|
||||
elseif nowMs - (loan.startedMs or nowMs) > ADFieldSilos.FETCH_TIMEOUT_MS then
|
||||
release(adEnv, vehicle, "stuck")
|
||||
end
|
||||
return
|
||||
end
|
||||
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local fill = fillLevel(adEnv, vehicle)
|
||||
local vehicleState = {
|
||||
adActive = stateModule ~= nil and stateModule:isActive(),
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false,
|
||||
fill = fill, moving = (vehicle.lastSpeedReal or 0) >= ADFieldSilos.MOVING_SPEED,
|
||||
}
|
||||
checkRest(adEnv, vehicle, loan, vehicleState.moving, fill)
|
||||
if loans[vehicle] ~= loan then return end
|
||||
vehicleState.adActive = stateModule ~= nil and stateModule:isActive()
|
||||
local reason = ADFieldSilos.releaseReason(loan, vehicleState, nowMs)
|
||||
if vehicleState.moving or fill ~= (loan.lastFill or 0) then loan.hasProgressed = true end
|
||||
loan.lastFill = fill
|
||||
if reason ~= nil then release(adEnv, vehicle, reason) end
|
||||
end)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "lån opgivet (fejl): %s", tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function currentPaused()
|
||||
local result = {}
|
||||
for k, untilMs in pairs(paused) do
|
||||
if untilMs > nowMs then result[k] = true else paused[k] = nil end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function loanList()
|
||||
local list = {}
|
||||
for _, loan in pairs(loans) do table.insert(list, {siloId = loan.siloId, storeId = loan.storeId, fillType = loan.fillType, liters = loan.liters}) end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Rute fra vognen til siloen og videre til lageret (samme opslag som Wait-punkterne); én advarsel pr. kombination.
|
||||
local warnedRoute = {}
|
||||
local function hasRoutes(adEnv, vehicle, order)
|
||||
if ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil then return true end
|
||||
local ok = ADUnloadWait.hasRouteTo(adEnv, vehicle, order.siloMarkerId) and ADUnloadWait.hasRouteTo(adEnv, vehicle, order.storeMarkerId)
|
||||
local warnKey = tostring(order.vehicleId) .. ">" .. tostring(order.siloMarkerId)
|
||||
if not ok and not warnedRoute[warnKey] then
|
||||
warnedRoute[warnKey] = true
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "ingen AutoDrive-rute for '%s' til marksilo-markør %s eller gårdlager-markør %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(order.siloMarkerId), tostring(order.storeMarkerId))
|
||||
end
|
||||
return ok
|
||||
end
|
||||
|
||||
function ADFieldSilos.planNow(adEnv)
|
||||
local cfg = ADFieldSilos.config
|
||||
local silos, stores, byId = ADFieldSilos.collectBuildings(adEnv)
|
||||
ADFieldSilos.lastSilos, ADFieldSilos.lastStores = silos, stores
|
||||
if cfg.enabled then
|
||||
local okAdopt, errAdopt = pcall(ADFieldSilos.adoptOrphans, adEnv, silos)
|
||||
if not okAdopt then Logging.warning(ADFieldSilos.LOG_PREFIX .. "overtagelse efter genindlæsning fejlede: %s", tostring(errAdopt)) end
|
||||
end
|
||||
local vehicles, vehiclesById, poolById = ADFieldSilos.collectVehicles(adEnv)
|
||||
local feedReserve, farmStock = nil, nil
|
||||
if cfg.toProduction then
|
||||
local ok, reserve, stock = pcall(ADFieldSilos.collectFeed, stores, byId, farmId(), cfg.reserveDays)
|
||||
if ok then feedReserve, farmStock = reserve, stock
|
||||
else Logging.warning(ADFieldSilos.LOG_PREFIX .. "foderreserve kunne ikke beregnes: %s", tostring(reserve)) end
|
||||
end
|
||||
local plan = ADFieldSiloPlanner.plan({silos = silos, stores = stores, loans = loanList(), vehicles = vehicles,
|
||||
paused = currentPaused(), maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters,
|
||||
feedReserve = feedReserve, farmStock = farmStock})
|
||||
ADFieldSilos.lastFeed = {reserve = feedReserve, stock = farmStock}
|
||||
ADFieldSilos.lastPlan = plan
|
||||
local paused = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused
|
||||
if not cfg.enabled or paused then return plan end
|
||||
local silosById, storesById = {}, {}
|
||||
for _, silo in ipairs(silos) do silosById[silo.id] = silo end
|
||||
for _, store in ipairs(stores) do storesById[store.id] = store end
|
||||
for _, order in ipairs(plan.dispatch) do
|
||||
local vehicle = vehiclesById[order.vehicleId]
|
||||
if vehicle ~= nil and not hasRoutes(adEnv, vehicle, order) then
|
||||
vehicle = nil
|
||||
end
|
||||
local pair = poolById ~= nil and poolById[order.vehicleId] or nil
|
||||
if vehicle ~= nil and pair ~= nil then
|
||||
local ok, err = pcall(dispatchPool, adEnv, order, pair, silosById, storesById)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s' efter puljevogn: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
elseif vehicle ~= nil then
|
||||
local ok, err = pcall(dispatchOne, adEnv, order, vehicle, silosById, storesById)
|
||||
if not ok then
|
||||
loans[vehicle] = nil
|
||||
Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s': %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
return plan
|
||||
end
|
||||
|
||||
function ADFieldSilos.tick(adEnv, dt)
|
||||
if adEnv == nil then return end
|
||||
ADFieldSilos.adEnv = adEnv
|
||||
nowMs = nowMs + (dt or 0)
|
||||
checkTimerMs = checkTimerMs + (dt or 0)
|
||||
if checkTimerMs >= ADFieldSilos.CHECK_INTERVAL_MS then
|
||||
checkTimerMs = 0
|
||||
checkLoans(adEnv)
|
||||
end
|
||||
planTimerMs = planTimerMs + (dt or 0)
|
||||
if planTimerMs >= ADFieldSilos.PLAN_INTERVAL_MS then
|
||||
planTimerMs = 0
|
||||
ADFieldSilos.planNow(adEnv)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- gem / indlæs (samme sti-opslag som runs) ----
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADFieldSilos.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADFieldSilos.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupFieldSilos", path, ADFieldSilos.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local record = ADFieldSiloConfig.toRecord(ADFieldSilos.config)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#enabled", record.enabled)
|
||||
xmlFile:setString(ADFieldSilos.ROOT .. "#stores", record.stores)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#maxVehicles", record.maxVehicles)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#minLiters", record.minLiters)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#toProduction", record.toProduction)
|
||||
xmlFile:setInt(ADFieldSilos.ROOT .. "#reserveDays", record.reserveDays)
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldSilos.load()
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
return
|
||||
end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupFieldSilos", path)
|
||||
if xmlFile == nil then
|
||||
ADFieldSilos.config = ADFieldSiloConfig.new()
|
||||
return
|
||||
end
|
||||
ADFieldSilos.config = ADFieldSiloConfig.fromRecord({
|
||||
enabled = xmlFile:getInt(ADFieldSilos.ROOT .. "#enabled"),
|
||||
stores = xmlFile:getString(ADFieldSilos.ROOT .. "#stores"),
|
||||
maxVehicles = xmlFile:getInt(ADFieldSilos.ROOT .. "#maxVehicles"),
|
||||
minLiters = xmlFile:getInt(ADFieldSilos.ROOT .. "#minLiters"),
|
||||
toProduction = xmlFile:getInt(ADFieldSilos.ROOT .. "#toProduction"),
|
||||
reserveDays = xmlFile:getInt(ADFieldSilos.ROOT .. "#reserveDays"),
|
||||
})
|
||||
xmlFile:delete()
|
||||
end
|
||||
@@ -11,8 +11,14 @@ ADFieldStorage.FIELD_TYPES = {
|
||||
id = "Int", stage = "String", mode = "String", product = "String", rake = "Int", store = "String",
|
||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||
kind = "String", lastFruit = "String", care = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", windrowOffset = "String",
|
||||
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
|
||||
dedicated = "String",
|
||||
}
|
||||
|
||||
-- Kursus-registret (ADCourseCache): hvilken mark/bredde/form/indstillinger hver maskines CP-kursus er lavet til.
|
||||
ADFieldStorage.COURSE_TYPES = {
|
||||
vehicle = "String", field = "Int", width = "Float", sig = "String", settings = "String", waypoints = "Int",
|
||||
}
|
||||
|
||||
function ADFieldStorage.getPath()
|
||||
@@ -36,6 +42,21 @@ function ADFieldStorage.readRecord(xmlFile, key)
|
||||
return record
|
||||
end
|
||||
|
||||
function ADFieldStorage.writeCourseRecord(xmlFile, key, record)
|
||||
for name, kind in pairs(ADFieldStorage.COURSE_TYPES) do
|
||||
local value = record[name]
|
||||
if value ~= nil then xmlFile["set" .. kind](xmlFile, key .. "#" .. name, value) end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldStorage.readCourseRecord(xmlFile, key)
|
||||
local record = {}
|
||||
for name, kind in pairs(ADFieldStorage.COURSE_TYPES) do
|
||||
record[name] = xmlFile["get" .. kind](xmlFile, key .. "#" .. name)
|
||||
end
|
||||
return record
|
||||
end
|
||||
|
||||
function ADFieldStorage.save(state)
|
||||
local path = ADFieldStorage.getPath()
|
||||
if path == nil then return false end
|
||||
@@ -46,22 +67,36 @@ function ADFieldStorage.save(state)
|
||||
ADFieldStorage.writeRecord(xmlFile, string.format("%s.field(%d)", ADFieldStorage.ROOT, index), ADFieldModel.toRecord(field))
|
||||
index = index + 1
|
||||
end
|
||||
xmlFile:setString(ADFieldStorage.ROOT .. "#fleetMode", state.fleetMode or "all")
|
||||
xmlFile:setInt(ADFieldStorage.ROOT .. "#paused", state.paused == true and 1 or 0)
|
||||
local vehicleIndex = 0
|
||||
for vehicleId in pairs(state.fleet) do
|
||||
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
|
||||
vehicleIndex = vehicleIndex + 1
|
||||
end
|
||||
local manualIndex = 0
|
||||
for vehicleId in pairs(state.manual or {}) do
|
||||
xmlFile:setString(string.format("%s.manual.vehicle(%d)#id", ADFieldStorage.ROOT, manualIndex), vehicleId)
|
||||
manualIndex = manualIndex + 1
|
||||
end
|
||||
local courseIndex = 0
|
||||
for _, record in pairs(state.courseRecords or {}) do
|
||||
ADFieldStorage.writeCourseRecord(xmlFile, string.format("%s.courses.course(%d)", ADFieldStorage.ROOT, courseIndex), record)
|
||||
courseIndex = courseIndex + 1
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldStorage.load()
|
||||
local result = {fields = {}, fleet = {}}
|
||||
local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}, manual = {}, paused = false}
|
||||
local path = ADFieldStorage.getPath()
|
||||
if path == nil or not fileExists(path) then return result end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
||||
if xmlFile == nil then return result end
|
||||
result.fleetMode = xmlFile:getString(ADFieldStorage.ROOT .. "#fleetMode") == "ticked" and "ticked" or "all"
|
||||
result.paused = xmlFile:getInt(ADFieldStorage.ROOT .. "#paused") == 1
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".field", function(_, key)
|
||||
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
||||
if field.id ~= nil then result.fields[field.id] = field end
|
||||
@@ -70,6 +105,14 @@ function ADFieldStorage.load()
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".manual.vehicle", function(_, key)
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil and id ~= "" then result.manual[id] = true end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".courses.course", function(_, key)
|
||||
local record = ADCourseCache ~= nil and ADCourseCache.newRecord(ADFieldStorage.readCourseRecord(xmlFile, key)) or nil
|
||||
if record ~= nil then result.courseRecords[record.vehicle] = record end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
return result
|
||||
end
|
||||
|
||||
@@ -37,3 +37,75 @@ function ADFieldView.soilParts(info)
|
||||
if (info.weed or 0) >= ADFieldModel.WEED_MIN then table.insert(parts, "weed") end
|
||||
return parts
|
||||
end
|
||||
|
||||
-- Afgrødekolonnen: høstet mark = "stub (Hvede)", bar mark = "bar jord", ellers afgrøden.
|
||||
function ADFieldView.cropText(info, getText)
|
||||
if info.kind == "bare" and info.stubbleOf ~= nil then
|
||||
return string.format(getText("spu_field_stubble"), tostring(info.stubbleOf))
|
||||
end
|
||||
if info.kind == "bare" then return getText("spu_field_bare") end
|
||||
return tostring(info.cropTitle or "-")
|
||||
end
|
||||
|
||||
-- Lagervalget for en mark: marksilo først, så gæringssiloer, så efter afstand. Valgte, marksiloer og
|
||||
-- gæringssiloer vises altid; af resten kun de `limit` nærmeste (34 ens "Farma 500"-rækker var ulæselige).
|
||||
-- entries = {{id, distance, fieldSilo, fermenter, chosen}}.
|
||||
function ADFieldView.storeRows(entries, limit)
|
||||
local sorted = {}
|
||||
for _, entry in ipairs(entries or {}) do table.insert(sorted, entry) end
|
||||
local function rank(e) return e.fieldSilo and 0 or (e.fermenter and 1 or 2) end
|
||||
table.sort(sorted, function(a, b)
|
||||
if rank(a) ~= rank(b) then return rank(a) < rank(b) end
|
||||
if (a.distance or math.huge) ~= (b.distance or math.huge) then return (a.distance or math.huge) < (b.distance or math.huge) end
|
||||
return tostring(a.id) < tostring(b.id)
|
||||
end)
|
||||
local rows, others, hidden = {}, 0, 0
|
||||
for _, entry in ipairs(sorted) do
|
||||
if entry.chosen or entry.fieldSilo or entry.fermenter then
|
||||
table.insert(rows, entry)
|
||||
elseif others < limit then
|
||||
others = others + 1
|
||||
table.insert(rows, entry)
|
||||
else
|
||||
hidden = hidden + 1
|
||||
end
|
||||
end
|
||||
return rows, hidden
|
||||
end
|
||||
|
||||
-- "90 m" / "1,4 km"
|
||||
function ADFieldView.distanceText(meters)
|
||||
if meters == nil then return "?" end
|
||||
if meters < 1000 then return string.format("%d m", math.floor(meters + 0.5)) end
|
||||
return (string.gsub(string.format("%.1f km", meters / 1000), "%.", ","))
|
||||
end
|
||||
|
||||
-- ---- 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,
|
||||
}
|
||||
|
||||
-- Arbejder marken reelt? Under global pause (Stop alt) kun hvis spilleren har genoptaget netop den.
|
||||
-- s = {working, paused, resumed}
|
||||
function ADFieldView.isActiveWork(s)
|
||||
return s.working == true and (not s.paused or s.resumed == true)
|
||||
end
|
||||
|
||||
-- s = {hasMarker, working, paused, resumed, 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.paused and not s.resumed then return "stopped" end
|
||||
if s.awaiting or s.startRequested then return "waiting" end
|
||||
if ADFieldView.isActiveWork(s) then return s.reasonKey ~= nil and "waiting" or "running" end
|
||||
if s.reasonKey ~= nil then return "waiting" end
|
||||
return "stopped"
|
||||
end
|
||||
|
||||
-- Må [Start]/[Stop] trykkes? Stop altid når marken arbejder; Start kræver en markør.
|
||||
function ADFieldView.canStart(s)
|
||||
return s.working == true or s.hasMarker == true
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+420
-125
@@ -11,6 +11,11 @@ ADFields.cache = {}
|
||||
ADFields.order = {}
|
||||
ADFields.scanIndex = 0
|
||||
ADFields.mowable = nil
|
||||
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
|
||||
ADFields.OWNED_REFRESH_MS = 30000
|
||||
ADFields.MARKER_REFRESH_MS = 30000
|
||||
ADFields.ownedAt = nil
|
||||
ADFields.now = 0
|
||||
|
||||
function ADFields.parseMarkerName(name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
@@ -45,6 +50,7 @@ end
|
||||
function ADFields.growthPhase(state, fruit)
|
||||
if state == nil or fruit == nil then return nil end
|
||||
if state == 0 then return "none" end
|
||||
if ADFields.isPreparable(state, fruit) then return "ready" end
|
||||
if fruit.cutState ~= nil and fruit.cutState ~= 0 and state == fruit.cutState then return "cut" end
|
||||
if fruit.witheredState ~= nil and state == fruit.witheredState then return "withered" end
|
||||
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
||||
@@ -54,6 +60,33 @@ function ADFields.growthPhase(state, fruit)
|
||||
return "cut"
|
||||
end
|
||||
|
||||
-- Fuldt udvokset stadie for en slåafgrøde: græs er "høstbart" allerede i mellemhøjde (halv ydelse,
|
||||
-- Dakota-græs: greenMiddle 3 < harvestReady 4 < harvestReady2 5), så slåning venter til stadiet efter
|
||||
-- det første høstbare, når afgrøden har flere. nil = ukendte grænser (ingen ekstra krav).
|
||||
function ADFields.primeState(fruit)
|
||||
if fruit == nil then return nil end
|
||||
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
||||
if minReady == nil then return nil end
|
||||
local maxReady = fruit.maxHarvestingGrowthState or fruit.maxHarvestGrowthState or minReady
|
||||
if maxReady > minReady then return minReady + 1 end
|
||||
return minReady
|
||||
end
|
||||
|
||||
-- Kan stadiet "forberedes" (toppes)? Dakota-kartofler: "harvestReady" er kun forberedelig — først det
|
||||
-- toppede stadie er høstklart, men skærebord med topper (Ropa rrXL9) tager begge i ét drag.
|
||||
function ADFields.isPreparable(state, fruit)
|
||||
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
|
||||
local minPrep, maxPrep = fruit.minPreparingGrowthState, fruit.maxPreparingGrowthState
|
||||
if type(minPrep) ~= "number" or minPrep <= 0 then return false end
|
||||
return state >= minPrep and state <= (type(maxPrep) == "number" and maxPrep >= minPrep and maxPrep or minPrep)
|
||||
end
|
||||
|
||||
-- Tæller punktets klar-pixels? Kun når stadiet (4. værdi fra getFruitArea) er fuldt udvokset.
|
||||
function ADFields.isPrime(growthState, prime)
|
||||
if prime == nil or type(growthState) ~= "number" or growthState <= 0 then return true end
|
||||
return growthState >= prime
|
||||
end
|
||||
|
||||
-- Vokser marken? Fasen vinder; ukendt fase -> der er voksende afgrøde (getFruitArea) og den er ikke klar.
|
||||
function ADFields.isGrowing(phase, anyPixels, readyRatio)
|
||||
if phase ~= nil then return phase == "growing" end
|
||||
@@ -69,6 +102,16 @@ function ADFields.soilNeeds(levels, settings)
|
||||
}
|
||||
end
|
||||
|
||||
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen
|
||||
-- (in-game 29/9: F22 hvide druer fik "Silage + så" og en skårlægger). Samme liste som ADCpBridge.VINE_FRUITS.
|
||||
ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||
|
||||
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
|
||||
function ADFields.isMowableFruit(fruitName, mowableEntry)
|
||||
if mowableEntry == nil then return false end
|
||||
return not ADFields.VINE_FRUITS[string.upper(tostring(fruitName or ""))]
|
||||
end
|
||||
|
||||
function ADFields.getMowable()
|
||||
if ADFields.mowable ~= nil then return ADFields.mowable end
|
||||
local ok, result = pcall(function()
|
||||
@@ -122,15 +165,6 @@ local function readWeed(x, z)
|
||||
return value
|
||||
end
|
||||
|
||||
local function average(points, reader, channel)
|
||||
local sum, count = 0, 0
|
||||
for _, p in ipairs(points) do
|
||||
local value = reader(channel, p.x, p.z)
|
||||
if value ~= nil then sum, count = sum + value, count + 1 end
|
||||
end
|
||||
return count > 0 and sum / count or nil
|
||||
end
|
||||
|
||||
local function missionSettings()
|
||||
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
||||
local function flag(value) return value == nil or value == true end
|
||||
@@ -141,22 +175,6 @@ local function missionSettings()
|
||||
}
|
||||
end
|
||||
|
||||
-- Jord og pleje: gødning (og max), kalk, pløjning, ukrudt — gennemsnit over prøvepunkterne.
|
||||
local function readSoil(info)
|
||||
if type(FieldDensityMap) ~= "table" or #info.points == 0 then return end
|
||||
local settings = missionSettings()
|
||||
info.fert = average(info.points, readDensity, FieldDensityMap.SPRAY_LEVEL)
|
||||
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
|
||||
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
|
||||
info.lime = average(info.points, readDensity, FieldDensityMap.LIME_LEVEL)
|
||||
info.plowLevel = average(info.points, readDensity, FieldDensityMap.PLOW_LEVEL)
|
||||
if settings.weedsEnabled then
|
||||
info.weed = average(info.points, function(_, x, z) return readWeed(x, z) end, nil)
|
||||
end
|
||||
info.needs = ADFields.soilNeeds(info, settings)
|
||||
if not settings.limeRequired then info.lime = nil end
|
||||
end
|
||||
|
||||
local function fieldOwner(field)
|
||||
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
||||
if farmland == nil then return nil end
|
||||
@@ -171,82 +189,141 @@ local function isOnField(x, z)
|
||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
||||
end
|
||||
|
||||
-- Centroid + halvvejs mod op til 8 hjørner; kun punkter der stadig er mark (bygninger fjerner areal).
|
||||
local function samplePoints(field)
|
||||
ADFields.SAMPLE_DIRECTIONS = 16
|
||||
|
||||
-- field.fieldState (FS25, bekræftet i AI-Bonde via aifProbe): {fruitIndex|nil, growthState} eller nil
|
||||
-- når spillet ikke har tilstanden (så bruges punkt-afstemningen).
|
||||
function ADFields.readFieldState(field)
|
||||
local state = field ~= nil and field.fieldState or nil
|
||||
if type(state) ~= "table" or type(state.fruitTypeIndex) ~= "number" then return nil end
|
||||
local fruitIndex = state.fruitTypeIndex > 0 and state.fruitTypeIndex or nil
|
||||
local groundNone = type(FieldGroundType) == "table" and FieldGroundType.NONE or 0
|
||||
return {fruitIndex = fruitIndex, growthState = fruitIndex ~= nil and state.growthState or nil,
|
||||
noFieldGround = type(state.groundType) == "number" and state.groundType == groundNone}
|
||||
end
|
||||
|
||||
-- Jordtyper der tæller som klargjort til såning (stubbearbejdet, kultiveret, såbed, pløjet).
|
||||
ADFields.PREPARED_GROUNDS = {"STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}
|
||||
|
||||
-- value = jordtypens tæthedsværdi i et punkt; values = {[typenavn] = værdi}. -> klargjort, pløjet (nil: ukendt)
|
||||
function ADFields.classifyGround(value, values)
|
||||
if value == nil or values == nil then return nil, nil end
|
||||
-- ingen markjord (bygget over / uden for marken): tæller ikke med i andelen
|
||||
if values.NONE ~= nil and values.NONE == value then return nil, nil end
|
||||
local prepared = false
|
||||
for _, name in ipairs(ADFields.PREPARED_GROUNDS) do
|
||||
if values[name] ~= nil and values[name] == value then prepared = true end
|
||||
end
|
||||
return prepared, values.PLOWED ~= nil and values.PLOWED == value
|
||||
end
|
||||
|
||||
-- Spillets tæthedsværdier for jordtyperne (FieldGroundType -> værdi i GROUND_TYPE-kanalen).
|
||||
function ADFields.groundValues()
|
||||
if ADFields.groundValueCache ~= nil then return ADFields.groundValueCache or nil end
|
||||
local system = g_currentMission ~= nil and g_currentMission.fieldGroundSystem or nil
|
||||
if type(FieldGroundType) ~= "table" or system == nil then return nil end
|
||||
local values = {}
|
||||
for _, name in ipairs({"NONE", "STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}) do
|
||||
local groundType = FieldGroundType[name]
|
||||
if groundType ~= nil then
|
||||
local ok, value = pcall(function() return system:getFieldGroundValue(groundType) end)
|
||||
values[name] = ok and type(value) == "number" and value or groundType
|
||||
end
|
||||
end
|
||||
ADFields.groundValueCache = values
|
||||
return values
|
||||
end
|
||||
|
||||
-- Kan marken dyrkes? Nej hvis spillet siger "ingen markjord" (groundType NONE: bygget over — gårdsplads,
|
||||
-- stald, biogasanlæg), eller hvis intet prøvepunkt ligger på markjord. En markensilo i et hjørne ændrer
|
||||
-- intet: resten af marken er stadig markjord.
|
||||
function ADFields.isCultivable(state, onFieldPoints)
|
||||
if state ~= nil and state.noFieldGround then return false end
|
||||
return (onFieldPoints or 0) > 0
|
||||
end
|
||||
|
||||
-- Markens omrids som {{x, z}, ...}. FS25: field:getDensityMapPolygon():getVerticesList() (flad liste
|
||||
-- x1, z1, x2, z2 ... — som Courseplay); ældre: polygonPoints (noder). Tom liste hvis intet findes.
|
||||
function ADFields.readPolygon(field)
|
||||
local verts = {}
|
||||
local okList, list = pcall(function()
|
||||
return field.getDensityMapPolygon ~= nil and field:getDensityMapPolygon():getVerticesList() or nil
|
||||
end)
|
||||
if okList and type(list) == "table" and #list >= 6 then
|
||||
for index = 1, #list - 1, 2 do
|
||||
table.insert(verts, {x = list[index], z = list[index + 1]})
|
||||
end
|
||||
return verts
|
||||
end
|
||||
for _, node in ipairs(field.polygonPoints or {}) do
|
||||
if type(node) == "number" then
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
table.insert(verts, {x = x, z = z})
|
||||
end
|
||||
end
|
||||
local cx, cz = field.posX, field.posZ
|
||||
if (cx == nil or cz == nil) and #verts > 0 then
|
||||
cx, cz = 0, 0
|
||||
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
|
||||
cx, cz = cx / #verts, cz / #verts
|
||||
end
|
||||
if cx == nil then return {}, verts end
|
||||
local candidates = {{x = cx, z = cz}}
|
||||
local step = math.max(1, math.floor(#verts / 8))
|
||||
for index = 1, #verts, step do
|
||||
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
|
||||
end
|
||||
local points = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local ok, onField = pcall(isOnField, p.x, p.z)
|
||||
if ok and onField then table.insert(points, p) end
|
||||
end
|
||||
return points, verts
|
||||
return verts
|
||||
end
|
||||
|
||||
local function fruitAt(points)
|
||||
local votes, best, bestVotes = {}, nil, 0
|
||||
for _, p in ipairs(points) do
|
||||
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, p.x, p.z)
|
||||
if ok and type(index) == "number" and index > 0 then
|
||||
votes[index] = (votes[index] or 0) + 1
|
||||
if votes[index] > bestVotes then best, bestVotes = index, votes[index] end
|
||||
end
|
||||
-- Markens form-fingeraftryk: antal hjørner, areal (shoelace, nærmeste 10 m²) og tyngdepunkt (nærmeste meter).
|
||||
-- Samme mark giver samme streng trods små float-forskelle; en ændret mark (hjørne/kant) giver en ny.
|
||||
-- Bruges til at genbruge Courseplay-kurser (ADCourseCache). nil uden en flade (under 3 hjørner).
|
||||
ADFields.SHAPE_AREA_STEP = 10
|
||||
local function roundTo(value, step)
|
||||
return math.floor(value / step + 0.5) * step
|
||||
end
|
||||
function ADFields.shapeSignature(verts)
|
||||
if type(verts) ~= "table" or #verts < 3 then return nil end
|
||||
local n = #verts
|
||||
local twiceArea, sx, sz = 0, 0, 0
|
||||
for index = 1, n do
|
||||
local a, b = verts[index], verts[index % n + 1]
|
||||
local cross = a.x * b.z - b.x * a.z
|
||||
twiceArea = twiceArea + cross
|
||||
sx, sz = sx + (a.x + b.x) * cross, sz + (a.z + b.z) * cross
|
||||
end
|
||||
local cx, cz
|
||||
if math.abs(twiceArea) > 1e-6 then
|
||||
cx, cz = sx / (3 * twiceArea), sz / (3 * twiceArea)
|
||||
else
|
||||
cx, cz = ADFields.polygonCenter(verts)
|
||||
end
|
||||
return string.format("n=%d;a=%d;c=%d,%d", n, roundTo(math.abs(twiceArea) / 2, ADFields.SHAPE_AREA_STEP),
|
||||
roundTo(cx, 1), roundTo(cz, 1))
|
||||
end
|
||||
|
||||
-- Hjørnernes gennemsnit (nil uden hjørner).
|
||||
function ADFields.polygonCenter(verts)
|
||||
if verts == nil or #verts == 0 then return nil end
|
||||
local cx, cz = 0, 0
|
||||
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
|
||||
return cx / #verts, cz / #verts
|
||||
end
|
||||
|
||||
-- Afstand fra (x, z) til omridsets nærmeste kant; nil uden omrids.
|
||||
function ADFields.distanceToPolygon(verts, x, z)
|
||||
if verts == nil or #verts == 0 then return nil end
|
||||
local best = math.huge
|
||||
for index = 1, #verts do
|
||||
local a, b = verts[index], verts[index % #verts + 1]
|
||||
local dx, dz = b.x - a.x, b.z - a.z
|
||||
local len2 = dx * dx + dz * dz
|
||||
local t = len2 > 0 and math.max(0, math.min(1, ((x - a.x) * dx + (z - a.z) * dz) / len2)) or 0
|
||||
local d = math.sqrt((a.x + dx * t - x) ^ 2 + (a.z + dz * t - z) ^ 2)
|
||||
if d < best then best = d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Andel af prøvefladen i høstvinduet (samme kald som Arbejdsseddel: any = true,true; klar = false,false).
|
||||
local function readyRatio(fruitIndex, points)
|
||||
local anySum, readySum = 0, 0
|
||||
for _, p in ipairs(points) do
|
||||
local okA, _, anyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, true, true)
|
||||
if okA and anyPixels then anySum = anySum + anyPixels end
|
||||
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, false, false)
|
||||
if okR and readyPixels then readySum = readySum + readyPixels end
|
||||
end
|
||||
return anySum > 0 and readySum / anySum or 0, anySum
|
||||
end
|
||||
|
||||
-- Vækststadiet på prøvepunkterne (flertal): getFieldDataAtWorldPosition's anden returværdi.
|
||||
local function growthStateAt(points)
|
||||
local votes, best, bestVotes = {}, nil, 0
|
||||
for _, p in ipairs(points) do
|
||||
local ok, _, state = pcall(function()
|
||||
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, p.x, 1, p.z)
|
||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(p.x, y, p.z)
|
||||
end)
|
||||
if ok and type(state) == "number" then
|
||||
votes[state] = (votes[state] or 0) + 1
|
||||
if votes[state] > bestVotes then best, bestVotes = state, votes[state] end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function findMarker(adEnv, fieldId, x, z)
|
||||
-- Markøren "F<nr>" nærmest marken: afstand til omridset (markører står ved kanten; midten kan
|
||||
-- ligge 900 m væk på store kort), ellers til midten.
|
||||
local function findMarker(adEnv, fieldId, x, z, verts)
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if ADFields.parseMarkerName(marker.name) == fieldId then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
if wayPoint ~= nil then
|
||||
local distance = math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2)
|
||||
local distance = ADFields.distanceToPolygon(verts, wayPoint.x, wayPoint.z)
|
||||
or math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2)
|
||||
if distance < bestDistance then best, bestDistance = marker, distance end
|
||||
end
|
||||
end
|
||||
@@ -257,84 +334,302 @@ local function findMarker(adEnv, fieldId, x, z)
|
||||
return nil, best ~= nil and bestDistance or nil
|
||||
end
|
||||
|
||||
local function scanField(adEnv, field, fieldId)
|
||||
local points, verts = samplePoints(field)
|
||||
local info = {id = fieldId, field = field, points = points, verts = verts, mowable = false, readyRatio = 0}
|
||||
info.x, info.z = field.posX, field.posZ
|
||||
if (info.x == nil or info.z == nil) and points[1] ~= nil then info.x, info.z = points[1].x, points[1].z end
|
||||
info.fruitIndex = #points > 0 and fruitAt(points) or nil
|
||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
||||
if info.fruitIndex ~= nil then
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
||||
-- ---- scanning som arbejdskø ----
|
||||
-- En marks scanning er en kø af små opgaver (ét prøvepunkt pr. opgave), og scanStep kører én opgave pr.
|
||||
-- frame. In-game 26/9 gav en hel mark i ét frame et hak (flere opslag gennemgår alle kortets ~40
|
||||
-- afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem), derfor en kø.
|
||||
|
||||
-- (eksplicit hale: # er udefineret på en tabel med huller, og udførte opgaver nulstilles)
|
||||
local function push(scan, task)
|
||||
scan.tail = (scan.tail or 0) + 1
|
||||
scan.queue[scan.tail] = task
|
||||
end
|
||||
|
||||
local function vote(votes, key)
|
||||
votes[key] = (votes[key] or 0) + 1
|
||||
end
|
||||
|
||||
local function winner(votes)
|
||||
local best, bestVotes = nil, 0
|
||||
for key, count in pairs(votes) do
|
||||
if count > bestVotes then best, bestVotes = key, count end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
local function newScan(adEnv, entry)
|
||||
local field, fieldId = entry.field, entry.id
|
||||
local scan = {id = fieldId, queue = {}, head = 1, tail = 0, points = {}, fruitVotes = {},
|
||||
anySum = 0, readySum = 0, immatureSum = 0, totalSum = 0, soil = {fert = {0, 0}, lime = {0, 0}, plowLevel = {0, 0}, weed = {0, 0}, prepared = {0, 0}, plowed = {0, 0}}}
|
||||
local info = {id = fieldId, field = field, points = scan.points, verts = {}, mowable = false, readyRatio = 0}
|
||||
scan.info = info
|
||||
-- 1) polygon og kandidatpunkter (centroid + halvvejs mod op til 8 hjørner)
|
||||
push(scan, function()
|
||||
local verts = ADFields.readPolygon(field)
|
||||
info.verts = verts
|
||||
local cx, cz = ADFields.polygonCenter(verts)
|
||||
if cx == nil then cx, cz = field.posX, field.posZ end
|
||||
info.x, info.z = cx, cz
|
||||
if cx == nil then return end
|
||||
-- midten, markskiltet og halvvejs mod op til SAMPLE_DIRECTIONS hjørner: store marker (Dakota
|
||||
-- 16x: 1,7 km) har striber uden afgrøde, så ét punkt kan ramme forkert
|
||||
local candidates = {{x = cx, z = cz}}
|
||||
if field.posX ~= nil and field.posZ ~= nil and (field.posX - cx) ^ 2 + (field.posZ - cz) ^ 2 > 1 then
|
||||
table.insert(candidates, {x = field.posX, z = field.posZ})
|
||||
end
|
||||
local step = math.max(1, math.floor(#verts / ADFields.SAMPLE_DIRECTIONS))
|
||||
for index = 1, #verts, step do
|
||||
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
|
||||
end
|
||||
-- 2) én opgave pr. kandidat: er punktet stadig mark? (bygninger fjerner areal)
|
||||
for _, candidate in ipairs(candidates) do
|
||||
push(scan, function()
|
||||
local ok, onField = pcall(isOnField, candidate.x, candidate.z)
|
||||
if ok and onField then table.insert(scan.points, candidate) end
|
||||
end)
|
||||
end
|
||||
-- 3) afgrøde pr. punkt (efter alle kandidater)
|
||||
push(scan, function()
|
||||
for _, point in ipairs(scan.points) do
|
||||
push(scan, function()
|
||||
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, point.x, point.z)
|
||||
if ok and type(index) == "number" and index > 0 then vote(scan.fruitVotes, index) end
|
||||
end)
|
||||
end
|
||||
push(scan, function() ADFields.scanPerFruit(scan) end)
|
||||
end)
|
||||
end)
|
||||
return scan
|
||||
end
|
||||
|
||||
-- Når afgrøden kendes: klar-andel, vækststadie og jord pr. punkt (én opgave pr. punkt).
|
||||
function ADFields.scanPerFruit(scan)
|
||||
local info = scan.info
|
||||
-- spillets egen marktilstand (som fields.xml) er sandheden; punkt-afstemningen er reserve.
|
||||
-- (getFieldDataAtWorldPosition giver isOnField, densityBits, groundType — IKKE vækststadie)
|
||||
local state = ADFields.readFieldState(info.field)
|
||||
info.growthState = state ~= nil and state.growthState or nil
|
||||
info.cultivable = ADFields.isCultivable(state, #scan.points)
|
||||
if state ~= nil then
|
||||
info.fruitIndex = state.fruitIndex
|
||||
else
|
||||
info.fruitIndex = winner(scan.fruitVotes)
|
||||
end
|
||||
local fruitIndex = info.fruitIndex
|
||||
local settings = missionSettings()
|
||||
scan.settings = settings
|
||||
-- slåafgrøder: genvækst i mellemhøjde tæller ikke som klar (in-game 27/9: halvt slået F90 blev
|
||||
-- klippet igen i mellemhøjde)
|
||||
local prime = nil
|
||||
local fruitDesc = fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
|
||||
if fruitIndex ~= nil and ADFields.getMowable()[fruitIndex] ~= nil then
|
||||
prime = ADFields.primeState(fruitDesc)
|
||||
end
|
||||
for _, point in ipairs(scan.points) do
|
||||
push(scan, function()
|
||||
local x, z = point.x, point.z
|
||||
if fruitIndex ~= nil then
|
||||
-- returnerer fruitValue, numPixels (afgrøde i høsthøjde), totalNumPixels (hele arealet), growthState
|
||||
local okA, _, anyPixels, totalPixels, pointState = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, true, true)
|
||||
if okA and type(totalPixels) == "number" then scan.totalSum = scan.totalSum + totalPixels end
|
||||
if okA and anyPixels then
|
||||
scan.anySum = scan.anySum + anyPixels
|
||||
if not ADFields.isPrime(pointState, prime) then scan.immatureSum = scan.immatureSum + anyPixels end
|
||||
end
|
||||
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, false, false)
|
||||
-- utoppede kartofler tæller som klar (in-game 27/9 F16: klar-andel 0 -> sprang høsten over)
|
||||
if okA and anyPixels and ADFields.isPreparable(pointState, fruitDesc) then
|
||||
okR, readyPixels = true, math.max(readyPixels or 0, anyPixels)
|
||||
end
|
||||
if okR and readyPixels and ADFields.isPrime(okA and pointState or nil, prime) then
|
||||
scan.readySum = scan.readySum + readyPixels
|
||||
end
|
||||
end
|
||||
end)
|
||||
push(scan, function()
|
||||
if type(FieldDensityMap) ~= "table" then return end
|
||||
local x, z = point.x, point.z
|
||||
local readings = {
|
||||
fert = readDensity(FieldDensityMap.SPRAY_LEVEL, x, z),
|
||||
lime = readDensity(FieldDensityMap.LIME_LEVEL, x, z),
|
||||
plowLevel = readDensity(FieldDensityMap.PLOW_LEVEL, x, z),
|
||||
weed = settings.weedsEnabled and readWeed(x, z) or nil,
|
||||
}
|
||||
-- klargjort/pløjet jord i punktet (kun på markjord): klargøringstrin der allerede er gjort går videre
|
||||
local prepared, plowed = ADFields.classifyGround(readDensity(FieldDensityMap.GROUND_TYPE, x, z), ADFields.groundValues())
|
||||
if prepared ~= nil then
|
||||
readings.prepared = prepared and 1 or 0
|
||||
readings.plowed = plowed and 1 or 0
|
||||
end
|
||||
for key, value in pairs(readings) do
|
||||
if value ~= nil then
|
||||
scan.soil[key][1] = scan.soil[key][1] + value
|
||||
scan.soil[key][2] = scan.soil[key][2] + 1
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Samler resultatet når køen er tom.
|
||||
local function finishScan(adEnv, scan)
|
||||
local info = scan.info
|
||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
||||
local fruitDesc = info.fruitIndex ~= nil and g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
|
||||
if mowable ~= nil and not ADFields.isMowableFruit(fruitDesc ~= nil and fruitDesc.name or nil, mowable) then mowable = nil end
|
||||
if mowable ~= nil then
|
||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||
end
|
||||
local fruit = nil
|
||||
if info.fruitIndex ~= nil then
|
||||
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
|
||||
info.readyRatio = scan.anySum > 0 and scan.readySum / scan.anySum or 0
|
||||
info.immatureRatio = scan.anySum > 0 and scan.immatureSum / scan.anySum or 0
|
||||
info.anyPixels = scan.anySum
|
||||
-- andel af hele arealet der stadig står i høsthøjde (fremdrift på fanen Måned)
|
||||
info.standingRatio = scan.totalSum > 0 and scan.readySum / scan.totalSum or nil
|
||||
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||
info.fruitName = fruit ~= nil and fruit.name or nil
|
||||
info.harvestFillType = fruit ~= nil and fruit.fillType ~= nil and fruit.fillType.index or nil
|
||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
||||
end
|
||||
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
||||
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), fruit) or nil
|
||||
info.phase = fruit ~= nil and ADFields.growthPhase(info.growthState, fruit) or nil
|
||||
info.growing = info.fruitIndex ~= nil and ADFields.isGrowing(info.phase, info.anyPixels, info.readyRatio)
|
||||
-- høstet stub eller visnet afgrøde er i praksis en bar mark (klargøring/såning)
|
||||
if info.kind == "harvest" and (info.phase == "cut" or info.phase == "withered" or info.phase == "none") then
|
||||
info.kind = "bare"
|
||||
info.stubbleOf = info.phase == "cut" and info.cropTitle or nil
|
||||
end
|
||||
readSoil(info)
|
||||
if info.x ~= nil then
|
||||
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
|
||||
local settings = scan.settings or missionSettings()
|
||||
for key, sum in pairs(scan.soil) do
|
||||
info[key] = sum[2] > 0 and sum[1] / sum[2] or nil
|
||||
end
|
||||
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
|
||||
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
|
||||
info.needs = ADFields.soilNeeds(info, settings)
|
||||
if not settings.limeRequired then info.lime = nil end
|
||||
local previous = ADFields.cache[info.id]
|
||||
if previous ~= nil and previous.markerCheckedAt ~= nil and ADFields.now - previous.markerCheckedAt < ADFields.MARKER_REFRESH_MS then
|
||||
info.markerId, info.markerName, info.markerDistance, info.markerCheckedAt = previous.markerId, previous.markerName, previous.markerDistance, previous.markerCheckedAt
|
||||
elseif info.x ~= nil then
|
||||
local marker, distance = findMarker(adEnv, info.id, info.x, info.z, info.verts)
|
||||
info.markerId = marker ~= nil and marker.id or nil
|
||||
info.markerName = marker ~= nil and marker.name or nil
|
||||
info.markerDistance = distance
|
||||
info.markerCheckedAt = ADFields.now
|
||||
end
|
||||
return info
|
||||
-- markens form: ændret mark (anden signatur end kursus-registret) -> gemte kurser til marken glemmes
|
||||
info.shapeSig = ADFields.shapeSignature(info.verts)
|
||||
if ADCourseCache ~= nil and ADCourseCache.checkField ~= nil then pcall(ADCourseCache.checkField, info.id, info.shapeSig) end
|
||||
ADFields.cache[info.id] = info
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
local function ownedFields(farmId)
|
||||
local result = {}
|
||||
local all = g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}
|
||||
for _, field in pairs(all) do
|
||||
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||
if id ~= nil and fieldOwner(field) == farmId then
|
||||
table.insert(result, {id = id, field = field})
|
||||
end
|
||||
-- Marklisten (ejede marker) i portioner af 25 marker pr. frame.
|
||||
local function newOwnedScan(farmId)
|
||||
local all = {}
|
||||
for _, field in pairs(g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}) do
|
||||
table.insert(all, field)
|
||||
end
|
||||
table.sort(result, function(a, b) return a.id < b.id end)
|
||||
return result
|
||||
local scan = {owned = true, queue = {}, head = 1, tail = 0, result = {}}
|
||||
for start = 1, #all, 25 do
|
||||
push(scan, function()
|
||||
for index = start, math.min(start + 24, #all) do
|
||||
local field = all[index]
|
||||
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||
if id ~= nil and fieldOwner(field) == farmId then
|
||||
table.insert(scan.result, {id = id, field = field})
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
return scan
|
||||
end
|
||||
|
||||
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
|
||||
function ADFields.scanNext(adEnv, farmId)
|
||||
local owned = ownedFields(farmId)
|
||||
local function finishOwnedScan(scan)
|
||||
table.sort(scan.result, function(a, b) return a.id < b.id end)
|
||||
local ids = {}
|
||||
for _, entry in ipairs(owned) do ids[entry.id] = true end
|
||||
for _, entry in ipairs(scan.result) do ids[entry.id] = true end
|
||||
for id in pairs(ADFields.cache) do
|
||||
if not ids[id] then ADFields.cache[id] = nil end
|
||||
end
|
||||
ADFields.order = owned
|
||||
if #owned == 0 then return end
|
||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
||||
local entry = owned[ADFields.scanIndex]
|
||||
local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
|
||||
if ok then
|
||||
ADFields.cache[entry.id] = info
|
||||
else
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
|
||||
end
|
||||
ADFields.order = scan.result
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
|
||||
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
|
||||
ADFields.SCAN_INTERVAL_MS = 2000
|
||||
ADFields.MAX_TASKS_PER_FRAME = 8
|
||||
ADFields.FRAME_BUDGET_MS = 0.5
|
||||
|
||||
function ADFields.clock()
|
||||
if netGetTime == nil then return nil end
|
||||
local ok, ms = pcall(netGetTime)
|
||||
return ok and type(ms) == "number" and ms or nil
|
||||
end
|
||||
ADFields.lastScanStart = nil
|
||||
|
||||
function ADFields.scanStep(adEnv, farmId, now)
|
||||
ADFields.now = now or ADFields.now
|
||||
local scan = ADFields.activeScan
|
||||
if scan == nil then
|
||||
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
|
||||
ADFields.ownedAt = ADFields.now
|
||||
scan = newOwnedScan(farmId)
|
||||
else
|
||||
if ADFields.lastScanStart ~= nil and ADFields.now - ADFields.lastScanStart < ADFields.SCAN_INTERVAL_MS then return end
|
||||
local owned = ADFields.order
|
||||
if #owned == 0 then return end
|
||||
ADFields.lastScanStart = ADFields.now
|
||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
||||
scan = newScan(adEnv, owned[ADFields.scanIndex])
|
||||
end
|
||||
ADFields.activeScan = scan
|
||||
end
|
||||
local task = scan.queue[scan.head]
|
||||
if task ~= nil then
|
||||
-- flere små opgaver pr. frame inden for et tidsbudget (efter genstart skal alle marker scannes;
|
||||
-- én pr. frame gjorde listen fuld først efter flere minutter). Uden ur (tests): én pr. frame.
|
||||
local started = ADFields.clock()
|
||||
for _ = 1, ADFields.MAX_TASKS_PER_FRAME do
|
||||
task = scan.queue[scan.head]
|
||||
if task == nil then return end
|
||||
scan.queue[scan.head] = nil
|
||||
scan.head = scan.head + 1
|
||||
local ok, err = pcall(task)
|
||||
if not ok then
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
|
||||
ADFields.activeScan = nil
|
||||
return
|
||||
end
|
||||
local current = started ~= nil and ADFields.clock() or nil
|
||||
if current == nil or current - started >= ADFields.FRAME_BUDGET_MS then return end
|
||||
end
|
||||
return
|
||||
end
|
||||
-- køen er tom: saml resultatet
|
||||
ADFields.activeScan = nil
|
||||
local ok, err
|
||||
if scan.owned then
|
||||
ok, err = pcall(finishOwnedScan, scan)
|
||||
else
|
||||
ok, err = pcall(finishScan, adEnv, scan)
|
||||
end
|
||||
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
|
||||
end
|
||||
|
||||
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||
function ADFields.list()
|
||||
if ADFields.listCache ~= nil then return ADFields.listCache end
|
||||
local infos = {}
|
||||
for _, entry in ipairs(ADFields.order) do
|
||||
local info = ADFields.cache[entry.id]
|
||||
if info ~= nil then table.insert(infos, info) end
|
||||
-- bygget-over marker (stald, biogas, gårdsplads) kan ikke dyrkes: ikke med i oversigt/arbejde
|
||||
if info ~= nil and info.cultivable ~= false then table.insert(infos, info) end
|
||||
end
|
||||
ADFields.listCache = infos
|
||||
return infos
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
-- AD Fleet Names
|
||||
-- Konsol "spuNameFleet": traktorer/lastbiler uden for flows får spillets butiksnavn (nummereret når der er flere
|
||||
-- af samme model), og deres egen AD-parkering hedder "P <navn>". Flow-traktorer beholder flowets navn (mod'en
|
||||
-- sætter det selv); selvkørende maskiner (mejetærsker, roeoptager …) beholder deres navn.
|
||||
|
||||
ADFleetNames = {}
|
||||
ADFleetNames.LOG_PREFIX = "ADSmartPickup: navne: "
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- vehicles = {{id, model, name, inRun, selfPropelled}} -> {{id, name, parkName, rename}}
|
||||
-- parkUse = {[parkIndex] = antal køretøjer (også redskaber) med den parkering}: delte parkeringer røres ikke.
|
||||
function ADFleetNames.plan(vehicles, parkUse)
|
||||
local byModel = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
if not vehicle.inRun and not vehicle.selfPropelled then
|
||||
byModel[vehicle.model] = byModel[vehicle.model] or {}
|
||||
table.insert(byModel[vehicle.model], vehicle)
|
||||
end
|
||||
end
|
||||
local newNames = {}
|
||||
for model, list in pairs(byModel) do
|
||||
table.sort(list, function(a, b) return tostring(a.id) < tostring(b.id) end)
|
||||
for index, vehicle in ipairs(list) do
|
||||
newNames[vehicle.id] = #list > 1 and string.format("%s %d", model, index) or model
|
||||
end
|
||||
end
|
||||
local result = {}
|
||||
for _, vehicle in ipairs(vehicles or {}) do
|
||||
local name = newNames[vehicle.id] or vehicle.name or vehicle.model
|
||||
local ownPark = vehicle.park == nil or ((parkUse or {})[vehicle.park] or 1) <= 1
|
||||
table.insert(result, {id = vehicle.id, name = name, parkName = "P " .. name,
|
||||
rename = newNames[vehicle.id] ~= nil and name ~= vehicle.name,
|
||||
renamePark = newNames[vehicle.id] ~= nil and ownPark})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
function ADFleetNames.apply(adEnv)
|
||||
local vehicles, byId, parkUse = {}, {}, {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
if type(index) == "number" and index >= 1 and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then
|
||||
parkUse[index] = (parkUse[index] or 0) + 1
|
||||
end
|
||||
end
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if id ~= nil and vehicle.getName ~= nil then
|
||||
local ownIndex = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||
table.insert(vehicles, {id = id, model = tostring(vehicle:getName()), name = ADRuns.vehicleLabel(vehicle),
|
||||
park = type(ownIndex) == "number" and ownIndex >= 1 and ownIndex or nil,
|
||||
inRun = ADRunsController.getRunForVehicle(vehicle) ~= nil,
|
||||
selfPropelled = not ADPool.canPull(ADPool.ownRole(vehicle))})
|
||||
byId[id] = vehicle
|
||||
end
|
||||
end
|
||||
local renamed, parks = 0, 0
|
||||
for _, entry in ipairs(ADFleetNames.plan(vehicles, parkUse)) do
|
||||
local vehicle = byId[entry.id]
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if entry.rename and module ~= nil and module.setName ~= nil then
|
||||
module:setName(entry.name)
|
||||
if module.raiseDirtyFlag ~= nil then module:raiseDirtyFlag() end
|
||||
renamed = renamed + 1
|
||||
end
|
||||
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
|
||||
if entry.renamePark and marker ~= nil and marker.name ~= entry.parkName then
|
||||
adEnv.ADGraphManager:renameMapMarker(entry.parkName, index)
|
||||
parks = parks + 1
|
||||
end
|
||||
end
|
||||
Logging.info(ADFleetNames.LOG_PREFIX .. "%d maskiner omdøbt, %d parkeringer omdøbt", renamed, parks)
|
||||
return renamed, parks
|
||||
end
|
||||
|
||||
function ADFleetNames:consoleNameFleet()
|
||||
local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil
|
||||
if adEnv == nil then return "spuNameFleet: AutoDrive er ikke klar endnu" end
|
||||
local ok, renamed, parks = pcall(ADFleetNames.apply, adEnv)
|
||||
if not ok then return "spuNameFleet: fejl: " .. tostring(renamed) end
|
||||
return string.format("spuNameFleet: %d maskiner og %d parkeringer omdøbt — gem spillet", renamed, parks)
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuNameFleet", "Giver traktorer uden for flows butiksnavne og deres parkeringer 'P <navn>'", "consoleNameFleet", ADFleetNames)
|
||||
end
|
||||
@@ -0,0 +1,567 @@
|
||||
-- AD Flow Guide
|
||||
-- "Opsæt flow": flowguidens trin oven på ADGuide (ren Lua, ingen spil-API) — samme trin-skærm og knapper
|
||||
-- som markguiden. Kladden er en kopi af flowets felter, så intet ændres før [Gem]/[Start flow]. Det
|
||||
-- spil-afhængige kommer i `context` (GUI'et henter det ud fra kladden, se previewRun):
|
||||
-- {buildings = {{id, name, label, hasMarker, text}} -- kandidater til Fra (udkørsel) / Til (forsyning)
|
||||
-- fillTypes = {{name, title}} -- varer målet tager (forsyning, Vælg selv)
|
||||
-- products = {{name, title}} -- varer kilderne har (udkørsel)
|
||||
-- storeBuildings = {{id, name, text}} -- lagre der kan tage flowets lagervarer
|
||||
-- waitPools = {{group, count}}
|
||||
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked, solo}} -- label = kort navn (resumé)
|
||||
-- wagons = {{id, label}}} -- ledige vogne til solo-traktorer (bedste først)
|
||||
-- Solo-traktor (ingen vogn bagpå) får en ledig vogn fra context.wagons (draft.wagons = {[traktorId] = vognId}),
|
||||
-- som den henter ved "Connect <vognens parkering>" — uden at vognens parkering skal omdøbes.
|
||||
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
|
||||
|
||||
ADFlowGuide = {}
|
||||
|
||||
ADFlowGuide.ORDER = {"name", "direction", "buildings", "goods", "store", "rules", "wait", "tractors", "summary"}
|
||||
ADFlowGuide.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||
ADFlowGuide.MAX_NAMED_TRACTORS = 3
|
||||
ADFlowGuide.MAX_NAMED_GOODS = 3
|
||||
|
||||
local function L(key, args) return {key = key, args = args} end
|
||||
local function T(text) return {text = text} end
|
||||
|
||||
local function isOut(guide) return guide.draft.kind == "outbound" end
|
||||
|
||||
local function hasStorePolicy(draft)
|
||||
for _, product in ipairs(draft.products or {}) do
|
||||
if product.policy == "store" or product.policy == "sellElseStore" then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local VISIBLE = {
|
||||
name = function(guide) return guide.draft.isNew == true end,
|
||||
store = function(guide) return isOut(guide) and hasStorePolicy(guide.draft) end,
|
||||
}
|
||||
|
||||
function ADFlowGuide.isVisible(step, guide)
|
||||
local rule = VISIBLE[step]
|
||||
if rule ~= nil then return rule(guide) == true end
|
||||
for _, id in ipairs(ADFlowGuide.ORDER) do
|
||||
if id == step then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
ADFlowGuide.DEF = {order = ADFlowGuide.ORDER, isVisible = ADFlowGuide.isVisible}
|
||||
|
||||
-- Overskrift pr. trin; "Fra"/"Til" følger retningen.
|
||||
function ADFlowGuide.stepKey(step, guide)
|
||||
if step == "buildings" then
|
||||
return (guide ~= nil and isOut(guide)) and "spu_guide_flow_step_from" or "spu_guide_flow_step_to"
|
||||
end
|
||||
return "spu_guide_flow_step_" .. tostring(step)
|
||||
end
|
||||
|
||||
local KIND_HINTS = {buildings = true, goods = true, rules = true}
|
||||
function ADFlowGuide.hintKey(step, guide)
|
||||
if KIND_HINTS[step] then
|
||||
return "spu_guide_flow_hint_" .. step .. ((guide ~= nil and isOut(guide)) and "_out" or "_in")
|
||||
end
|
||||
return "spu_guide_flow_hint_" .. tostring(step)
|
||||
end
|
||||
|
||||
-- ---- kladde ----
|
||||
|
||||
local RUN_FIELDS = {
|
||||
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
||||
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
||||
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
|
||||
}
|
||||
|
||||
local function draftFrom(run, isNew)
|
||||
local draft = {}
|
||||
for _, key in ipairs(RUN_FIELDS) do draft[key] = ADGuide.deepCopy(run[key]) end
|
||||
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products", "ingredients", "vehicleIds"}) do
|
||||
draft[key] = draft[key] or {}
|
||||
end
|
||||
draft.kind = draft.kind == "outbound" and "outbound" or "supply"
|
||||
draft.ingredientMode = draft.ingredientMode == "manual" and "manual" or "auto"
|
||||
draft.sellAtPercent = draft.sellAtPercent or ADRuns.DEFAULT_SELL_AT_PERCENT
|
||||
draft.storeAbovePercent = draft.storeAbovePercent or ADRuns.DEFAULT_STORE_ABOVE_PERCENT
|
||||
draft.sellAbovePercent = draft.sellAbovePercent or ADRuns.DEFAULT_SELL_ABOVE_PERCENT
|
||||
draft.returnBelowPercent = draft.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT
|
||||
draft.loops = draft.loops or 0
|
||||
draft.waitPoolGroup = draft.waitPoolGroup or ""
|
||||
draft.wagons = draft.wagons or {}
|
||||
draft.collectRound = draft.collectRound == true
|
||||
draft.isNew = isNew == true
|
||||
draft.showAllBuildings = false
|
||||
draft.storeAuto = #draft.storeBuildingIds == 0
|
||||
return draft
|
||||
end
|
||||
|
||||
function ADFlowGuide.open(run, context, options)
|
||||
return ADGuide.new(ADFlowGuide.DEF, draftFrom(run, false), context, options)
|
||||
end
|
||||
|
||||
-- Nyt flow: intet oprettes før [Gem]; kladden har standardværdierne fra ADRuns.createRun.
|
||||
function ADFlowGuide.openNew(name, context)
|
||||
local _, run = ADRuns.createRun(ADRuns.newStore(), name, "supply")
|
||||
local draft = draftFrom(run, true)
|
||||
draft.id = nil
|
||||
return ADGuide.new(ADFlowGuide.DEF, draft, context)
|
||||
end
|
||||
|
||||
-- Run-formet tabel til controllerens opslag (varer, vogne) før noget er gemt: markører fra de valgte
|
||||
-- bygninger. markerIds(buildingId, role) -> {wayPointId, ...}.
|
||||
function ADFlowGuide.previewRun(draft, markerIds)
|
||||
local run = ADGuide.deepCopy(draft)
|
||||
local function resolve(ids, role)
|
||||
local result, seen = {}, {}
|
||||
for _, buildingId in ipairs(ids or {}) do
|
||||
for _, wayPointId in ipairs(markerIds(buildingId, role) or {}) do
|
||||
if not seen[wayPointId] then
|
||||
seen[wayPointId] = true
|
||||
table.insert(result, wayPointId)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
run.sourceWayPointIds = resolve(draft.sourceBuildingIds, "load")
|
||||
run.targetWayPointIds = resolve(draft.targetBuildingIds, "unload")
|
||||
run.targetWayPointId = run.targetWayPointIds[1]
|
||||
return run
|
||||
end
|
||||
|
||||
-- ---- vogne til solo-traktorer ----
|
||||
|
||||
local function soloIds(context)
|
||||
local set = {}
|
||||
for _, vehicle in ipairs((context or {}).vehicles or {}) do
|
||||
if vehicle.solo then set[vehicle.id] = true end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
-- Ny vogn-tabel: fravalgte traktorer slipper deres vogn; valgte solo-traktorer uden (gyldig) vogn får den første
|
||||
-- ledige i context.wagons, som ingen anden traktor i flowet har. Vogne til ikke-solo-traktorer bevares (husket).
|
||||
function ADFlowGuide.assignWagons(draft, context)
|
||||
local chosen, solo, offered = {}, soloIds(context), {}
|
||||
for _, id in ipairs(draft.vehicleIds or {}) do chosen[id] = true end
|
||||
for _, wagon in ipairs((context or {}).wagons or {}) do offered[wagon.id] = true end
|
||||
local wagons, used = {}, {}
|
||||
for tractorId, wagonId in pairs(draft.wagons or {}) do
|
||||
local valid = chosen[tractorId] and (not solo[tractorId] or offered[wagonId])
|
||||
if valid and not used[wagonId] then
|
||||
wagons[tractorId] = wagonId
|
||||
used[wagonId] = true
|
||||
end
|
||||
end
|
||||
for _, tractorId in ipairs(draft.vehicleIds or {}) do
|
||||
if solo[tractorId] and wagons[tractorId] == nil then
|
||||
for _, wagon in ipairs((context or {}).wagons or {}) do
|
||||
if not used[wagon.id] then
|
||||
wagons[tractorId] = wagon.id
|
||||
used[wagon.id] = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return wagons
|
||||
end
|
||||
|
||||
-- ---- valg ----
|
||||
|
||||
local SET_VALUES = {
|
||||
kind = true, ingredientMode = true, sellAtPercent = true, storeAbovePercent = true, sellAbovePercent = true, loops = true,
|
||||
returnBelowPercent = true, waitPoolGroup = true,
|
||||
}
|
||||
local BOOL_VALUES = {showAllBuildings = true, storeAuto = true, collectRound = true}
|
||||
local LIST_VALUES = {ingredients = true, storeBuildingIds = true, vehicleIds = true}
|
||||
|
||||
local function setPolicy(products, name, policy)
|
||||
local result = {}
|
||||
for _, product in ipairs(products or {}) do
|
||||
if product.fillType ~= name then table.insert(result, {fillType = product.fillType, policy = product.policy}) end
|
||||
end
|
||||
if policy ~= "off" then table.insert(result, {fillType = name, policy = policy}) end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADFlowGuide.choose(guide, id, value)
|
||||
if id == "showBlocked" then
|
||||
-- visning, ikke en del af flowet: fold listen over traktorer der ikke kan bruges ud/ind
|
||||
local copy = {}
|
||||
for key, field in pairs(guide) do copy[key] = field end
|
||||
copy.showBlocked = not guide.showBlocked
|
||||
return copy
|
||||
end
|
||||
local draft = ADGuide.deepCopy(guide.draft)
|
||||
if SET_VALUES[id] then
|
||||
draft[id] = value
|
||||
elseif BOOL_VALUES[id] then
|
||||
draft[id] = value == true
|
||||
elseif LIST_VALUES[id] then
|
||||
draft[id] = ADGuide.toggleItem(draft[id], value.item, value.on)
|
||||
if id == "vehicleIds" then draft.wagons = ADFlowGuide.assignWagons(draft, guide.context) end
|
||||
elseif id == "wagon" then
|
||||
draft.wagons = ADGuide.deepCopy(draft.wagons or {})
|
||||
draft.wagons[value.item] = value.value
|
||||
elseif id == "buildings" then
|
||||
local key = draft.kind == "outbound" and "sourceBuildingIds" or "targetBuildingIds"
|
||||
draft[key] = ADGuide.toggleItem(draft[key], value.item, value.on)
|
||||
elseif id == "policy" then
|
||||
draft.products = setPolicy(draft.products, value.item, value.value)
|
||||
elseif id == "name" then
|
||||
if type(value) ~= "string" or not value:match("%S") then return guide end
|
||||
draft.name = value
|
||||
else
|
||||
return guide
|
||||
end
|
||||
return ADGuide.withDraft(guide, draft)
|
||||
end
|
||||
|
||||
-- ---- rækker ----
|
||||
|
||||
local function choice(id, title, options, value, item)
|
||||
return {type = "choice", id = id, title = title, options = options, value = value, item = item}
|
||||
end
|
||||
|
||||
local function toggle(id, title, value)
|
||||
return {type = "toggle", id = id, title = title, off = L("ui_no"), on = L("ui_yes"), value = value == true}
|
||||
end
|
||||
|
||||
local function check(id, item, text, value)
|
||||
return {type = "check", id = id, item = item, title = T(text), value = value == true}
|
||||
end
|
||||
|
||||
local function text(key, args) return {type = "text", label = L(key, args)} end
|
||||
|
||||
local function percentOptions(from, to, step)
|
||||
local options = {}
|
||||
for percent = from, to, step do table.insert(options, {label = T(string.format("%d %%", percent)), value = percent}) end
|
||||
return options
|
||||
end
|
||||
|
||||
-- "Sælg altid over": Fra, 50-100 %
|
||||
local function sellAboveOptions()
|
||||
local options = {{label = L("spu_sellAbove_off"), value = 0}}
|
||||
for _, option in ipairs(percentOptions(50, ADRuns.MAX_SELL_ABOVE_PERCENT, 5)) do table.insert(options, option) end
|
||||
return options
|
||||
end
|
||||
|
||||
local function chosenSet(list)
|
||||
local set = {}
|
||||
for _, value in ipairs(list or {}) do set[value] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
local function buildingIds(draft)
|
||||
return draft.kind == "outbound" and draft.sourceBuildingIds or draft.targetBuildingIds
|
||||
end
|
||||
|
||||
local function rowsName(guide)
|
||||
return {
|
||||
{type = "text", label = T(guide.draft.name or "")},
|
||||
{type = "link", id = "rename", value = true, label = L("spu_guide_flow_rename")},
|
||||
}
|
||||
end
|
||||
|
||||
local function rowsDirection(guide)
|
||||
return {choice("kind", L("spu_direction"), {
|
||||
{label = L("spu_direction_in"), value = "supply"}, {label = L("spu_direction_out"), value = "outbound"}}, guide.draft.kind)}
|
||||
end
|
||||
|
||||
-- Bygninger i fast rækkefølge (navn, markør); uden markør kun med "Vis alle" (eller når valgt).
|
||||
local function rowsBuildings(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local chosen = chosenSet(buildingIds(draft))
|
||||
local candidates = guide.context.buildings or {}
|
||||
local shown = ADFlowView.buildingRows(candidates, chosen, draft.showAllBuildings)
|
||||
local hidden = ADFlowView.hiddenCount(candidates, shown)
|
||||
if hidden > 0 or draft.showAllBuildings then
|
||||
table.insert(rows, toggle("showAllBuildings", L("spu_showAllBuildings", {hidden}), draft.showAllBuildings))
|
||||
end
|
||||
for _, building in ipairs(shown) do
|
||||
table.insert(rows, check("buildings", building.id, building.text or building.name, chosen[building.id]))
|
||||
end
|
||||
if #shown == 0 then table.insert(rows, text("spu_noBuildings")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsGoods(guide)
|
||||
local draft, context, rows = guide.draft, guide.context, {}
|
||||
if isOut(guide) then
|
||||
local products = context.products or {}
|
||||
if #products == 0 then return {text("spu_guide_flow_noProducts")} end
|
||||
local options = {}
|
||||
for _, policy in ipairs(ADFlowGuide.POLICIES) do table.insert(options, {label = L("spu_policy_" .. policy), value = policy}) end
|
||||
for _, product in ipairs(products) do
|
||||
table.insert(rows, choice("policy", T(product.text or product.title), options, ADRuns.getProductPolicy(draft, product.name), product.name))
|
||||
end
|
||||
return rows
|
||||
end
|
||||
table.insert(rows, choice("ingredientMode", L("spu_guide_flow_goods"), {
|
||||
{label = L("spu_guide_flow_goodsAutoChoice"), value = "auto"}, {label = L("spu_guide_manual"), value = "manual"}}, draft.ingredientMode))
|
||||
if draft.ingredientMode ~= "manual" then
|
||||
table.insert(rows, text("spu_guide_flow_goodsAutoText"))
|
||||
return rows
|
||||
end
|
||||
local chosen = chosenSet(draft.ingredients)
|
||||
for _, fillType in ipairs(context.fillTypes or {}) do
|
||||
table.insert(rows, check("ingredients", fillType.name, " " .. tostring(fillType.title), chosen[fillType.name]))
|
||||
end
|
||||
if #(context.fillTypes or {}) == 0 then table.insert(rows, text("spu_guide_flow_noGoodsTo")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsStore(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
|
||||
{label = L("spu_guide_flow_storeAutoChoice"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeAuto))
|
||||
if draft.storeAuto then
|
||||
table.insert(rows, text("spu_guide_flow_storeAutoText"))
|
||||
return rows
|
||||
end
|
||||
local chosen = chosenSet(draft.storeBuildingIds)
|
||||
for _, building in ipairs(guide.context.storeBuildings or {}) do
|
||||
table.insert(rows, check("storeBuildingIds", building.id, building.text, chosen[building.id]))
|
||||
end
|
||||
if #(guide.context.storeBuildings or {}) == 0 then table.insert(rows, text("spu_noBuildings")) end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function productPolicies(draft)
|
||||
local policies = {}
|
||||
for _, product in ipairs(draft.products or {}) do table.insert(policies, product.policy) end
|
||||
return policies
|
||||
end
|
||||
|
||||
-- Kun regler varernes politik bruger: rent lager-flow viser ikke salgsgrænser og omvendt (in-game 29/9).
|
||||
local function rowsRules(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local rows = {}
|
||||
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
||||
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||
return rows
|
||||
end
|
||||
local loops = {{label = L("spu_loops_infinite"), value = 0}}
|
||||
for count = 1, ADRuns.MAX_LOOPS do table.insert(loops, {label = T(tostring(count)), value = count}) end
|
||||
return {
|
||||
choice("loops", L("spu_loops"), loops, draft.loops),
|
||||
choice("returnBelowPercent", L("spu_returnBelow"), percentOptions(ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10), draft.returnBelowPercent),
|
||||
}
|
||||
end
|
||||
|
||||
local function rowsWait(guide)
|
||||
local options = {{label = L("spu_waitPool_all"), value = ""}}
|
||||
for _, pool in ipairs(guide.context.waitPools or {}) do
|
||||
table.insert(options, {label = T(string.format("%s (%d)", tostring(pool.group), pool.count or 0)), value = pool.group})
|
||||
end
|
||||
return {choice("waitPoolGroup", L("spu_waitPool"), options, guide.draft.waitPoolGroup or "")}
|
||||
end
|
||||
|
||||
-- Vognvalg under en valgt solo-traktor: dens egen vogn + ledige vogne ingen anden traktor i flowet har.
|
||||
local function rowWagon(guide, tractorId)
|
||||
local wagons = guide.draft.wagons or {}
|
||||
local usedByOthers = {}
|
||||
for otherId, wagonId in pairs(wagons) do
|
||||
if otherId ~= tractorId then usedByOthers[wagonId] = true end
|
||||
end
|
||||
local options = {}
|
||||
for _, wagon in ipairs(guide.context.wagons or {}) do
|
||||
if not usedByOthers[wagon.id] then table.insert(options, {label = T(tostring(wagon.label)), value = wagon.id}) end
|
||||
end
|
||||
if #options == 0 or wagons[tractorId] == nil then return text("spu_guide_flow_noFreeWagon") end
|
||||
return choice("wagon", L("spu_guide_flow_wagon"), options, wagons[tractorId], tractorId)
|
||||
end
|
||||
|
||||
-- Traktorer: flowets egne først, så de ledige. Traktorer i andre flows og blokerede (vognen passer ikke)
|
||||
-- skjules med en linje hver.
|
||||
local function rowsTractors(guide)
|
||||
local draft, rows = guide.draft, {}
|
||||
local inRun = chosenSet(draft.vehicleIds)
|
||||
local entries = {}
|
||||
local other, blocked = 0, 0
|
||||
local blockedTexts = {}
|
||||
for _, vehicle in ipairs(guide.context.vehicles or {}) do
|
||||
local state = "free"
|
||||
if inRun[vehicle.id] then
|
||||
state = "in"
|
||||
elseif vehicle.otherRunId ~= nil and vehicle.otherRunId ~= draft.id then
|
||||
state = "other"
|
||||
other = other + 1
|
||||
elseif vehicle.blocked then
|
||||
state = "blocked"
|
||||
blocked = blocked + 1
|
||||
table.insert(blockedTexts, tostring(vehicle.text))
|
||||
end
|
||||
table.insert(entries, {id = vehicle.id, label = tostring(vehicle.text), state = state})
|
||||
end
|
||||
local solo = soloIds(guide.context)
|
||||
for _, entry in ipairs(ADFlowView.vehicleRows(entries, false)) do
|
||||
table.insert(rows, check("vehicleIds", entry.id, entry.label, entry.state == "in"))
|
||||
if entry.state == "in" and solo[entry.id] then
|
||||
local wagonRow = rowWagon(guide, entry.id)
|
||||
if wagonRow ~= nil then table.insert(rows, wagonRow) end
|
||||
end
|
||||
end
|
||||
if #rows == 0 then table.insert(rows, text("spu_guide_flow_noTractors")) end
|
||||
if other > 0 then table.insert(rows, text("spu_guide_flow_otherFlows", {other})) end
|
||||
if blocked > 0 then
|
||||
-- foldet sammen som standard; linjen med antallet folder navn + grund ud/ind
|
||||
local key = guide.showBlocked and "spu_guide_flow_blockedHide" or "spu_guide_flow_blockedShow"
|
||||
table.insert(rows, {type = "link", id = "showBlocked", value = true, label = L(key, {blocked})})
|
||||
if guide.showBlocked then
|
||||
table.sort(blockedTexts)
|
||||
for _, line in ipairs(blockedTexts) do table.insert(rows, {type = "text", label = T(" " .. line)}) end
|
||||
end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local function rowsSummary(guide)
|
||||
local rows = {}
|
||||
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local ROWS = {
|
||||
name = rowsName, direction = rowsDirection, buildings = rowsBuildings, goods = rowsGoods, store = rowsStore,
|
||||
rules = rowsRules, wait = rowsWait, tractors = rowsTractors, summary = rowsSummary,
|
||||
}
|
||||
|
||||
function ADFlowGuide.rows(guide)
|
||||
local builder = ROWS[guide.step]
|
||||
return builder ~= nil and builder(guide) or {}
|
||||
end
|
||||
|
||||
-- ---- resumé ----
|
||||
|
||||
-- Korte navne til resuméet (field, ellers navn, ellers rækkens tekst).
|
||||
local function namesFor(ids, entries, field)
|
||||
local byId = {}
|
||||
for _, entry in ipairs(entries or {}) do byId[entry.id or entry.name] = entry[field] or entry.name or entry.text end
|
||||
local names = {}
|
||||
for _, id in ipairs(ids or {}) do table.insert(names, tostring(byId[id] or id)) end
|
||||
return names
|
||||
end
|
||||
|
||||
local SUMMARY = {
|
||||
name = function(guide) return {T(guide.draft.name or "")} end,
|
||||
direction = function(guide) return {L(isOut(guide) and "spu_direction_out" or "spu_direction_in")} end,
|
||||
buildings = function(guide)
|
||||
local names = namesFor(buildingIds(guide.draft), guide.context.buildings, "name")
|
||||
if #names == 0 then return {L("spu_guide_flow_none")} end
|
||||
return {T(ADFlowView.joinCounted(names, " + ", ""))}
|
||||
end,
|
||||
goods = function(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local parts = {}
|
||||
local titles = {}
|
||||
for _, product in ipairs(guide.context.products or {}) do titles[product.name] = product.title end
|
||||
for _, product in ipairs(draft.products or {}) do
|
||||
table.insert(parts, {key = "spu_policy_" .. product.policy, prefix = tostring(titles[product.fillType] or product.fillType) .. ": "})
|
||||
end
|
||||
if #parts == 0 then return {L("spu_guide_flow_none")} end
|
||||
return parts
|
||||
end
|
||||
if draft.ingredientMode ~= "manual" then return {L("spu_guide_flow_goodsAuto")} end
|
||||
if #draft.ingredients == 0 then return {L("spu_guide_flow_none")} end
|
||||
if #draft.ingredients > ADFlowGuide.MAX_NAMED_GOODS then return {L("spu_guide_flow_sumGoods", {#draft.ingredients})} end
|
||||
local byName = {}
|
||||
for _, fillType in ipairs(guide.context.fillTypes or {}) do byName[fillType.name] = fillType.title end
|
||||
local names = {}
|
||||
for _, name in ipairs(draft.ingredients) do table.insert(names, tostring(byName[name] or name)) end
|
||||
return {T(table.concat(names, " + "))}
|
||||
end,
|
||||
store = function(guide)
|
||||
local draft = guide.draft
|
||||
if draft.storeAuto or #draft.storeBuildingIds == 0 then return {L("spu_guide_sum_auto")} end
|
||||
if #draft.storeBuildingIds > 2 then return {L("spu_guide_sum_stores", {#draft.storeBuildingIds})} end
|
||||
return {T(table.concat(namesFor(draft.storeBuildingIds, guide.context.storeBuildings, "name"), " + "))}
|
||||
end,
|
||||
rules = function(guide)
|
||||
local draft = guide.draft
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||
return parts
|
||||
end
|
||||
local loops = (draft.loops or 0) > 0 and L("spu_guide_flow_sumLoops", {draft.loops}) or L("spu_guide_flow_sumLoopsInf")
|
||||
return {loops, L("spu_guide_flow_sumReturn", {draft.returnBelowPercent})}
|
||||
end,
|
||||
wait = function(guide)
|
||||
local group = guide.draft.waitPoolGroup or ""
|
||||
return {group == "" and L("spu_waitPool_all") or T(group)}
|
||||
end,
|
||||
tractors = function(guide)
|
||||
local ids = guide.draft.vehicleIds
|
||||
if #ids == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||
if #ids > ADFlowGuide.MAX_NAMED_TRACTORS then return {L("spu_guide_flow_sumTractors", {#ids})} end
|
||||
-- traktorer der ikke findes (solgt, eller ikke indlæst endnu) vises ikke som rå id
|
||||
local known = {}
|
||||
for _, vehicle in ipairs(guide.context.vehicles or {}) do known[vehicle.id] = true end
|
||||
local present = {}
|
||||
for _, id in ipairs(ids) do if known[id] then table.insert(present, id) end end
|
||||
if #present == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||
return {T(table.concat(namesFor(present, guide.context.vehicles, "label"), ", "))}
|
||||
end,
|
||||
}
|
||||
|
||||
-- Resumé-linjer {step, title, parts}: Navn (altid) + de synlige trin.
|
||||
function ADFlowGuide.summary(guide)
|
||||
local lines = {}
|
||||
for _, step in ipairs(ADFlowGuide.ORDER) do
|
||||
local build = SUMMARY[step]
|
||||
if build ~= nil and (step == "name" or ADFlowGuide.isVisible(step, guide)) then
|
||||
table.insert(lines, {step = step, title = L(ADFlowGuide.stepKey(step, guide)), parts = build(guide)})
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- gem ----
|
||||
|
||||
function ADFlowGuide.result(guide)
|
||||
local draft = guide.draft
|
||||
local storeIds = ADGuide.deepCopy(draft.storeBuildingIds)
|
||||
if ADFlowGuide.isVisible("store", guide) and draft.storeAuto then storeIds = {} end
|
||||
local changes = {
|
||||
kind = draft.kind, sourceBuildingIds = ADGuide.deepCopy(draft.sourceBuildingIds),
|
||||
targetBuildingIds = ADGuide.deepCopy(draft.targetBuildingIds), storeBuildingIds = storeIds,
|
||||
products = ADGuide.deepCopy(draft.products), ingredientMode = draft.ingredientMode,
|
||||
ingredients = ADGuide.deepCopy(draft.ingredients), sellAtPercent = draft.sellAtPercent,
|
||||
storeAbovePercent = draft.storeAbovePercent, sellAbovePercent = draft.sellAbovePercent or 0,
|
||||
collectRound = draft.collectRound, loops = draft.loops,
|
||||
returnBelowPercent = draft.returnBelowPercent, waitPoolGroup = draft.waitPoolGroup or "",
|
||||
}
|
||||
if draft.name ~= nil and draft.name:match("%S") then changes.name = draft.name end
|
||||
return {isNew = draft.isNew == true, runId = draft.id, name = draft.name, kind = draft.kind, changes = changes,
|
||||
vehicleIds = ADGuide.deepCopy(draft.vehicleIds), wagons = ADFlowGuide.resultWagons(draft)}
|
||||
end
|
||||
|
||||
-- Vogne for de valgte traktorer (til run.wagons ved gem).
|
||||
function ADFlowGuide.resultWagons(draft)
|
||||
local chosen, wagons = chosenSet(draft.vehicleIds), {}
|
||||
for tractorId, wagonId in pairs(draft.wagons or {}) do
|
||||
if chosen[tractorId] then wagons[tractorId] = wagonId end
|
||||
end
|
||||
return wagons
|
||||
end
|
||||
|
||||
-- Traktorer der skal tildeles/fjernes efter gem.
|
||||
function ADFlowGuide.vehicleChanges(oldIds, newIds)
|
||||
local old, new = chosenSet(oldIds), chosenSet(newIds)
|
||||
local added, removed = {}, {}
|
||||
for _, id in ipairs(newIds or {}) do if not old[id] then table.insert(added, id) end end
|
||||
for _, id in ipairs(oldIds or {}) do if not new[id] then table.insert(removed, id) end end
|
||||
return added, removed
|
||||
end
|
||||
@@ -101,18 +101,27 @@ end
|
||||
-- Bygninger: buildings = {{id, name, hasMarker}}, chosen = {[id] = true}. Valgte først; bygninger uden
|
||||
-- markør kun hvis valgte eller showAll (de kan alligevel ikke bruges før en markør er sat).
|
||||
function ADFlowView.buildingRows(buildings, chosen, showAll)
|
||||
local picked, withMarker, withoutMarker = {}, {}, {}
|
||||
local shown, withoutMarker = {}, {}
|
||||
for _, building in ipairs(buildings or {}) do
|
||||
if chosen ~= nil and chosen[building.id] then
|
||||
table.insert(picked, building)
|
||||
elseif building.hasMarker then
|
||||
table.insert(withMarker, building)
|
||||
if (chosen ~= nil and chosen[building.id]) or building.hasMarker then
|
||||
table.insert(shown, building)
|
||||
elseif showAll then
|
||||
table.insert(withoutMarker, building)
|
||||
end
|
||||
end
|
||||
-- fast rækkefølge: bygningsnavn, så markørnavn (label) — et tryk må ikke flytte rækken under markøren
|
||||
local function byNameThenLabel(list)
|
||||
local copy = {}
|
||||
for _, item in ipairs(list) do table.insert(copy, item) end
|
||||
table.sort(copy, function(a, b)
|
||||
if a.name ~= b.name then return ADFlowView.naturalLess(a.name, b.name) end
|
||||
if (a.label or "") ~= (b.label or "") then return ADFlowView.naturalLess(a.label or "", b.label or "") end
|
||||
return tostring(a.id) < tostring(b.id)
|
||||
end)
|
||||
return copy
|
||||
end
|
||||
local rows = {}
|
||||
for _, list in ipairs({sortedBy(picked, "name"), sortedBy(withMarker, "name"), sortedBy(withoutMarker, "name")}) do
|
||||
for _, list in ipairs({byNameThenLabel(shown), byNameThenLabel(withoutMarker)}) do
|
||||
for _, building in ipairs(list) do table.insert(rows, building) end
|
||||
end
|
||||
return rows
|
||||
@@ -138,3 +147,53 @@ function ADFlowView.joinCounted(names, separator, empty)
|
||||
end
|
||||
return table.concat(parts, separator)
|
||||
end
|
||||
|
||||
-- Lang tekst -> linjer på højst maxChars (ombrydes ved mellemrum; indrykning gentages på hver linje,
|
||||
-- så en indrykket forklaring forbliver indrykket). Ét ord længere end linjen deles ikke.
|
||||
function ADFlowView.wrapText(text, maxChars)
|
||||
text = tostring(text or "")
|
||||
if #text <= maxChars then return {text} end
|
||||
local indent = string.match(text, "^%s*") or ""
|
||||
local lines, line = {}, indent
|
||||
for word in string.gmatch(string.sub(text, #indent + 1), "%S+") do
|
||||
if line ~= indent and #line + 1 + #word > maxChars then
|
||||
table.insert(lines, line)
|
||||
line = indent .. word
|
||||
else
|
||||
line = line == indent and (indent .. word) or (line .. " " .. word)
|
||||
end
|
||||
end
|
||||
if line ~= indent then table.insert(lines, line) end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- ---- 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"
|
||||
-- Hvilke udkørselsregler betyder noget for varernes politik? policies = {"store"|"sell"|"sellElseStore"|"off"}
|
||||
-- -> sells, stores. Ingen varer med salg/lager endnu = begge (reglerne kan sættes før varerne vælges).
|
||||
function ADFlowView.policyUse(policies)
|
||||
local sells, stores = false, false
|
||||
for _, policy in ipairs(policies or {}) do
|
||||
if policy == "sell" or policy == "sellElseStore" then sells = true end
|
||||
if policy == "store" or policy == "sellElseStore" then stores = true end
|
||||
end
|
||||
if not sells and not stores then return true, true end
|
||||
return sells, stores
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
-- AD Flow Wagons
|
||||
-- Flows ejer deres vogne, men traktor og vogn er ikke låst sammen: den vogn der hænger på en flow-traktor huskes
|
||||
-- (run.wagons = {[traktorId] = vognId}). Starter flowet med en solo-traktor, henter den sin vogn ved "Connect <vognens
|
||||
-- parkering>". Stoppes/slettes flowet, eller er dets bygning væk, sættes vognen på plads og traktoren kører hjem —
|
||||
-- så er begge ledige i maskinpuljen (en stoppet flows vogn må lånes; en kørende flows aldrig).
|
||||
|
||||
ADFlowWagons = {}
|
||||
ADFlowWagons.LOG_PREFIX = "ADSmartPickup: flow-vogne: "
|
||||
ADFlowWagons.REMEMBER_MS = 10000 -- husk vogne / prøv ventende starter igen
|
||||
ADFlowWagons.BUILDING_CHECK_MS = 60000
|
||||
ADFlowWagons.MISSING_CHECKS = 2 -- så mange tjek i træk før en bygning regnes for væk
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
ADFlowWagons.serialize = function(wagons) return ADRuns.serializeWagons(wagons) end
|
||||
ADFlowWagons.parse = function(text) return ADRuns.parseWagons(text) end
|
||||
|
||||
-- Husk traktorens vogn: ny tabel ved ændring, samme tabel når intet ændres.
|
||||
function ADFlowWagons.remember(wagons, tractorId, wagonId)
|
||||
if (wagons or {})[tractorId] == wagonId then return wagons end
|
||||
local copy = ADRuns.copyWagons(wagons)
|
||||
copy[tractorId] = wagonId
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Må redskabet bag traktoren huskes som flowets vogn? Kun noget der kan rumme last — ikke plov/kultivator (ingen
|
||||
-- plads) og ikke såmaskine (frøtank). s = {capacity, isSowingMachine}. in-game 30/9: 'Sell Slurry 2' huskede ploven.
|
||||
function ADFlowWagons.isRememberableWagon(s)
|
||||
return (s.capacity or 0) > 0 and not s.isSowingMachine
|
||||
end
|
||||
|
||||
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
|
||||
function ADFlowWagons.startAction(s)
|
||||
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
|
||||
if s.tractorBusy then return "wait" end
|
||||
if s.hasRear or s.wagonId == nil or not s.wagonExists or not s.hasConnect then return "start" end
|
||||
if s.wagonAttached or s.wagonBusy then return "wait" end
|
||||
return "fetch"
|
||||
end
|
||||
|
||||
-- Hent vogn fejlede: ét nyt forsøg når AD standsede undervejs (forbigående, fx spærret af en anden traktor);
|
||||
-- ikke ved kobling der fejler ved markøren (den skal rettes).
|
||||
ADFlowWagons.FETCH_RETRIES = 1
|
||||
function ADFlowWagons.retryFetch(reason, retries)
|
||||
return tostring(reason):find("^stoppedAway") ~= nil and (retries or 0) < ADFlowWagons.FETCH_RETRIES
|
||||
end
|
||||
|
||||
-- s = {flowRunning, ownWagon, adActive} -> start traktoren efter spillerens egen påkobling?
|
||||
function ADFlowWagons.startAfterManualAttach(s)
|
||||
return s.flowRunning == true and s.ownWagon == true and not s.adActive
|
||||
end
|
||||
|
||||
-- Husk vognen bag traktoren ved hjemtur? Kun når et KØRENDE flow stoppes (reason "stop") — ved oprydning efter
|
||||
-- indlæsning ("afterLoad") kan traktoren have et puljeredskab på fra et lån (in-game 29/9).
|
||||
function ADFlowWagons.shouldRememberOnHome(reason)
|
||||
return reason == "stop"
|
||||
end
|
||||
|
||||
-- Påkobling uden ejer (hentet før genindlæsning, markopgaven tabt): s = {owned, idleMs} -> "home"|"wait"|"none"
|
||||
ADFlowWagons.ORPHAN_MS = 120000
|
||||
-- s.atOwnPark: traktoren holder på sin egen parkering = parkeret sæt, ikke forældreløs (vinmarks-REX, 29/9)
|
||||
ADFlowWagons.OWN_PARK_M = 30
|
||||
function ADFlowWagons.orphanAction(s)
|
||||
if s.owned or s.atOwnPark then return "none" end
|
||||
return (s.idleMs or 0) >= ADFlowWagons.ORPHAN_MS and "home" or "wait"
|
||||
end
|
||||
|
||||
-- Hvorfor "wait" (til loggen). s som startAction.
|
||||
function ADFlowWagons.waitReason(s)
|
||||
if s.tractorBusy then return "tractorBusy" end
|
||||
if s.wagonAttached then return "wagonAttached" end
|
||||
return "wagonReserved"
|
||||
end
|
||||
|
||||
-- Ingen husket vogn: gæt ud fra parkeringsnavnet — en ledig vogn hvis parkering indeholder flowets navn, helst med
|
||||
-- traktorens nummer til sidst ("Cow Food 3" -> "P Cow Food vogn 3"). candidates = {{id, parkName}}, taken = {[id]=true}
|
||||
function ADFlowWagons.guessWagon(runName, tractorNumber, candidates, taken)
|
||||
local wanted = tostring(runName or ""):lower()
|
||||
if wanted == "" then return nil end
|
||||
local matching = {}
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
local park = tostring(candidate.parkName or ""):lower()
|
||||
if not (taken or {})[candidate.id] and park:find(wanted, 1, true) ~= nil then table.insert(matching, candidate) end
|
||||
end
|
||||
table.sort(matching, function(a, b) return tostring(a.parkName) < tostring(b.parkName) end)
|
||||
for _, candidate in ipairs(matching) do
|
||||
if tractorNumber ~= nil and tostring(candidate.parkName):match("%s(%d+)$") == tostring(tractorNumber) then return candidate.id end
|
||||
end
|
||||
return matching[1] ~= nil and matching[1].id or nil
|
||||
end
|
||||
|
||||
-- Ledige vogne til flow-guiden, bedste først: parkering med flowets navn, så "Disponibel", så resten (alfabetisk).
|
||||
-- candidates = {{id, label}} -> ny sorteret liste.
|
||||
function ADFlowWagons.rankWagons(runName, candidates)
|
||||
local wanted = tostring(runName or ""):lower()
|
||||
local function rank(wagon)
|
||||
local label = tostring(wagon.label or ""):lower()
|
||||
if wanted ~= "" and label:find(wanted, 1, true) ~= nil then return 1 end
|
||||
if label:find("disponibel", 1, true) ~= nil then return 2 end
|
||||
return 3
|
||||
end
|
||||
local ranked = {}
|
||||
for _, wagon in ipairs(candidates or {}) do table.insert(ranked, wagon) end
|
||||
table.sort(ranked, function(a, b)
|
||||
local ra, rb = rank(a), rank(b)
|
||||
if ra ~= rb then return ra < rb end
|
||||
return tostring(a.label) < tostring(b.label)
|
||||
end)
|
||||
return ranked
|
||||
end
|
||||
|
||||
function ADFlowWagons.ownerOf(runs, wagonId)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
for _, id in pairs(run.wagons or {}) do
|
||||
if id == wagonId then return run end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Holdes vognen af et flow? Flowets vogn er flowets, også når flowet er stoppet — puljen, markarbejde og
|
||||
-- marksilo må ikke låne den (in-game 29/9: marksilo lånte Cow Food-traktoren med vognen på efter stop).
|
||||
function ADFlowWagons.isHeld(runs, wagonId)
|
||||
return ADFlowWagons.ownerOf(runs, wagonId) ~= nil
|
||||
end
|
||||
|
||||
-- Et tjek af om bygningen mangler: -> ny tæller, væk?
|
||||
function ADFlowWagons.missingStep(count, missingNow)
|
||||
if not missingNow then return 0, false end
|
||||
local next = (count or 0) + 1
|
||||
return next, next >= ADFlowWagons.MISSING_CHECKS
|
||||
end
|
||||
|
||||
-- Flowets bygnings-id'er der ikke findes i exists = {[id] = true}. Kun bygninger der er set i denne session
|
||||
-- (seen) tæller: et forældet id i et gammelt flow må ikke stoppe et flow der virker.
|
||||
function ADFlowWagons.missingBuildings(run, exists, seen)
|
||||
local missing = {}
|
||||
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds"}) do
|
||||
for _, id in ipairs(run[key] or {}) do
|
||||
if not exists[id] and (seen or {})[id] then table.insert(missing, id) end
|
||||
end
|
||||
end
|
||||
return missing
|
||||
end
|
||||
|
||||
-- Hvor læsset afleveres ved stop: AD's 2. markør kun hvis den er et af flowets mål — den kan være forældet
|
||||
-- (gammel AD-tilstand) eller et Wait-punkt, Wait-logikken har skiftet til. Ellers flowets første mål; nil = silo.
|
||||
function ADFlowWagons.emptyTarget(secondMarkerId, runTargetIds)
|
||||
local targets = runTargetIds or {}
|
||||
for _, id in ipairs(targets) do
|
||||
if id == secondMarkerId then return id end
|
||||
end
|
||||
return targets[1]
|
||||
end
|
||||
|
||||
-- Tøm vognen før den parkeres (flowet stoppet): flowets mål -> silo/egen fabrik -> hjem.
|
||||
-- stage: "start" | "target" | "dump"; s = {cargo, hasTarget, adActive, elapsedMs}
|
||||
-- -> "home" | "target" | "dump" | "wait" | "giveUp"
|
||||
ADFlowWagons.EMPTY_MIN_LITERS = 100
|
||||
ADFlowWagons.EMPTY_TIMEOUT_MS = 600000
|
||||
ADFlowWagons.EMPTY_STALL_MS = 45000 -- ved målet uden at læsset falder så længe = målet er fuldt
|
||||
ADFlowWagons.EMPTY_NEAR_M = 40 -- m fra aflæsningsmarkøren tæller som "ved målet"
|
||||
ADFlowWagons.MAX_DUMP_ATTEMPTS = 3 -- siloer der prøves før vognen parkeres med last
|
||||
|
||||
-- s = {nearTarget, sinceChangeMs}
|
||||
function ADFlowWagons.isStalled(s)
|
||||
return s.nearTarget == true and (s.sinceChangeMs or 0) >= ADFlowWagons.EMPTY_STALL_MS
|
||||
end
|
||||
|
||||
function ADFlowWagons.emptyStep(stage, s)
|
||||
local cargo = s.cargo or 0
|
||||
if stage == "start" then
|
||||
if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end
|
||||
return s.hasTarget and "target" or "dump"
|
||||
end
|
||||
if (s.elapsedMs or 0) >= ADFlowWagons.EMPTY_TIMEOUT_MS then return "giveUp" end
|
||||
if s.stalled and cargo >= ADFlowWagons.EMPTY_MIN_LITERS then
|
||||
if stage == "target" then return "dump" end
|
||||
-- silo der ikke kunne tage imod: prøv den næste (op til MAX_DUMP_ATTEMPTS)
|
||||
return (s.dumpAttempts or 0) < ADFlowWagons.MAX_DUMP_ATTEMPTS and "dump" or "giveUp"
|
||||
end
|
||||
if s.adActive then return "wait" end
|
||||
if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end
|
||||
if stage == "target" then return "dump" end
|
||||
return "giveUp"
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local waiting = setmetatable({}, {__mode = "k"}) -- traktor -> runId (venter på sin vogn)
|
||||
local fetching = setmetatable({}, {__mode = "k"}) -- traktor -> runId (henter sin vogn)
|
||||
local emptying = setmetatable({}, {__mode = "k"}) -- traktor -> {stage, since, fillType, runName}
|
||||
local homing = setmetatable({}, {__mode = "k"}) -- traktor -> true (flowets hjemtur: vogn af, traktor hjem)
|
||||
local fetchRetries = setmetatable({}, {__mode = "k"}) -- traktor -> antal nye hente-forsøg
|
||||
local lastWaitText = setmetatable({}, {__mode = "k"}) -- traktor -> sidst loggede venteårsag (kun ændringer logges)
|
||||
local orphans = setmetatable({}, {__mode = "k"}) -- traktor -> ms siden påkobling uden ejer
|
||||
local missingCount = {} -- runId -> tjek i træk
|
||||
local seenBuildings = {} -- bygnings-id'er set i denne session
|
||||
local rememberTimer, buildingTimer = 0, 0
|
||||
|
||||
local function runs()
|
||||
return ADRunsController.store ~= nil and ADRunsController.store.runs or {}
|
||||
end
|
||||
|
||||
local function findAnyVehicle(id)
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
if ADRunsController.getVehicleId(vehicle) == id then return vehicle end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function connectOf(adEnv, wagon)
|
||||
local index = wagon.advd ~= nil and wagon.advd.parkDestination or nil
|
||||
local park = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
if park == nil then return nil end
|
||||
local marker = ADPool.connectFor(park.name, adEnv.ADGraphManager:getMapMarkers())
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
-- Traktorer i stoppede flows er ledige i puljen; traktorer i kørende flows aldrig.
|
||||
-- s = {hasRun, stopped, carriesFlowWagon, homeTrip} -> må puljen/markarbejde/marksilo låne traktoren?
|
||||
function ADFlowWagons.tractorFree(s)
|
||||
if not s.hasRun then return true end
|
||||
return s.stopped == true and not s.carriesFlowWagon and not s.homeTrip
|
||||
end
|
||||
|
||||
function ADFlowWagons.isFreeTractor(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local carries = false
|
||||
if run ~= nil then
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(vehicle)) do
|
||||
local wagonId = ADRunsController.getVehicleId(implement.object)
|
||||
if wagonId ~= nil and ADFlowWagons.ownerOf(runs(), wagonId) ~= nil then carries = true end
|
||||
end
|
||||
end
|
||||
return ADFlowWagons.tractorFree({hasRun = run ~= nil, stopped = run ~= nil and run.stopped == true,
|
||||
carriesFlowWagon = carries, homeTrip = emptying[vehicle] ~= nil or homing[vehicle] ~= nil})
|
||||
end
|
||||
|
||||
-- Holder flowet noget: en traktor henter/venter på sin vogn eller har en vogn bagpå (til Stop-knappen).
|
||||
function ADFlowWagons.hasActivity(run, vehiclesById)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = vehiclesById ~= nil and vehiclesById[vehicleId] or nil
|
||||
-- vogn bagpå tæller kun i et flow der er startet: et flow der aldrig kom i gang (fx 'ingen vogn', og spilleren
|
||||
-- har selv koblet på) skal vise Start, ikke Stop (in-game 29/9: Milk Store)
|
||||
local holdsWagon = vehicle ~= nil and ADRunsController.runningRuns[run.id] == true and #ADHitch.rearImplementsOf(vehicle) > 0
|
||||
if vehicle ~= nil and (waiting[vehicle] ~= nil or fetching[vehicle] ~= nil or holdsWagon) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADFlowWagons.hasWagonFor(run, tractorId)
|
||||
return (run.wagons or {})[tractorId] ~= nil
|
||||
end
|
||||
|
||||
function ADFlowWagons.isHeldWagon(wagonId)
|
||||
return ADFlowWagons.isHeld(runs(), wagonId)
|
||||
end
|
||||
|
||||
-- Frakoblede vogne med parkering, som intet kørende flow holder, og som flowet ikke allerede har givet en traktor.
|
||||
local function guessFor(adEnv, run, vehicle, tractorId)
|
||||
local taken = {}
|
||||
for _, id in pairs(run.wagons or {}) do taken[id] = true end
|
||||
local candidates = {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
local attached = any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil
|
||||
if any.spec_motorized == nil and any.spec_attachable ~= nil and not attached and type(index) == "number" and index >= 1
|
||||
and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then
|
||||
local id = ADRunsController.getVehicleId(any)
|
||||
local park = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
if id ~= nil and park ~= nil and not ADFlowWagons.isHeld(runs(), id) then
|
||||
table.insert(candidates, {id = id, parkName = park.name})
|
||||
end
|
||||
end
|
||||
end
|
||||
local number = nil
|
||||
for index, id in ipairs(run.vehicleIds or {}) do
|
||||
if id == tractorId then number = index end
|
||||
end
|
||||
local label = tostring(ADRuns.vehicleLabel(vehicle))
|
||||
number = tonumber(label:match("%s(%d+)$")) or number
|
||||
local wagonId = ADFlowWagons.guessWagon(run.name, number, candidates, taken)
|
||||
if wagonId ~= nil then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' får vognen ved parkeringen der matcher flowets navn", tostring(run.name), label)
|
||||
end
|
||||
return wagonId
|
||||
end
|
||||
|
||||
-- Flowet startes igen mens traktoren tømmer/kører hjem efter et stop: afbryd, så den kører videre med sin vogn.
|
||||
-- Puljens egen hjemtur (fra markarbejde) røres ikke — der sidder et andet redskab bagpå.
|
||||
-- s = {emptying, flowHoming, poolReturning} -> true = afbryd
|
||||
function ADFlowWagons.abortHomeTripOnStart(s)
|
||||
return s.emptying == true or s.flowHoming == true
|
||||
end
|
||||
|
||||
-- Hvilken af isPoolBusy's grunde (til loggen).
|
||||
function ADFlowWagons.busyWhy(vehicle, tractorId)
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil then return "markopgave" end
|
||||
if ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle) then return "lånt af marksilo" end
|
||||
if ADPool ~= nil and ADPool.isReturning(vehicle) then return "sætter redskab af" end
|
||||
if ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle) then return "Courseplay kører" end
|
||||
if emptying[vehicle] ~= nil then return "tømmer vognen" end
|
||||
return "?"
|
||||
end
|
||||
|
||||
-- Lånt ud af puljen: markarbejde, marksilo-lån, på hjemtur eller Courseplay kører den.
|
||||
function ADFlowWagons.isPoolBusy(vehicle, tractorId)
|
||||
return (ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil)
|
||||
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle))
|
||||
or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||
or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle))
|
||||
or (ADFlowWagons.isEmptying ~= nil and ADFlowWagons.isEmptying(vehicle))
|
||||
end
|
||||
|
||||
-- Før en flow-traktor startes: hent sin vogn / vent på den. -> håndteret, resultat (som startVehicle)
|
||||
function ADFlowWagons.beforeStart(adEnv, run, vehicle)
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
if fetching[vehicle] ~= nil then return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"} end
|
||||
if ADFlowWagons.abortHomeTripOnStart({emptying = emptying[vehicle] ~= nil, flowHoming = homing[vehicle] ~= nil}) then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' startet igen — tømning/hjemtur afbrudt, kører videre med vognen",
|
||||
tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
emptying[vehicle] = nil
|
||||
if homing[vehicle] ~= nil then
|
||||
homing[vehicle] = nil
|
||||
ADHitch.cancel(vehicle)
|
||||
end
|
||||
end
|
||||
local wagonId = (run.wagons or {})[tractorId]
|
||||
if wagonId == nil and #ADHitch.rearImplementsOf(vehicle) == 0 then
|
||||
wagonId = guessFor(adEnv, run, vehicle, tractorId)
|
||||
if wagonId ~= nil then run.wagons = ADFlowWagons.remember(run.wagons, tractorId, wagonId) end
|
||||
end
|
||||
local wagon = wagonId ~= nil and findAnyVehicle(wagonId) or nil
|
||||
local connectId = wagon ~= nil and connectOf(adEnv, wagon) or nil
|
||||
local startState = {
|
||||
tractorBusy = ADFlowWagons.isPoolBusy(vehicle, tractorId),
|
||||
hasRear = #ADHitch.rearImplementsOf(vehicle) > 0, wagonId = wagonId, wagonExists = wagon ~= nil,
|
||||
hasConnect = connectId ~= nil,
|
||||
wagonAttached = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() ~= nil,
|
||||
wagonBusy = wagonId ~= nil and ADPool.isReserved(wagonId),
|
||||
}
|
||||
local action = ADFlowWagons.startAction(startState)
|
||||
if action == "start" then lastWaitText[vehicle] = nil return false end
|
||||
ADRunsController.runningRuns[run.id] = true
|
||||
if action == "wait" then
|
||||
local holder = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() or nil
|
||||
local waitText = ADFlowWagons.waitReason(startState) .. ": " .. (startState.tractorBusy and ADFlowWagons.busyWhy(vehicle, tractorId)
|
||||
or (holder ~= nil and ("sidder på " .. tostring(ADRuns.vehicleLabel(holder)))) or "reserveret af puljen")
|
||||
if lastWaitText[vehicle] ~= waitText then
|
||||
lastWaitText[vehicle] = waitText
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' venter på sin vogn (%s)", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), waitText)
|
||||
end
|
||||
waiting[vehicle] = run.id
|
||||
return true, {vehicle = vehicle, ok = true, reason = "waitingWagon"}
|
||||
end
|
||||
waiting[vehicle] = nil
|
||||
lastWaitText[vehicle] = nil
|
||||
fetching[vehicle] = run.id
|
||||
local runId = run.id
|
||||
local okFetch, errFetch = pcall(ADHitch.fetch, adEnv, vehicle, connectId, function(tractor, ok, detail)
|
||||
fetching[tractor] = nil
|
||||
local current = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if current == nil or current.stopped then
|
||||
ADHitch.returnHome(adEnv, tractor)
|
||||
return
|
||||
end
|
||||
if ok then
|
||||
fetchRetries[tractor] = nil
|
||||
local result = ADRunsController.startVehicle(adEnv, current, tractor)
|
||||
if result ~= nil and not result.ok then
|
||||
-- flowet kan ikke startes (markør, bygning …): ikke efterladt ved Connect med vognen
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke starte (%s) — vognen sættes tilbage",
|
||||
tostring(current.name), tostring(ADRuns.vehicleLabel(tractor)), tostring(result.reason))
|
||||
ADHitch.returnHome(adEnv, tractor)
|
||||
end
|
||||
elseif ADFlowWagons.retryFetch(detail, fetchRetries[tractor]) then
|
||||
fetchRetries[tractor] = (fetchRetries[tractor] or 0) + 1
|
||||
waiting[tractor] = runId -- retryWaiting starter den igen om lidt
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s) — prøver igen", tostring(current.name),
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(detail))
|
||||
else
|
||||
fetchRetries[tractor] = nil
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s)", tostring(current.name),
|
||||
tostring(ADRuns.vehicleLabel(tractor)), tostring(detail))
|
||||
end
|
||||
end)
|
||||
if not okFetch then
|
||||
fetching[vehicle] = nil
|
||||
return true, {vehicle = vehicle, ok = false, reason = "error"}
|
||||
end
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' henter sin vogn", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"}
|
||||
end
|
||||
|
||||
-- Stop/slet/bygning væk: vognen tømmes (flowets mål, ellers silo/egen fabrik), sættes på plads, traktoren hjem.
|
||||
local nowMs = 0
|
||||
|
||||
local function cargoOf(adEnv, vehicle)
|
||||
local level, fillType = 0, nil
|
||||
local ok, units = pcall(adEnv.AutoDrive.getAllDischargeableUnits, vehicle)
|
||||
for _, unit in ipairs(ok and units or {}) do
|
||||
local object, index = unit.object, unit.fillUnitIndex
|
||||
if object ~= nil and object.getFillUnitFillLevel ~= nil then
|
||||
local unitLevel = object:getFillUnitFillLevel(index) or 0
|
||||
if unitLevel > 0 then
|
||||
level = level + unitLevel
|
||||
fillType = fillType or object:getFillUnitFillType(index)
|
||||
end
|
||||
end
|
||||
end
|
||||
return level, fillType
|
||||
end
|
||||
|
||||
local function goHome(adEnv, vehicle)
|
||||
emptying[vehicle] = nil
|
||||
homing[vehicle] = true
|
||||
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle, function(tractor) homing[tractor] = nil end)
|
||||
if not (okHome and pooled) then
|
||||
homing[vehicle] = nil
|
||||
local park = ADFieldWork.parkMarkerId(adEnv, vehicle)
|
||||
if park ~= nil then pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
end
|
||||
end
|
||||
|
||||
local function fillName(fillType)
|
||||
local desc = fillType ~= nil and g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||||
return desc ~= nil and (desc.title or desc.name) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function startDump(adEnv, vehicle, state)
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local isReachable = function(markerId)
|
||||
return ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil or ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId)
|
||||
end
|
||||
-- marksiloer er ikke tømmesteder (de er til markens høst); siloer der allerede fejlede springes over
|
||||
local okSet, fieldSilos = pcall(function() return ADFieldSilos ~= nil and ADFieldSilos.fieldSiloPlaceableSet(adEnv) or nil end)
|
||||
state.triedMarkers = state.triedMarkers or {}
|
||||
local ok, marker, why = pcall(ADLoadSwap.findDumpMarker, adEnv, vehicle, fillType, cargo, nil, isReachable,
|
||||
okSet and fieldSilos or nil, state.triedMarkers, true)
|
||||
if not ok or marker == nil then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' har stadig %d l %s — intet sted at aflæsse (%s); parkeres med last",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(ok and why or marker))
|
||||
goHome(adEnv, vehicle)
|
||||
return
|
||||
end
|
||||
state.stage, state.since = "dump", nowMs
|
||||
state.markerId, state.markerName, state.lastCargo = marker.id, marker.name, nil
|
||||
state.dumpAttempts = (state.dumpAttempts or 0) + 1
|
||||
state.triedMarkers[marker.id] = true
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, marker.id, nil, fillType)
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' aflæsser resten (%d l %s) ved %s før parkering",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(marker.name))
|
||||
end
|
||||
|
||||
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows).
|
||||
-- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
|
||||
local function isCargoWagon(object)
|
||||
local _, capacity = ADPool.cargoOf(object)
|
||||
return ADFlowWagons.isRememberableWagon({capacity = capacity, isSowingMachine = object.spec_sowingMachine ~= nil})
|
||||
end
|
||||
|
||||
local function rememberWagonOf(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
if wagonId == nil or tractorId == nil then return end
|
||||
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
run.wagons = updated
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object)))
|
||||
end
|
||||
end
|
||||
|
||||
-- Én traktor hjem (fra et stoppet/slettet flow): tøm først, så parkering.
|
||||
-- reason: "stop" (et kørende flow stoppes: vognen huskes) | "afterLoad" (oprydning: intet huskes)
|
||||
function ADFlowWagons.homeVehicle(adEnv, vehicle, runName, reason)
|
||||
waiting[vehicle] = nil
|
||||
if ADFlowWagons.shouldRememberOnHome(reason or "stop") then
|
||||
rememberWagonOf(vehicle) -- vognen sættes af nu: flowet skal kunne finde den igen ved næste start
|
||||
end
|
||||
ADHitch.stow(vehicle, "flowet stoppet")
|
||||
if fetching[vehicle] ~= nil then
|
||||
fetching[vehicle] = nil
|
||||
ADHitch.cancel(vehicle)
|
||||
end
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local second = module ~= nil and module.getSecondMarker ~= nil and module:getSecondMarker() or nil
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local targetId = ADFlowWagons.emptyTarget(second ~= nil and second.id or nil, run ~= nil and run.targetWayPointIds or nil)
|
||||
local target = targetId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local action = ADFlowWagons.emptyStep("start", {cargo = cargo, hasTarget = target ~= nil})
|
||||
if action == "home" then return goHome(adEnv, vehicle) end
|
||||
local state = {stage = action, since = nowMs, runName = runName,
|
||||
markerId = target ~= nil and target.id or nil, markerName = target ~= nil and target.name or nil}
|
||||
emptying[vehicle] = state
|
||||
if action == "target" then
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, target.id, nil, fillType)
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' afleverer sit læs (%d l %s) ved %s før parkering",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(target.name))
|
||||
else
|
||||
startDump(adEnv, vehicle, state)
|
||||
end
|
||||
end
|
||||
|
||||
-- ADHitch: spilleren har selv kørt traktoren til Connect og koblet på.
|
||||
function ADFlowWagons.onManualAttach(adEnv, vehicle, wagon)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
orphans[vehicle] = 0 -- ryddes af tickOrphans når nogen ejer opgaven (flow, markopgave, AD, spilleren)
|
||||
if run == nil then return end
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
local wagonId = wagon ~= nil and ADRunsController.getVehicleId(wagon) or nil
|
||||
local remembered = (run.wagons or {})[tractorId]
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
-- ikke stoppet af spilleren = gælder også et flow hvis Start blev afvist ('ingen vogn') — det er derfor han henter vognen
|
||||
if not ADFlowWagons.startAfterManualAttach({flowRunning = not run.stopped,
|
||||
ownWagon = wagonId ~= nil and (remembered == nil or remembered == wagonId),
|
||||
adActive = module ~= nil and module:isActive()}) then return end
|
||||
fetchRetries[vehicle], waiting[vehicle], orphans[vehicle] = nil, nil, nil
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har selv hentet sin vogn — startes", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
end
|
||||
|
||||
-- Traktorer der har koblet et redskab på uden ejer: efter ORPHAN_MS sættes redskabet tilbage og traktoren kører hjem.
|
||||
local function tickOrphans(adEnv, elapsed)
|
||||
for vehicle, idle in pairs(orphans) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local owned = (module ~= nil and module:isActive()) or (vehicle.getIsControlled ~= nil and vehicle:getIsControlled())
|
||||
or (run ~= nil and not run.stopped) or (ADFieldJobs ~= nil and id ~= nil and ADFieldJobs.state.assignments[id] ~= nil)
|
||||
or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle)) or #ADHitch.rearImplementsOf(vehicle) == 0
|
||||
local atOwnPark = false
|
||||
local parkId = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
|
||||
local parkWp = parkId ~= nil and adEnv.ADGraphManager:getWayPointById(parkId) or nil
|
||||
if parkWp ~= nil and vehicle.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
atOwnPark = (x - parkWp.x) ^ 2 + (z - parkWp.z) ^ 2 <= ADFlowWagons.OWN_PARK_M ^ 2
|
||||
end
|
||||
local action = ADFlowWagons.orphanAction({owned = owned, atOwnPark = atOwnPark, idleMs = idle + elapsed})
|
||||
if action == "none" then
|
||||
orphans[vehicle] = nil
|
||||
elseif action == "home" then
|
||||
orphans[vehicle] = nil
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' har et redskab på uden opgave (hentet før genindlæsning?) — sættes tilbage og kører hjem",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
goHome(adEnv, vehicle)
|
||||
else
|
||||
orphans[vehicle] = idle + elapsed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFlowWagons.isEmptying(vehicle)
|
||||
return emptying[vehicle] ~= nil
|
||||
end
|
||||
|
||||
function ADFlowWagons.sendHome(adEnv, run)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
if vehicle ~= nil then
|
||||
local ok, err = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name)
|
||||
if not ok then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "hjem fejlede: %s", tostring(err))
|
||||
goHome(adEnv, vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Står traktoren ved tømningens markør, uden at læsset er faldet i EMPTY_STALL_MS?
|
||||
local function stalledAt(adEnv, vehicle, state, cargo)
|
||||
if state.lastCargo == nil or cargo < state.lastCargo - 1 then
|
||||
state.lastCargo, state.changedAt = cargo, nowMs
|
||||
end
|
||||
local wayPoint = state.markerId ~= nil and adEnv.ADGraphManager:getWayPointById(state.markerId) or nil
|
||||
local near = false
|
||||
if wayPoint ~= nil and vehicle.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
near = math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2) <= ADFlowWagons.EMPTY_NEAR_M
|
||||
end
|
||||
return ADFlowWagons.isStalled({nearTarget = near, sinceChangeMs = nowMs - (state.changedAt or nowMs)})
|
||||
end
|
||||
|
||||
local function tickEmptying(adEnv)
|
||||
local list = {}
|
||||
for vehicle, state in pairs(emptying) do table.insert(list, {vehicle = vehicle, state = state}) end
|
||||
for _, item in ipairs(list) do
|
||||
local vehicle, state = item.vehicle, item.state
|
||||
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local cargo, fillType = cargoOf(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if controlled then
|
||||
emptying[vehicle] = nil -- spilleren har overtaget
|
||||
else
|
||||
local stalled = stalledAt(adEnv, vehicle, state, cargo)
|
||||
local action = ADFlowWagons.emptyStep(state.stage, {cargo = cargo, adActive = module ~= nil and module:isActive(),
|
||||
elapsedMs = nowMs - state.since, stalled = stalled, dumpAttempts = state.dumpAttempts})
|
||||
if stalled and action ~= "wait" then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' kan ikke aflæsse ved %s (fuldt?) — videre", tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
tostring(state.markerName))
|
||||
end
|
||||
if action == "home" then
|
||||
goHome(adEnv, vehicle)
|
||||
elseif action == "dump" then
|
||||
startDump(adEnv, vehicle, state)
|
||||
elseif action == "giveUp" then
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' kunne ikke tømme vognen (%d l %s tilbage) — parkeres med last",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType))
|
||||
goHome(adEnv, vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function rememberAll(adEnv)
|
||||
for _, run in ipairs(runs()) do
|
||||
if not run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
if wagonId ~= nil then
|
||||
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
run.wagons = updated
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function retryWaiting(adEnv)
|
||||
local list = {}
|
||||
for vehicle, runId in pairs(waiting) do table.insert(list, {vehicle = vehicle, runId = runId}) end
|
||||
for _, item in ipairs(list) do
|
||||
waiting[item.vehicle] = nil
|
||||
local run = ADRuns.getRun(ADRunsController.store, item.runId)
|
||||
if run ~= nil and not run.stopped then pcall(ADRunsController.startVehicle, adEnv, run, item.vehicle) end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkBuildings(adEnv)
|
||||
local exists = {}
|
||||
local placeables = g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
local id = ADBuildings.getId(placeable)
|
||||
if id ~= nil then exists[id] = true end
|
||||
end
|
||||
if next(exists) == nil then return end -- listen er ikke klar: sig intet
|
||||
for id in pairs(exists) do seenBuildings[id] = true end
|
||||
for _, run in ipairs(runs()) do
|
||||
if not run.stopped then
|
||||
local missing = ADFlowWagons.missingBuildings(run, exists, seenBuildings)
|
||||
local count, gone = ADFlowWagons.missingStep(missingCount[run.id], #missing > 0)
|
||||
missingCount[run.id] = count
|
||||
if gone then
|
||||
missingCount[run.id] = 0
|
||||
Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': bygning findes ikke længere (%s) — stoppet, traktorer kørt hjem",
|
||||
tostring(run.name), table.concat(missing, ", "))
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||
pcall(ADFieldJobs.notify, string.format("Flow '%s': bygning findes ikke længere — stoppet, traktorer kørt hjem", tostring(run.name)), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Én gang efter indlæsning: et stoppet flows traktor der holder stille med sin vogn (gemt midt i hjemturen — AD
|
||||
-- kørte den færdig, men afkoblingen er glemt) køres hjem. Ikke løbende: spilleren kan selv have koblet på.
|
||||
ADFlowWagons.AFTER_LOAD_MS = 120000
|
||||
local sinceLoad, cleanedUp = 0, false
|
||||
|
||||
-- Efter indlæsning: traktorer uden for flows med et puljeredskab på (hentet før gemning, opgaven tabt) følges som
|
||||
-- forældreløse — får de ingen opgave inden ORPHAN_MS, sættes redskabet tilbage (in-game 29/9: 3 læssevogne).
|
||||
local function adoptOrphansAfterLoad(adEnv)
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
if (run == nil or run.stopped) and vehicle.spec_attacherJoints ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
and not ADFlowWagons.isEmptying(vehicle) and not homing[vehicle] then
|
||||
orphans[vehicle] = orphans[vehicle] or 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanupAfterLoad(adEnv)
|
||||
for _, run in ipairs(runs()) do
|
||||
if run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
local module = vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
local controlled = vehicle ~= nil and vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if module ~= nil and not module:isActive() and not controlled and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||
and not ADFlowWagons.isPoolBusy(vehicle, vehicleId) then
|
||||
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s' er stoppet: '%s' tømmer og sætter sin vogn på plads", tostring(run.name),
|
||||
tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name, "afterLoad")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local emptyTimer = 0
|
||||
|
||||
function ADFlowWagons.tick(adEnv, dt)
|
||||
if adEnv == nil or ADRunsController.store == nil then return end
|
||||
if ADHitch ~= nil and ADHitch.onManualAttach == nil then ADHitch.onManualAttach = ADFlowWagons.onManualAttach end
|
||||
nowMs = nowMs + dt
|
||||
emptyTimer = emptyTimer + dt
|
||||
if emptyTimer >= 2000 then
|
||||
emptyTimer = 0
|
||||
local okOrphan, errOrphan = pcall(tickOrphans, adEnv, 2000)
|
||||
if not okOrphan then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave: %s", tostring(errOrphan)) end
|
||||
local okEmpty, errEmpty = pcall(tickEmptying, adEnv)
|
||||
if not okEmpty then Logging.warning(ADFlowWagons.LOG_PREFIX .. "tømning: %s", tostring(errEmpty)) end
|
||||
end
|
||||
if not cleanedUp then
|
||||
sinceLoad = sinceLoad + dt
|
||||
if sinceLoad >= ADFlowWagons.AFTER_LOAD_MS then
|
||||
cleanedUp = true
|
||||
local ok, err = pcall(cleanupAfterLoad, adEnv)
|
||||
if not ok then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning efter indlæsning: %s", tostring(err)) end
|
||||
local okAdopt, errAdopt = pcall(adoptOrphansAfterLoad, adEnv)
|
||||
if not okAdopt then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave efter indlæsning: %s", tostring(errAdopt)) end
|
||||
local okPrune, errPrune = pcall(ADRunsController.pruneMissingVehicles, adEnv)
|
||||
if not okPrune then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning af solgte traktorer: %s", tostring(errPrune)) end
|
||||
end
|
||||
end
|
||||
rememberTimer = rememberTimer + dt
|
||||
buildingTimer = buildingTimer + dt
|
||||
if rememberTimer >= ADFlowWagons.REMEMBER_MS then
|
||||
rememberTimer = 0
|
||||
rememberAll(adEnv)
|
||||
retryWaiting(adEnv)
|
||||
end
|
||||
if buildingTimer >= ADFlowWagons.BUILDING_CHECK_MS then
|
||||
buildingTimer = 0
|
||||
checkBuildings(adEnv)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
-- AD Guide
|
||||
-- Fælles guide-motor (ren Lua, ingen spil-API) for "Opsæt mark" og "Opsæt flow": ét trin ad gangen,
|
||||
-- trin der ikke er relevante springes over, tælling "Trin 3 af 7" over de synlige trin, og [Ret] der
|
||||
-- åbner ét enkelt trin. Motoren kender ikke marker eller flows; en trin-definition giver rækkefølgen og
|
||||
-- hvornår et trin vises:
|
||||
-- def = {order = {stepId, ...}, isVisible = function(stepId, guide) -> bool}
|
||||
-- Guiden er en tabel {def, draft, context, step, single, returnTo, snapshot}; alle funktioner returnerer
|
||||
-- en ny guide (kladden kopieres), så [Annullér] aldrig har ændret noget.
|
||||
|
||||
ADGuide = {}
|
||||
|
||||
-- Kopi af rene data (tabeller uden cykler); funktioner og tal/tekster deles.
|
||||
function ADGuide.deepCopy(value)
|
||||
if type(value) ~= "table" then return value end
|
||||
local copy = {}
|
||||
for key, item in pairs(value) do copy[key] = ADGuide.deepCopy(item) end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Ny liste med item tilføjet (on) eller fjernet; aldrig dubletter.
|
||||
function ADGuide.toggleItem(list, item, on)
|
||||
local result, found = {}, false
|
||||
for _, value in ipairs(list or {}) do
|
||||
if value == item then
|
||||
found = true
|
||||
if on then table.insert(result, value) end
|
||||
else
|
||||
table.insert(result, value)
|
||||
end
|
||||
end
|
||||
if on and not found then table.insert(result, item) end
|
||||
return result
|
||||
end
|
||||
|
||||
local function clone(guide, changes)
|
||||
local copy = {}
|
||||
for key, value in pairs(guide) do copy[key] = value end
|
||||
for key, value in pairs(changes or {}) do
|
||||
if value == false and (key == "snapshot") then copy[key] = nil else copy[key] = value end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function isVisible(guide, step)
|
||||
return guide.def.isVisible == nil or guide.def.isVisible(step, guide) == true
|
||||
end
|
||||
|
||||
function ADGuide.visibleSteps(guide)
|
||||
local steps = {}
|
||||
for _, step in ipairs(guide.def.order) do
|
||||
if isVisible(guide, step) then table.insert(steps, step) end
|
||||
end
|
||||
return steps
|
||||
end
|
||||
|
||||
local function orderIndex(guide, step)
|
||||
for index, id in ipairs(guide.def.order) do
|
||||
if id == step then return index end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Første synlige trin efter (dir = 1) eller før (dir = -1) det aktuelle i rækkefølgen.
|
||||
local function neighbour(guide, dir)
|
||||
local order = guide.def.order
|
||||
local index = orderIndex(guide, guide.step) + dir
|
||||
while index >= 1 and index <= #order do
|
||||
if isVisible(guide, order[index]) then return order[index] end
|
||||
index = index + dir
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- options = {step = id, single = bool, returnTo = "close"|"summary"}
|
||||
function ADGuide.new(def, draft, context, options)
|
||||
options = options or {}
|
||||
local guide = {def = def, draft = draft, context = context, step = options.step,
|
||||
single = options.single == true, returnTo = options.returnTo or "close"}
|
||||
if guide.step == nil then
|
||||
guide.step = ADGuide.visibleSteps(guide)[1] or def.order[1]
|
||||
end
|
||||
if guide.single then guide.snapshot = ADGuide.deepCopy(draft) end
|
||||
return guide
|
||||
end
|
||||
|
||||
-- index (nil hvis det aktuelle trin ikke er synligt), total synlige trin.
|
||||
function ADGuide.position(guide)
|
||||
local steps = ADGuide.visibleSteps(guide)
|
||||
local index = nil
|
||||
for position, step in ipairs(steps) do
|
||||
if step == guide.step then index = position end
|
||||
end
|
||||
return index, #steps
|
||||
end
|
||||
|
||||
-- Sidste trin: ingen synlige efter, eller et [Ret]-trin der gemmer direkte.
|
||||
function ADGuide.isLast(guide)
|
||||
if guide.single and guide.returnTo == "close" then return true end
|
||||
if guide.single then return false end
|
||||
return neighbour(guide, 1) == nil
|
||||
end
|
||||
|
||||
-- Næste: event "done" når der gemmes (sidste trin, eller [Ret] fra fanens oversigt).
|
||||
function ADGuide.next(guide)
|
||||
if guide.single then
|
||||
if guide.returnTo == "summary" then
|
||||
return clone(guide, {step = "summary", single = false, snapshot = false}), nil
|
||||
end
|
||||
return guide, "done"
|
||||
end
|
||||
local step = neighbour(guide, 1)
|
||||
if step == nil then return guide, "done" end
|
||||
return clone(guide, {step = step}), nil
|
||||
end
|
||||
|
||||
-- Tilbage: event "cancel" når guiden lukkes (første trin, eller [Ret] fra fanens oversigt).
|
||||
-- I et [Ret]-trin fra guidens eget oversigts-trin fortrydes trinnets ændringer.
|
||||
function ADGuide.back(guide)
|
||||
if guide.single then
|
||||
if guide.returnTo == "summary" then
|
||||
return clone(guide, {step = "summary", single = false, draft = guide.snapshot or guide.draft, snapshot = false}), nil
|
||||
end
|
||||
return guide, "cancel"
|
||||
end
|
||||
local step = neighbour(guide, -1)
|
||||
if step == nil then return guide, "cancel" end
|
||||
return clone(guide, {step = step}), nil
|
||||
end
|
||||
|
||||
-- [Ret]: åbn kun ét trin. returnTo "summary" (guidens oversigt) eller "close" (gem og luk).
|
||||
function ADGuide.jump(guide, step, returnTo)
|
||||
return clone(guide, {step = step, single = true, returnTo = returnTo or "close", snapshot = ADGuide.deepCopy(guide.draft)})
|
||||
end
|
||||
|
||||
function ADGuide.withDraft(guide, draft)
|
||||
return clone(guide, {draft = draft})
|
||||
end
|
||||
|
||||
function ADGuide.withContext(guide, context)
|
||||
return clone(guide, {context = context})
|
||||
end
|
||||
|
||||
-- label = {key = "...", args = {...}} (oversættes) eller {text = "..."} (færdig tekst); valgfri prefix
|
||||
-- (færdig tekst foran, fx "Mælk: " foran den oversatte politik).
|
||||
function ADGuide.labelText(label, getText)
|
||||
if type(label) ~= "table" then return "" end
|
||||
if label.prefix ~= nil then
|
||||
local rest = {}
|
||||
for key, value in pairs(label) do if key ~= "prefix" then rest[key] = value end end
|
||||
return tostring(label.prefix) .. ADGuide.labelText(rest, getText)
|
||||
end
|
||||
if label.text ~= nil then return tostring(label.text) end
|
||||
local template = getText(label.key)
|
||||
if label.args == nil or #label.args == 0 then return tostring(template) end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local ok, text = pcall(string.format, template, unpackFn(label.args))
|
||||
return ok and text or tostring(template)
|
||||
end
|
||||
|
||||
-- Resumé-linje {step, title, parts} -> "Afgrøde: Behold det der gror, så igen".
|
||||
function ADGuide.lineText(line, getText)
|
||||
local parts = {}
|
||||
for _, part in ipairs(line.parts or {}) do
|
||||
local text = ADGuide.labelText(part, getText)
|
||||
if text ~= "" then table.insert(parts, text) end
|
||||
end
|
||||
return ADGuide.labelText(line.title, getText) .. ": " .. (#parts > 0 and table.concat(parts, ", ") or "-")
|
||||
end
|
||||
@@ -0,0 +1,591 @@
|
||||
-- AD Hitch
|
||||
-- Kobler redskaber på og af med AutoDrive.
|
||||
-- "Connect <navn>" (eller "Kobl <navn>"): markøren ligger lige under vognens trækøje med et bakkespor ned til
|
||||
-- den. Når en traktor har kørt AD "Kør til" dertil og står stille, kobles det redskab på, som spillet selv finder
|
||||
-- inden for koblingsafstand (samme kald som Courseplay bruger til skærebord: updateVehiclesInAttachRange +
|
||||
-- attachImplementFromInfo).
|
||||
-- "Afkobl <navn>" (eller "Kobl af", "Detach", "Disconnect"): ved ankomst kobles redskaberne BAG traktoren af;
|
||||
-- frontvægt og frontlæsser bliver siddende.
|
||||
-- ADHitch.fetch()/ADHitch.drop() er indgangen for flows/markarbejde: kør til markøren, kobl på/af, meld tilbage.
|
||||
|
||||
ADHitch = {}
|
||||
ADHitch.LOG_PREFIX = "ADSmartPickup: kobling: "
|
||||
-- afkobling tjekkes først, så "Kobl af X" ikke læses som "Kobl <af X>"
|
||||
ADHitch.MARKER_PREFIXES = {
|
||||
{prefix = "afkobl", action = "detach"},
|
||||
{prefix = "kobl af", action = "detach"},
|
||||
{prefix = "detach", action = "detach"},
|
||||
{prefix = "disconnect", action = "detach"},
|
||||
{prefix = "connect", action = "attach"},
|
||||
{prefix = "kobl", action = "attach"},
|
||||
}
|
||||
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
|
||||
ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet
|
||||
ADHitch.DETACH_ARRIVE_DISTANCE = 8 -- afkobling: langt redskab (valse) står med roden >5 m inde i båsen
|
||||
ADHitch.STILL_SPEED = 1 -- km/t
|
||||
ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop
|
||||
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
|
||||
ADHitch.DETACH_TIMEOUT_MS = 20000 -- afkobling: redskabet skal måske foldes ud først (plov: "unfold the tool")
|
||||
ADHitch.TICK_MS = 250
|
||||
ADHitch.ROUTE_WAIT_MS = 10000 -- pause ved almindelige markører midt i en tur
|
||||
ADHitch.ROOMY_ATTACH_DISTANCE = 3 -- m: koblingsrækkevidde når spillets egen ikke rækker
|
||||
ADHitch.CREPT_EXTRA_DISTANCE = 8 -- m: ekstra ankomstafstand når traktoren har bakket hen til vognen
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- "Connect Bredal" -> "attach", "Bredal"; "Afkobl" -> "detach", ""; andre navne -> nil
|
||||
function ADHitch.parseMarker(name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
local trimmed = name:match("^%s*(.-)%s*$")
|
||||
local lower = trimmed:lower()
|
||||
for _, entry in ipairs(ADHitch.MARKER_PREFIXES) do
|
||||
local prefix = entry.prefix
|
||||
if lower == prefix then return entry.action, "" end
|
||||
if lower:sub(1, #prefix + 1) == prefix .. " " then
|
||||
return entry.action, (trimmed:sub(#prefix + 2):match("^%s*(.-)$"))
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- implements = {{z = position i traktorens rum, ...}} -> kun dem bag traktoren
|
||||
function ADHitch.rearImplements(implements)
|
||||
local rear = {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if implement.z ~= nil and implement.z < 0 then table.insert(rear, implement) end
|
||||
end
|
||||
return rear
|
||||
end
|
||||
|
||||
-- konsol-argumenter "Connect Bredal > Start > Afkobl Bredal > Start" -> {"Connect Bredal", "Start", ...}
|
||||
function ADHitch.parseTrip(...)
|
||||
local steps = {}
|
||||
for part in (table.concat({...}, " ") .. ">"):gmatch("([^>]*)>") do
|
||||
local name = part:match("^%s*(.-)%s*$")
|
||||
if name ~= "" then table.insert(steps, name) end
|
||||
end
|
||||
return steps
|
||||
end
|
||||
|
||||
-- markørnavn -> attach / detach / arrive (almindelig markør: bare køre dertil)
|
||||
function ADHitch.stepAction(name)
|
||||
return ADHitch.parseMarker(name) or "arrive"
|
||||
end
|
||||
|
||||
-- Spillerens egen AD "Kør til": markørens navn (Connect/Afkobl) først, ellers er puljeredskabets egen parkering
|
||||
-- stedet vognen sættes af. -> "attach" | "detach" | nil
|
||||
function ADHitch.manualAction(parsedAction, isPoolImplementPark)
|
||||
if parsedAction ~= nil then return parsedAction end
|
||||
return isPoolImplementPark and "detach" or nil
|
||||
end
|
||||
|
||||
-- I en tur: en almindelig markør, der er AD-parkeringen for redskabet bag traktoren, er stedet vognen sættes af.
|
||||
function ADHitch.resolveAction(action, isImplementPark)
|
||||
if action == "arrive" and isImplementPark then return "detach" end
|
||||
return action
|
||||
end
|
||||
|
||||
-- pause efter et trin: ved almindelige markører undervejs holdes der, ikke efter sidste trin
|
||||
function ADHitch.waitAfter(action, index, count)
|
||||
if action == "arrive" and index < count then return ADHitch.ROUTE_WAIT_MS end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- markers = {{name, id}} -> markøren med navnet (store/små bogstaver ligegyldigt)
|
||||
function ADHitch.findMarker(markers, name)
|
||||
if type(name) ~= "string" then return nil end
|
||||
local wanted = name:lower()
|
||||
for _, marker in pairs(markers or {}) do
|
||||
if type(marker.name) == "string" and marker.name:lower() == wanted then return marker end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvor tæt på markøren sættet skal være: afkobling ved egen parkering godtager længere redskaber (in-game 29/9:
|
||||
-- valsen stod 5 m inde i båsen -> 'stoppedAway'); efter bak hen til vognen udvides koblingsafstanden.
|
||||
function ADHitch.arriveDistanceFor(action, crept)
|
||||
if action == "detach" then return ADHitch.DETACH_ARRIVE_DISTANCE end
|
||||
if crept then return ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
|
||||
return ADHitch.ARRIVE_DISTANCE
|
||||
end
|
||||
|
||||
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
|
||||
local function arriveDistanceOf(state)
|
||||
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
||||
end
|
||||
|
||||
function ADHitch.hasArrived(state)
|
||||
return not state.adActive and state.distance <= arriveDistanceOf(state) and state.speed <= ADHitch.STILL_SPEED
|
||||
end
|
||||
|
||||
function ADHitch.newJob(markerId, markerName, action)
|
||||
return {markerId = markerId, markerName = markerName, action = action, state = "driving", elapsed = 0,
|
||||
actElapsed = 0, seenActive = false}
|
||||
end
|
||||
|
||||
-- Afkobling bekræftes: "done" først når intet sidder bag traktoren efter et forsøg. -> "attempt" | "done" | "nothing"
|
||||
function ADHitch.detachStep(rearCount, attempted)
|
||||
if rearCount > 0 then return "attempt" end
|
||||
return attempted and "done" or "nothing"
|
||||
end
|
||||
|
||||
-- Et skridt i jobbet. succeeded = lykkedes kobling/afkobling i denne tick (kun relevant i "acting").
|
||||
-- Tilstande: driving -> acting -> done | failed (reason: stoppedAway / noAttachable / nothingToDetach)
|
||||
function ADHitch.step(job, state, dt, succeeded)
|
||||
if job.state == "driving" then
|
||||
job.elapsed = job.elapsed + dt
|
||||
if state.adActive then
|
||||
job.seenActive = true
|
||||
elseif ADHitch.hasArrived(state) then
|
||||
job.state = "acting"
|
||||
elseif state.distance > arriveDistanceOf(state) and (job.seenActive or job.elapsed >= ADHitch.START_GRACE_MS) then
|
||||
job.state, job.reason = "failed", string.format("stoppedAway (%.0f m fra markøren)", state.distance)
|
||||
end
|
||||
elseif job.state == "acting" then
|
||||
if succeeded then
|
||||
job.state = "done"
|
||||
else
|
||||
job.actElapsed = job.actElapsed + dt
|
||||
local limit = job.action == "detach" and ADHitch.DETACH_TIMEOUT_MS or ADHitch.ACT_TIMEOUT_MS
|
||||
if job.actElapsed >= limit then
|
||||
job.state, job.reason = "failed", ADHitch.TIMEOUT_REASON[job.action]
|
||||
end
|
||||
end
|
||||
end
|
||||
return job
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local jobs = setmetatable({}, {__mode = "k"}) -- [vehicle] = job (+ onDone)
|
||||
local tickTimer = 0
|
||||
|
||||
local function vehicleName(vehicle)
|
||||
if type(vehicle) ~= "table" then return tostring(vehicle) end
|
||||
return vehicle.getFullName ~= nil and vehicle:getFullName() or tostring(vehicle.configFileName)
|
||||
end
|
||||
|
||||
local function stateModule(vehicle)
|
||||
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
end
|
||||
|
||||
local function vehicleState(adEnv, vehicle, markerId)
|
||||
local module = stateModule(vehicle)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(markerId)
|
||||
-- nærmeste led i sættet: bakker AD med vogn, er det vognen der ender på markøren, ikke traktoren
|
||||
-- (in-game 28/9: vognen stod på Bredal, traktoren et vognslæb væk -> "stoppedAway")
|
||||
local distance = math.huge
|
||||
local parts = {vehicle}
|
||||
if vehicle.getChildVehicles ~= nil then
|
||||
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
||||
if ok and children ~= nil then parts = children end
|
||||
end
|
||||
for _, part in pairs(parts) do
|
||||
if wayPoint ~= nil and part.rootNode ~= nil then
|
||||
local x, _, z = getWorldTranslation(part.rootNode)
|
||||
distance = math.min(distance, math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2))
|
||||
end
|
||||
end
|
||||
return {
|
||||
adActive = module ~= nil and module:isActive(),
|
||||
distance = distance,
|
||||
speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- Kobler det redskab på, som spillet finder inden for koblingsafstand. -> ok, redskab eller årsag
|
||||
function ADHitch.tryAttach(vehicle)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then
|
||||
return false, "noJoints"
|
||||
end
|
||||
-- spillets egen rækkevidde først, så en rummeligere (spillet retter selv vognen ind ved koblingen)
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ,
|
||||
AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
local info = spec.attachableInfo
|
||||
if (info == nil or info.attachable == nil) and ADHitch.ROOMY_ATTACH_DISTANCE ^ 2 > AttacherJoints.MAX_ATTACH_DISTANCE_SQ then
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitch.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
info = spec.attachableInfo
|
||||
end
|
||||
if info == nil or info.attachable == nil then return false, "noAttachable" end
|
||||
local attacher = info.attacherVehicle or vehicle
|
||||
local farmId = vehicle.getActiveFarm ~= nil and vehicle:getActiveFarm() or vehicle:getOwnerFarmId()
|
||||
local allowed, warning = info.attachable:isAttachAllowed(farmId, attacher)
|
||||
if not allowed then return false, tostring(warning or "notAllowed") end
|
||||
attacher:attachImplementFromInfo(info)
|
||||
return true, info.attachable
|
||||
end
|
||||
|
||||
-- Redskaber koblet direkte på traktoren, med deres placering langs traktoren (z > 0 foran, z < 0 bagved).
|
||||
local function attachedImplements(vehicle)
|
||||
local list = {}
|
||||
if vehicle.getAttachedImplements == nil then return list end
|
||||
for _, implement in ipairs(vehicle:getAttachedImplements()) do
|
||||
local jointDesc = vehicle.spec_attacherJoints.attacherJoints[implement.jointDescIndex]
|
||||
local node = jointDesc ~= nil and (jointDesc.jointTransform or jointDesc.rootNode) or nil
|
||||
if implement.object ~= nil and node ~= nil then
|
||||
local _, _, z = localToLocal(node, vehicle.rootNode, 0, 0, 0)
|
||||
table.insert(list, {object = implement.object, z = z, name = vehicleName(implement.object)})
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Er markøren (waypoint-id) AD-parkeringen for et af redskaberne bag traktoren?
|
||||
local function isImplementPark(adEnv, vehicle, markerId)
|
||||
for _, implement in ipairs(ADHitch.rearImplements(attachedImplements(vehicle))) do
|
||||
local advd = implement.object.advd
|
||||
local index = advd ~= nil and advd.parkDestination or nil
|
||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||
if marker ~= nil and marker.id == markerId then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Efter påkobling: fold foldbare redskaber sammen til vejen (CP folder selv ud på marken).
|
||||
-- Klar til vej: s = {turnedOn, lowered, unfolded, canFold} -> liste af "turnOff" | "raise" | "fold"
|
||||
function ADHitch.stowActions(s)
|
||||
local actions = {}
|
||||
if s.turnedOn then table.insert(actions, "turnOff") end
|
||||
if s.lowered then table.insert(actions, "raise") end
|
||||
if s.unfolded and s.canFold then table.insert(actions, "fold") end
|
||||
return actions
|
||||
end
|
||||
|
||||
-- Alle redskaber på maskinen (også forrest og bag et andet redskab): {object, attacher, jointDescIndex}
|
||||
local function allImplements(vehicle, list)
|
||||
list = list or {}
|
||||
if vehicle == nil or vehicle.getAttachedImplements == nil then return list end
|
||||
for _, implement in ipairs(vehicle:getAttachedImplements()) do
|
||||
if implement.object ~= nil then
|
||||
table.insert(list, {object = implement.object, attacher = vehicle, jointDescIndex = implement.jointDescIndex})
|
||||
allImplements(implement.object, list)
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Stop af mark/flow: sluk, løft og fold alle redskaber til transport. -> antal redskaber der fik noget gjort
|
||||
function ADHitch.stowForTransport(vehicle)
|
||||
local count = 0
|
||||
for _, item in ipairs(allImplements(vehicle)) do
|
||||
local object = item.object
|
||||
local foldDirection = object.spec_foldable ~= nil and -(object.spec_foldable.turnOnFoldDirection or 1) or nil
|
||||
local actions = ADHitch.stowActions({
|
||||
turnedOn = object.getIsTurnedOn ~= nil and object:getIsTurnedOn(),
|
||||
lowered = object.getIsLowered ~= nil and object:getIsLowered(true),
|
||||
unfolded = object.getIsUnfolded ~= nil and object:getIsUnfolded() and foldDirection ~= nil,
|
||||
canFold = foldDirection ~= nil and object.setFoldDirection ~= nil
|
||||
and (object.getIsFoldAllowed == nil or object:getIsFoldAllowed(foldDirection, false)),
|
||||
})
|
||||
for _, action in ipairs(actions) do
|
||||
if action == "turnOff" and object.setIsTurnedOn ~= nil then
|
||||
pcall(object.setIsTurnedOn, object, false)
|
||||
elseif action == "raise" and item.attacher.setJointMoveDown ~= nil and item.jointDescIndex ~= nil then
|
||||
pcall(item.attacher.setJointMoveDown, item.attacher, item.jointDescIndex, false)
|
||||
elseif action == "fold" then
|
||||
pcall(object.setFoldDirection, object, foldDirection)
|
||||
end
|
||||
end
|
||||
if #actions > 0 then count = count + 1 end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Som stowForTransport, men logget og pcall'et (til stop-vejene).
|
||||
function ADHitch.stow(vehicle, why)
|
||||
local ok, count = pcall(ADHitch.stowForTransport, vehicle)
|
||||
if not ok then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s': kunne ikke gøre redskaber klar til vej: %s", vehicleName(vehicle), tostring(count))
|
||||
elseif count > 0 then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s': %d redskab(er) slukket, løftet og foldet (%s)", vehicleName(vehicle), count, tostring(why))
|
||||
end
|
||||
end
|
||||
|
||||
function ADHitch.foldForTransport(vehicle)
|
||||
for _, implement in ipairs(ADHitch.rearImplementsOf(vehicle)) do
|
||||
local object = implement.object
|
||||
local unfolded = object.getIsUnfolded ~= nil and object:getIsUnfolded()
|
||||
if unfolded and object.spec_foldable ~= nil and object.setFoldDirection ~= nil then
|
||||
object:setFoldDirection(-(object.spec_foldable.turnOnFoldDirection or 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- Kobler redskaberne bag traktoren af. -> ok, navne eller årsag. Med job bekræftes afkoblingen ved næste tick
|
||||
-- (ok først når intet sidder bagpå); uden job (konsol) meldes ok straks efter kaldet.
|
||||
function ADHitch.tryDetach(vehicle, job)
|
||||
local rear = ADHitch.rearImplements(attachedImplements(vehicle))
|
||||
local step = ADHitch.detachStep(#rear, job ~= nil and job.detachNames ~= nil)
|
||||
if step == "done" then return true, job.detachNames end
|
||||
if step == "nothing" then return false, "nothingToDetach" end
|
||||
local names = {}
|
||||
for _, implement in ipairs(rear) do
|
||||
local object = implement.object
|
||||
local allowed, warning = true, nil
|
||||
if object.isDetachAllowed ~= nil then allowed, warning = object:isDetachAllowed() end
|
||||
if not allowed and object.spec_foldable ~= nil and object.setFoldDirection ~= nil then
|
||||
-- plove o.l. må kun kobles af udfoldet (in-game 28/9: "You first need to unfold the tool") — fold ud
|
||||
-- og prøv igen ved næste tick, mens foldningen kører
|
||||
object:setFoldDirection(object.spec_foldable.turnOnFoldDirection or 1)
|
||||
return false, "unfolding: " .. tostring(warning)
|
||||
end
|
||||
if not allowed then return false, tostring(warning or "detachNotAllowed") end
|
||||
vehicle:detachImplementByObject(object)
|
||||
table.insert(names, implement.name)
|
||||
end
|
||||
if job == nil then return true, table.concat(names, ", ") end
|
||||
if job.detachNames ~= nil then
|
||||
job.detachRetries = (job.detachRetries or 0) + 1
|
||||
if job.detachRetries == 1 then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s': '%s' sidder der stadig efter afkobling — prøver igen", vehicleName(vehicle), table.concat(names, ", "))
|
||||
end
|
||||
end
|
||||
job.detachNames = job.detachNames or table.concat(names, ", ")
|
||||
return false, "stillAttached: " .. table.concat(names, ", ")
|
||||
end
|
||||
|
||||
local function start(adEnv, vehicle, markerId, action, onDone)
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerId)
|
||||
local job = ADHitch.newJob(markerId, marker ~= nil and marker.name or tostring(markerId), action)
|
||||
job.onDone = onDone
|
||||
jobs[vehicle] = job
|
||||
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, markerId)
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' kører til '%s'", vehicleName(vehicle), job.markerName)
|
||||
return job
|
||||
end
|
||||
|
||||
-- Kørende kobl-på-job for traktoren (til bakningen de sidste meter), ellers nil.
|
||||
function ADHitch.pendingAttach(vehicle)
|
||||
local job = jobs[vehicle]
|
||||
if job ~= nil and job.action == "attach" and job.state == "driving" then return job end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Afbryd et hente-/koble-job (Stop alt, høsten slut, spilleren overtager): intet kobles på bagefter, og
|
||||
-- onDone kaldes ikke. -> true hvis der var et job.
|
||||
function ADHitch.cancel(vehicle)
|
||||
local had = jobs[vehicle] ~= nil
|
||||
jobs[vehicle] = nil
|
||||
if ADPool ~= nil and ADPool.setReturning ~= nil then ADPool.setReturning(vehicle, false) end
|
||||
if had and vehicle.stopAutoDrive ~= nil then pcall(vehicle.stopAutoDrive, vehicle) end
|
||||
return had
|
||||
end
|
||||
|
||||
-- Kør til en Connect-markør og kobl på. onDone(vehicle, ok, redskab eller årsag) kaldes én gang.
|
||||
function ADHitch.fetch(adEnv, vehicle, markerId, onDone)
|
||||
return start(adEnv, vehicle, markerId, "attach", onDone)
|
||||
end
|
||||
|
||||
-- Kør til en Afkobl-markør og kobl af. onDone(vehicle, ok, navne eller årsag) kaldes én gang.
|
||||
function ADHitch.drop(adEnv, vehicle, markerId, onDone)
|
||||
return start(adEnv, vehicle, markerId, "detach", onDone)
|
||||
end
|
||||
|
||||
-- Tur: markørerne køres i rækkefølge. Connect = kobl på, Afkobl eller vognens egen AD-parkering = kobl af,
|
||||
-- andre = kør dertil (og hold
|
||||
-- ROUTE_WAIT_MS, hvis det ikke er sidste trin). Stopper ved første fejl. onDone(vehicle, ok, årsag) til sidst.
|
||||
local waits = {} -- {vehicle, leftMs, continue}
|
||||
|
||||
function ADHitch.route(adEnv, vehicle, markerIds, onDone)
|
||||
local function runStep(index)
|
||||
if index > #markerIds then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' er færdig med turen (%d trin)", vehicleName(vehicle), #markerIds)
|
||||
if onDone ~= nil then pcall(onDone, vehicle, true) end
|
||||
return
|
||||
end
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerIds[index])
|
||||
local action = ADHitch.resolveAction(ADHitch.stepAction(marker ~= nil and marker.name or nil),
|
||||
isImplementPark(adEnv, vehicle, markerIds[index]))
|
||||
start(adEnv, vehicle, markerIds[index], action, function(_, ok, detail)
|
||||
if not ok then
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s' stopper turen ved trin %d", vehicleName(vehicle), index)
|
||||
if onDone ~= nil then pcall(onDone, vehicle, false, detail) end
|
||||
return
|
||||
end
|
||||
local waitMs = ADHitch.waitAfter(action, index, #markerIds)
|
||||
if waitMs > 0 then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' holder %d s ved '%s'", vehicleName(vehicle), waitMs / 1000,
|
||||
marker ~= nil and marker.name or "?")
|
||||
table.insert(waits, {vehicle = vehicle, leftMs = waitMs, continue = function() runStep(index + 1) end})
|
||||
else
|
||||
runStep(index + 1)
|
||||
end
|
||||
end)
|
||||
end
|
||||
runStep(1)
|
||||
end
|
||||
|
||||
-- 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.
|
||||
-- Parkeringer (waypoint-id) for puljeredskaberne bag traktoren (tom = ingen puljeredskaber bag).
|
||||
function ADHitch.poolParkIds(adEnv, tractor)
|
||||
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
|
||||
return parkIds
|
||||
end
|
||||
|
||||
function ADHitch.returnHome(adEnv, tractor, onDone)
|
||||
local parkIds = ADHitch.poolParkIds(adEnv, tractor)
|
||||
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
|
||||
|
||||
local function tickWaits(elapsed)
|
||||
for i = #waits, 1, -1 do
|
||||
local wait = waits[i]
|
||||
wait.leftMs = wait.leftMs - elapsed
|
||||
if wait.leftMs <= 0 then
|
||||
table.remove(waits, i)
|
||||
wait.continue()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Spilleren sender selv en traktor til en Connect-/Afkobl-markør med AD "Kør til": den kobles også.
|
||||
local function watchManualTrips(adEnv)
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local module = stateModule(vehicle)
|
||||
if jobs[vehicle] == nil and vehicle.spec_attacherJoints ~= nil and module ~= nil and module:isActive()
|
||||
and module:getMode() == adEnv.AutoDrive.MODE_DRIVETO then
|
||||
local marker = module:getFirstMarker()
|
||||
local action = nil
|
||||
if marker ~= nil then
|
||||
local isPoolPark = false
|
||||
for _, parkId in ipairs(ADHitch.poolParkIds(adEnv, vehicle)) do
|
||||
if parkId == marker.id then isPoolPark = true end
|
||||
end
|
||||
action = ADHitch.manualAction(ADHitch.parseMarker(marker.name), isPoolPark)
|
||||
end
|
||||
if action ~= nil then jobs[vehicle] = ADHitch.newJob(marker.id, marker.name, action) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local DONE_TEXT = {attach = "koblede '%s' på", detach = "koblede '%s' af", arrive = "er fremme%s"}
|
||||
local FAIL_TEXT = {attach = "kunne ikke koble på", detach = "kunne ikke koble af", arrive = "nåede ikke frem"}
|
||||
|
||||
local function finish(vehicle, job, ok, detail, adEnv)
|
||||
jobs[vehicle] = nil
|
||||
if ok then
|
||||
Logging.info(ADHitch.LOG_PREFIX .. "'%s' " .. DONE_TEXT[job.action] .. " ved '%s'", vehicleName(vehicle),
|
||||
vehicleName(detail), job.markerName)
|
||||
else
|
||||
Logging.warning(ADHitch.LOG_PREFIX .. "'%s' %s ved '%s': %s", vehicleName(vehicle), FAIL_TEXT[job.action],
|
||||
job.markerName, tostring(detail))
|
||||
end
|
||||
if job.onDone ~= nil then
|
||||
pcall(job.onDone, vehicle, ok, detail)
|
||||
elseif ok and job.action == "attach" and ADHitch.onManualAttach ~= nil then
|
||||
-- spillerens egen tur til Connect: lad fx flowet starte traktoren bagefter
|
||||
local okHook, err = pcall(ADHitch.onManualAttach, adEnv, vehicle, detail)
|
||||
if not okHook then Logging.warning(ADHitch.LOG_PREFIX .. "efter manuel kobling: %s", tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
local ACTIONS = {attach = ADHitch.tryAttach, detach = ADHitch.tryDetach, arrive = function() return true, "" end}
|
||||
|
||||
function ADHitch.tick(adEnv, dt)
|
||||
if adEnv == nil or adEnv.ADGraphManager == nil then return end
|
||||
tickTimer = tickTimer + dt
|
||||
if tickTimer < ADHitch.TICK_MS then return end
|
||||
local elapsed = tickTimer
|
||||
tickTimer = 0
|
||||
tickWaits(elapsed)
|
||||
watchManualTrips(adEnv)
|
||||
-- kopi: et færdigt trin starter næste trin (nyt job på samme maskine) midt i løkken
|
||||
local current = {}
|
||||
for vehicle, job in pairs(jobs) do current[vehicle] = job end
|
||||
for vehicle, job in pairs(current) do
|
||||
local succeeded, detail = false, nil
|
||||
if job.state == "acting" then
|
||||
succeeded, detail = ACTIONS[job.action](vehicle, job)
|
||||
if not succeeded then job.lastReason = detail end
|
||||
end
|
||||
local state = vehicleState(adEnv, vehicle, job.markerId)
|
||||
state.arriveDistance = ADHitch.arriveDistanceFor(job.action, job.crept)
|
||||
ADHitch.step(job, state, elapsed, succeeded)
|
||||
if job.state == "done" then
|
||||
finish(vehicle, job, true, detail, adEnv)
|
||||
elseif job.state == "failed" then
|
||||
finish(vehicle, job, false, job.lastReason or job.reason, adEnv)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Konsol: "spuConnect" / "spuDetach" på den maskine spilleren sidder i.
|
||||
local function currentVehicle()
|
||||
return g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil and g_localPlayer:getCurrentVehicle() or nil
|
||||
end
|
||||
|
||||
function ADHitch:consoleConnect()
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuConnect: sæt dig i en traktor først" end
|
||||
local ok, detail = ADHitch.tryAttach(vehicle)
|
||||
if ok then return "spuConnect: koblede " .. vehicleName(detail) .. " på" end
|
||||
return "spuConnect: intet koblet på (" .. tostring(detail) .. ")"
|
||||
end
|
||||
|
||||
function ADHitch:consoleDetach()
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuDetach: sæt dig i en traktor først" end
|
||||
local ok, detail = ADHitch.tryDetach(vehicle)
|
||||
if ok then return "spuDetach: koblede " .. tostring(detail) .. " af" end
|
||||
return "spuDetach: intet koblet af (" .. tostring(detail) .. ")"
|
||||
end
|
||||
|
||||
-- "spuTrip Connect Bredal > Start > Bredal > Start": maskinen spilleren sidder i kører turen
|
||||
-- (spilleren kan blive siddende; AD kører også med spilleren i førerhuset).
|
||||
function ADHitch:consoleTrip(...)
|
||||
local vehicle = currentVehicle()
|
||||
if vehicle == nil then return "spuTrip: sæt dig i en traktor først" end
|
||||
local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil
|
||||
if adEnv == nil then return "spuTrip: AutoDrive er ikke klar endnu" end
|
||||
local names = ADHitch.parseTrip(...)
|
||||
if #names == 0 then return "spuTrip: brug fx spuTrip Connect Bredal > Start > Bredal > Start" end
|
||||
local markers, ids = adEnv.ADGraphManager:getMapMarkers(), {}
|
||||
for _, name in ipairs(names) do
|
||||
local marker = ADHitch.findMarker(markers, name)
|
||||
if marker == nil then return "spuTrip: ukendt markør '" .. name .. "'" end
|
||||
table.insert(ids, marker.id)
|
||||
end
|
||||
ADHitch.route(adEnv, vehicle, ids)
|
||||
return "spuTrip: " .. vehicleName(vehicle) .. " kører " .. table.concat(names, " > ")
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuConnect", "Kobler nærmeste redskab på den maskine du sidder i", "consoleConnect", ADHitch)
|
||||
addConsoleCommand("spuTrip", "Kør en tur: spuTrip Connect Bredal > Start > Bredal > Start", "consoleTrip", ADHitch)
|
||||
addConsoleCommand("spuDetach", "Kobler redskaberne bag den maskine du sidder i af", "consoleDetach", ADHitch)
|
||||
end
|
||||
@@ -0,0 +1,193 @@
|
||||
-- AD Hitch Creep
|
||||
-- Bak de sidste meter hen til vognen: når AD "Kør til" når en Connect-markør, og vognens trækøje står 3-8 m
|
||||
-- lige bag traktorens krog, lægges en lille bakke-opgave i AD's kø før stop. Traktoren bakker i krybefart og
|
||||
-- standser, så snart spillet ser vognen inden for koblingsrækkevidde; ADHitch kobler derefter på som før.
|
||||
-- AD's egen bak-kollisionssensor bruges ikke: den ser vognen selv som forhindring.
|
||||
|
||||
ADHitchCreep = {}
|
||||
ADHitchCreep.LOG_PREFIX = "ADSmartPickup: kobling: "
|
||||
ADHitchCreep.ROOMY_ATTACH_DISTANCE = 3 -- m: herinden for kobler ADHitch direkte (samme som ADHitch.ROOMY_ATTACH_DISTANCE)
|
||||
ADHitchCreep.CREEP_MAX_DISTANCE = 8 -- m: længere væk end det bakkes ikke — markøren ligger forkert
|
||||
ADHitchCreep.CREEP_MAX_LATERAL = 1.5 -- m til siden: mere end det rammer en lige bakning ikke trækøjet
|
||||
ADHitchCreep.CREEP_TIMEOUT_MS = 25000
|
||||
ADHitchCreep.CREEP_SPEED = 2 -- km/t
|
||||
ADHitchCreep.AWAY_TOLERANCE = 1 -- m: længere væk end ved start = den bakker forkert
|
||||
ADHitchCreep.RANGE_CHECK_MS = 250
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- offset = {x, z}: vognens trækøje i traktorens rum set fra krogen (z < 0 = bagved) -> "attach" | "creep" | "none"
|
||||
function ADHitchCreep.plan(offset)
|
||||
if offset == nil then return "none" end
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if distance <= ADHitchCreep.ROOMY_ATTACH_DISTANCE then return "attach" end
|
||||
if offset.z >= 0 or distance > ADHitchCreep.CREEP_MAX_DISTANCE or math.abs(offset.x) > ADHitchCreep.CREEP_MAX_LATERAL then
|
||||
return "none"
|
||||
end
|
||||
return "creep"
|
||||
end
|
||||
|
||||
-- s = {inRange, distance, startDistance, elapsedMs, passed} -> "done" | "drive" | "giveUp"
|
||||
function ADHitchCreep.step(s)
|
||||
if s.inRange then return "done" end
|
||||
if (s.elapsedMs or 0) >= ADHitchCreep.CREEP_TIMEOUT_MS then return "giveUp" end
|
||||
if s.passed then return "giveUp" end
|
||||
if s.distance > s.startDistance + ADHitchCreep.AWAY_TOLERANCE then return "giveUp" end
|
||||
return "drive"
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function worldPos(node)
|
||||
local x, y, z = getWorldTranslation(node)
|
||||
return {x = x, y = y, z = z}
|
||||
end
|
||||
|
||||
local function vehicleName(vehicle)
|
||||
return vehicle ~= nil and vehicle.getName ~= nil and vehicle:getName() or "?"
|
||||
end
|
||||
|
||||
-- Traktorens ledige bagerste krog-noder.
|
||||
local function freeRearJoints(vehicle)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or spec.attacherJoints == nil or vehicle.rootNode == nil then return {} end
|
||||
local used = {}
|
||||
for _, implement in ipairs(spec.attachedImplements or {}) do
|
||||
if implement.jointDescIndex ~= nil then used[implement.jointDescIndex] = true end
|
||||
end
|
||||
local joints = {}
|
||||
for index, joint in ipairs(spec.attacherJoints) do
|
||||
if not used[index] and joint.jointTransform ~= nil then
|
||||
local p = worldPos(joint.jointTransform)
|
||||
local _, _, lz = worldToLocal(vehicle.rootNode, p.x, p.y, p.z)
|
||||
if lz < 0 then table.insert(joints, joint.jointTransform) end
|
||||
end
|
||||
end
|
||||
return joints
|
||||
end
|
||||
|
||||
-- Vognens trækøje i traktorens rum set fra krogen.
|
||||
local function offsetOf(vehicle, jointNode, targetNode)
|
||||
local j, t = worldPos(jointNode), worldPos(targetNode)
|
||||
local x, _, z = worldDirectionToLocal(vehicle.rootNode, t.x - j.x, 0, t.z - j.z)
|
||||
return {x = x, z = z}
|
||||
end
|
||||
|
||||
-- Nærmeste frakoblede redskab (samme gård) med et trækøje bag en ledig krog. -> {wagon, joint, target, offset, plan} | nil
|
||||
function ADHitchCreep.findTarget(vehicle)
|
||||
local joints = freeRearJoints(vehicle)
|
||||
if #joints == 0 or g_currentMission == nil or g_currentMission.vehicleSystem == nil then return nil end
|
||||
local farmId = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() or nil
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, object in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
|
||||
local attachable = object.spec_attachable
|
||||
if object ~= vehicle and attachable ~= nil and object.spec_motorized == nil and attachable.inputAttacherJoints ~= nil
|
||||
and (object.getAttacherVehicle == nil or object:getAttacherVehicle() == nil)
|
||||
and (farmId == nil or object.getOwnerFarmId == nil or object:getOwnerFarmId() == farmId) then
|
||||
for _, input in ipairs(attachable.inputAttacherJoints) do
|
||||
if input.node ~= nil then
|
||||
for _, joint in ipairs(joints) do
|
||||
local offset = offsetOf(vehicle, joint, input.node)
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if distance < bestDistance and distance <= ADHitchCreep.CREEP_MAX_DISTANCE + 2 then
|
||||
bestDistance = distance
|
||||
best = {wagon = object, joint = joint, target = input.node, offset = offset}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if best ~= nil then best.plan = ADHitchCreep.plan(best.offset) end
|
||||
return best
|
||||
end
|
||||
|
||||
local function isInRange(vehicle, wagon)
|
||||
local spec = vehicle.spec_attacherJoints
|
||||
if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then return false end
|
||||
AttacherJoints.updateVehiclesInAttachRange(vehicle, ADHitchCreep.ROOMY_ATTACH_DISTANCE ^ 2, AttacherJoints.MAX_ATTACH_ANGLE, true)
|
||||
local info = spec.attachableInfo
|
||||
return info ~= nil and info.attachable == wagon
|
||||
end
|
||||
|
||||
-- AD-opgave (samme metoder som AD's AbstractTask).
|
||||
local CreepTask = {}
|
||||
CreepTask.__index = CreepTask
|
||||
|
||||
function CreepTask.new(adEnv, vehicle, target)
|
||||
return setmetatable({adEnv = adEnv, vehicle = vehicle, target = target, elapsedMs = 0, checkMs = 0, inRange = false}, CreepTask)
|
||||
end
|
||||
|
||||
function CreepTask:setUp()
|
||||
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
|
||||
self.startDistance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' bakker %.1f m hen til '%s' (%.1f m til siden)", vehicleName(self.vehicle),
|
||||
self.startDistance, vehicleName(self.target.wagon), offset.x)
|
||||
end
|
||||
|
||||
function CreepTask:finish(action, distance)
|
||||
local sdm = self.vehicle.ad.specialDrivingModule
|
||||
if sdm ~= nil and sdm.stopVehicle ~= nil then sdm:stopVehicle() end
|
||||
if action == "done" then
|
||||
Logging.info(ADHitchCreep.LOG_PREFIX .. "'%s' står ved '%s' — kobler på", vehicleName(self.vehicle), vehicleName(self.target.wagon))
|
||||
else
|
||||
Logging.warning(ADHitchCreep.LOG_PREFIX .. "'%s' opgav at bakke hen til '%s' (%.1f m tilbage efter %d s)", vehicleName(self.vehicle),
|
||||
vehicleName(self.target.wagon), distance, math.floor(self.elapsedMs / 1000))
|
||||
end
|
||||
local taskModule = self.adEnv.ADTaskModule
|
||||
self.vehicle.ad.taskModule:setCurrentTaskFinished(taskModule ~= nil and taskModule.DONT_PROPAGATE or 1)
|
||||
end
|
||||
|
||||
function CreepTask:update(dt)
|
||||
self.elapsedMs = self.elapsedMs + dt
|
||||
self.checkMs = self.checkMs + dt
|
||||
if self.target.wagon.getAttacherVehicle ~= nil and self.target.wagon:getAttacherVehicle() ~= nil then
|
||||
return self:finish("giveUp", 0) -- en anden har taget vognen
|
||||
end
|
||||
local offset = offsetOf(self.vehicle, self.target.joint, self.target.target)
|
||||
local distance = math.sqrt(offset.x ^ 2 + offset.z ^ 2)
|
||||
if self.checkMs >= ADHitchCreep.RANGE_CHECK_MS then
|
||||
self.checkMs = 0
|
||||
self.inRange = isInRange(self.vehicle, self.target.wagon)
|
||||
end
|
||||
local action = ADHitchCreep.step({inRange = self.inRange, distance = distance, startDistance = self.startDistance,
|
||||
elapsedMs = self.elapsedMs, passed = offset.z > 0.5})
|
||||
if action ~= "drive" then return self:finish(action, distance) end
|
||||
|
||||
local autoDrive, vehicle = self.adEnv.AutoDrive, self.vehicle
|
||||
local p = worldPos(self.target.target)
|
||||
local lx, lz = autoDrive.getDriveDirection(vehicle, p.x, p.y, p.z)
|
||||
local sdm = vehicle.ad.specialDrivingModule
|
||||
if sdm ~= nil and sdm.releaseVehicle ~= nil then sdm:releaseVehicle() end
|
||||
if vehicle.startMotor ~= nil and not vehicle:getIsMotorStarted() and vehicle:getCanMotorRun() then vehicle:startMotor() end
|
||||
-- som AD's ADSpecialDrivingModule:driveReverse (ikke-guidet): retning spejles ved bakning
|
||||
autoDrive.driveInDirection(vehicle, dt, 30, 0.3, 0.2, 20, true, false, -lx, -lz, ADHitchCreep.CREEP_SPEED, 1)
|
||||
end
|
||||
|
||||
function CreepTask:abort() end
|
||||
function CreepTask:finished() end
|
||||
function CreepTask:getInfoText() return "Bakker hen til vognen" end
|
||||
function CreepTask:getI18nInfo() return "" end
|
||||
function CreepTask:getExcludedVehiclesForCollisionCheck() return {self.target.wagon} end
|
||||
|
||||
local isInstalled = false
|
||||
|
||||
-- DriveToMode:handleFinishedTask lægger StopAndDisableADTask i køen; bakningen lægges ind lige før.
|
||||
function ADHitchCreep.install(adEnv)
|
||||
local mode = adEnv ~= nil and adEnv.DriveToMode or nil
|
||||
if isInstalled or mode == nil or mode.handleFinishedTask == nil or ADHitch == nil or ADHitch.pendingAttach == nil then return end
|
||||
isInstalled = true
|
||||
local original = mode.handleFinishedTask
|
||||
mode.handleFinishedTask = function(self)
|
||||
local ok, err = pcall(function()
|
||||
local job = self.driveToDestinationTask ~= nil and ADHitch.pendingAttach(self.vehicle) or nil
|
||||
if job == nil or job.markerId ~= self.destinationID then return end
|
||||
local target = ADHitchCreep.findTarget(self.vehicle)
|
||||
if target == nil or target.plan ~= "creep" then return end
|
||||
job.crept = true
|
||||
self.vehicle.ad.taskModule:addTask(CreepTask.new(adEnv, self.vehicle, target))
|
||||
end)
|
||||
if not ok then Logging.warning(ADHitchCreep.LOG_PREFIX .. "bakning til vognen: %s", tostring(err)) end
|
||||
return original(self)
|
||||
end
|
||||
end
|
||||
@@ -97,6 +97,19 @@ function ADLoadSwap.rankDumpCandidates(candidates, avoidMarkers)
|
||||
return ranked
|
||||
end
|
||||
|
||||
-- Flydende varer lægges kun i tanke/siloer, aldrig automatisk i en fabriks aflæsning: fabrikkerne har ofte en
|
||||
-- rist med præcis aflæsning, som en gyllevogns udløb bag på ikke rammer (in-game 26/9: 1 mio. l gylle holdt
|
||||
-- ved AIO'ens rist og spærrede vejen). Vælger brugeren selv fabrikken som lager, gælder valget.
|
||||
ADLoadSwap.LIQUIDS = {LIQUIDMANURE = true, DIGESTATE = true, MILK = true, BUFFALOMILK = true, GOATMILK = true,
|
||||
SHEEPMILK = true, WATER = true, LIQUIDFERTILIZER = true, HERBICIDE = true, METHANE = true, DIESEL = true}
|
||||
function ADLoadSwap.isLiquidName(name)
|
||||
return name ~= nil and ADLoadSwap.LIQUIDS[string.upper(tostring(name))] == true
|
||||
end
|
||||
function ADLoadSwap.isLiquid(fillType)
|
||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeNameByIndex ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or nil
|
||||
return ADLoadSwap.isLiquidName(name)
|
||||
end
|
||||
|
||||
function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables, avoidMarkers, allowFactories)
|
||||
if vehicle.rootNode == nil then
|
||||
return nil, "køretøjet har ingen position"
|
||||
@@ -166,7 +179,8 @@ end
|
||||
-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne.
|
||||
-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
|
||||
function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
|
||||
wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0)
|
||||
-- tilfældig start (0-2 s): ventende vogne tjekker ikke alle i samme frame
|
||||
wait.swapTimerMs = (wait.swapTimerMs or math.random(0, ADLoadSwap.CHECK_INTERVAL_MS)) + (dt or 0)
|
||||
if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-- AD Manual Transport
|
||||
-- Brede høstere (mejetærsker med skærebord) kan ikke køre på vejene: spilleren kører dem til marken, monterer
|
||||
-- skærebordet og stiger ud; moden overtager derfra. Rene regler (testbare uden spillet); spil-delen ligger i
|
||||
-- adFieldWork.
|
||||
|
||||
ADManualTransport = {}
|
||||
|
||||
ADManualTransport.REMIND_MS = 300000 -- påmindelse hvert 5. minut, mens moden venter på spilleren
|
||||
|
||||
-- Må høsteren vælges til marken? Det monterede skærebord/optager skal kunne tage afgrøden. Undtagelse: en manuel
|
||||
-- høster UDEN skærebord monteret (det monteres først på marken — tjekkes ved overtagelse). En roeoptager har sin
|
||||
-- optager fast på og kan aldrig tage hvede (in-game 27/9: F111 valgte roeoptageren).
|
||||
function ADManualTransport.acceptsHarvester(candidate, fruit)
|
||||
if fruit == nil then return false end
|
||||
local fruits = candidate.fruits or {}
|
||||
if candidate.manual and next(fruits) == nil then return true end
|
||||
return fruits[fruit] == true
|
||||
end
|
||||
|
||||
-- Klar til overtagelse? s = {atField, fruits = {[fruit] = true}, fruit, controlled, forced}.
|
||||
-- Returnerer true, eller false + grund: "notThere" | "noHeader" | "wrongHeader" | "playerInside".
|
||||
-- forced ("Start nu"): sted og spiller springes over, skærebordet tjekkes stadig.
|
||||
function ADManualTransport.readiness(s)
|
||||
if not s.forced and not s.atField then return false, "notThere" end
|
||||
local fruits = s.fruits or {}
|
||||
if next(fruits) == nil then return false, "noHeader" end
|
||||
if s.fruit == nil or not fruits[s.fruit] then return false, "wrongHeader" end
|
||||
if not s.forced and s.controlled then return false, "playerInside" end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADManualTransport.shouldRemind(lastAt, now)
|
||||
return lastAt == nil or now - lastAt >= ADManualTransport.REMIND_MS
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
-- AD Month View
|
||||
-- Rene regler for fanen Måned: hvilke marker har arbejde i gang, hvor langt er de, hvad kommer bagefter,
|
||||
-- og lukker vinduet (høst/såning) ved månedsskiftet? GUI'et oversætter nøglerne; spillet kaldes kun via
|
||||
-- ctx-funktionerne (testbart uden FS).
|
||||
|
||||
ADMonthView = {}
|
||||
|
||||
ADMonthView.PERIODS = 12
|
||||
-- Trin der kræver afgrøden eller såvinduet: sorteres før pleje/klargøring
|
||||
ADMonthView.MAIN_STAGES = {harvesting = true, mowing = true, sowing = true}
|
||||
-- Trin før såningen i klargøringskæden (lukker såvinduet, bliver marken ikke sået)
|
||||
ADMonthView.BEFORE_SOWING = {mowing = true, tedding = true, raking = true, collecting = true, harvesting = true,
|
||||
strawCollecting = true, baling = true, baleCollecting = true, mulching = true,
|
||||
liming = true, plowing = true, cultivating = true, sowing = true}
|
||||
|
||||
function ADMonthView.nextPeriod(period)
|
||||
if type(period) ~= "number" or period < 1 then return 1 end
|
||||
return period % ADMonthView.PERIODS + 1
|
||||
end
|
||||
|
||||
-- Visse FS25-sæsondata gemmer overgangene som lister af {startState, endState}; strukturen er ikke
|
||||
-- dokumenteret, så alle tabeller i periodens data gennemsøges. Ukendt = nej (ingen falsk alarm).
|
||||
local function findsTransition(data, fromState, toState, depth)
|
||||
if type(data) ~= "table" or depth > 3 then return false end
|
||||
if data.startState == fromState and data.endState == toState then return true end
|
||||
for _, value in pairs(data) do
|
||||
if type(value) == "table" and findsTransition(value, fromState, toState, depth + 1) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Visner afgrøden (stadie state) ved overgangen til periode nextPeriod?
|
||||
function ADMonthView.withersIn(fruit, nextPeriod, state)
|
||||
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
|
||||
local withered = fruit.witheredState
|
||||
if type(withered) ~= "number" or withered <= 0 then return false end
|
||||
local periods = fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
|
||||
local data = periods ~= nil and periods[nextPeriod] or nil
|
||||
return findsTransition(data, state, withered, 0)
|
||||
end
|
||||
|
||||
-- Såvindue for en afgrøde: "closes" (kan sås nu, ikke næste måned), "open" (også næste måned), "closed".
|
||||
function ADMonthView.sowWindow(plantableIn, fruitName, period)
|
||||
if fruitName == nil or fruitName == "" or plantableIn == nil then return "open" end
|
||||
local now = plantableIn(fruitName, period)
|
||||
if not now then return "closed" end
|
||||
return plantableIn(fruitName, ADMonthView.nextPeriod(period)) and "open" or "closes"
|
||||
end
|
||||
|
||||
-- Er marken i gang (vises i tabellen)? Venter/færdig og passiv "klar" (manuel uden Start nu) er ikke.
|
||||
function ADMonthView.isActive(field)
|
||||
if field == nil then return false end
|
||||
if field.stage == "idle" or field.stage == "done" then return false end
|
||||
if field.stage == "ready" then return ADFieldModel.shouldStart(field) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Næste trin i kæden (uden at ændre marken). nil = intet (pleje går tilbage til venter, sidste trin -> færdig).
|
||||
function ADMonthView.nextStage(field, info)
|
||||
if field.stage == "ready" then
|
||||
local stages = ADFieldModel.workStages(ADFieldModel.applyTarget(field.plan, info.kind, info.fruitName), info.kind, info.needs)
|
||||
return stages[1]
|
||||
end
|
||||
if ADFieldModel.isCare(field.stage) then return nil end
|
||||
local nextField = ADFieldModel.advance(field, info.needs)
|
||||
if nextField.stage == "done" or nextField.stage == "idle" then return nil end
|
||||
return nextField.stage
|
||||
end
|
||||
|
||||
-- Vindue for en aktiv mark: "harvestCloses" | "lastSowMonth" | "ok".
|
||||
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod)}
|
||||
function ADMonthView.window(field, info, ctx)
|
||||
local stage = field.stage
|
||||
local cropStage = stage == "harvesting" or stage == "mowing" or stage == "ready"
|
||||
if cropStage and info.kind ~= "bare" and ctx.withers ~= nil and ctx.withers(info, ADMonthView.nextPeriod(ctx.period)) then
|
||||
return "harvestCloses"
|
||||
end
|
||||
local sowsLater = field.plan ~= nil and field.plan.sow and ADMonthView.BEFORE_SOWING[stage]
|
||||
if sowsLater and ADMonthView.sowWindow(ctx.plantableIn, ADFieldModel.sowFruit(field), ctx.period) == "closes" then
|
||||
return "lastSowMonth"
|
||||
end
|
||||
return "ok"
|
||||
end
|
||||
|
||||
-- Fremdrift 0-1. Høst/slåning: andelen af HELE arealet der stadig står (standingRatio) — readyRatio duer ikke,
|
||||
-- for slået/høstet afgrøde forsvinder også fra dens nævner (in-game 27/9 F90: halvt slået = 0 %), og
|
||||
-- Courseplays tal er kun positionen i kurset (starter forfra på 0 ved genstart). Øvrige trin: Courseplay.
|
||||
function ADMonthView.progress(field, info, cpProgress)
|
||||
local cropStage = field.stage == "harvesting" or field.stage == "mowing"
|
||||
if cropStage and type(info.standingRatio) == "number" then
|
||||
return math.max(0, math.min(1, 1 - info.standingRatio))
|
||||
end
|
||||
if type(cpProgress) == "number" then return math.max(0, math.min(1, cpProgress)) end
|
||||
if cropStage and type(info.readyRatio) == "number" then
|
||||
return math.max(0, math.min(1, 1 - info.readyRatio))
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function priority(row)
|
||||
if row.window ~= "ok" then return 0 end
|
||||
if ADMonthView.MAIN_STAGES[row.stage] then return 1 end
|
||||
return 2
|
||||
end
|
||||
|
||||
-- Hele fanen: {rows = {...}, critical = n, upcoming = {...}}.
|
||||
-- infos = ADFields.list(), fieldsById = ADFieldJobs.state.fields,
|
||||
-- ctx = {period, plantableIn(name, period), withers(info, nextPeriod), cpProgress(fieldId) -> 0-1|nil}
|
||||
function ADMonthView.build(infos, fieldsById, ctx)
|
||||
local rows, upcoming, critical = {}, {}, 0
|
||||
for _, info in ipairs(infos or {}) do
|
||||
local field = fieldsById[info.id]
|
||||
if field ~= nil and ADMonthView.isActive(field) then
|
||||
local row = {id = info.id, stage = field.stage, crop = info.cropTitle, sowFruit = nil,
|
||||
progress = ADMonthView.progress(field, info, ctx.cpProgress ~= nil and ctx.cpProgress(info.id) or nil),
|
||||
nextStage = ADMonthView.nextStage(field, info), window = ADMonthView.window(field, info, ctx)}
|
||||
if field.stage == "sowing" or (info.kind == "bare" and ADMonthView.BEFORE_SOWING[field.stage]) then
|
||||
row.sowFruit = field.plan.sow and ADFieldModel.sowFruit(field) or nil
|
||||
end
|
||||
if row.window ~= "ok" then critical = critical + 1 end
|
||||
table.insert(rows, row)
|
||||
elseif field ~= nil and info.kind == "bare" and (field.stage == "idle" or field.stage == "done") then
|
||||
local target = field.plan.targetFruit
|
||||
if target ~= nil and target ~= "" and ctx.plantableIn ~= nil and ctx.plantableIn(target, ctx.period) then
|
||||
table.insert(upcoming, {id = info.id, fruit = target,
|
||||
closes = ADMonthView.sowWindow(ctx.plantableIn, target, ctx.period) == "closes"})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(rows, function(a, b)
|
||||
local pa, pb = priority(a), priority(b)
|
||||
if pa ~= pb then return pa < pb end
|
||||
return a.id < b.id
|
||||
end)
|
||||
table.sort(upcoming, function(a, b)
|
||||
if a.closes ~= b.closes then return a.closes end
|
||||
return a.id < b.id
|
||||
end)
|
||||
return {rows = rows, critical = critical, upcoming = upcoming}
|
||||
end
|
||||
|
||||
-- "50 %" / "–"
|
||||
function ADMonthView.percentText(progress)
|
||||
if progress == nil then return "–" end
|
||||
return string.format("%d %%", math.floor(progress * 100 + 0.5))
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
-- AD Names
|
||||
-- Egne bygningsnavne (FS25 kan ikke omdøbe bygninger): ADSmartPickup_names.xml i savegame-mappen,
|
||||
-- <ADSmartPickupNames><building id="placeable…" name="Gårdsilo 10"/></ADSmartPickupNames>.
|
||||
-- Moden tilføjer kun navne (nye bygninger / Omdøb-knappen) og ændrer aldrig et eksisterende navn af sig selv.
|
||||
|
||||
ADNames = {}
|
||||
ADNames.FILE_NAME = "ADSmartPickup_names.xml"
|
||||
ADNames.ROOT = "ADSmartPickupNames"
|
||||
ADNames.byId = nil
|
||||
|
||||
-- records = {{id, name}} -> {[id] = name}; tomme/ugyldige springes over
|
||||
function ADNames.fromRecords(records)
|
||||
local byId = {}
|
||||
for _, record in ipairs(records or {}) do
|
||||
local id, name = record.id, record.name
|
||||
if type(id) == "string" and id ~= "" and type(name) == "string" and name:match("%S") then byId[id] = name end
|
||||
end
|
||||
return byId
|
||||
end
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADNames.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADNames.load()
|
||||
ADNames.byId = {}
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupNames", path)
|
||||
if xmlFile == nil then return end
|
||||
local records = {}
|
||||
xmlFile:iterate(ADNames.ROOT .. ".building", function(_, key)
|
||||
table.insert(records, {id = xmlFile:getString(key .. "#id"), name = xmlFile:getString(key .. "#name")})
|
||||
end)
|
||||
xmlFile:delete()
|
||||
ADNames.byId = ADNames.fromRecords(records)
|
||||
local count = 0
|
||||
for _ in pairs(ADNames.byId) do count = count + 1 end
|
||||
Logging.info("ADSmartPickup: %d egne bygningsnavne indlæst", count)
|
||||
end
|
||||
|
||||
-- Eget navn for bygningen (placeable), ellers nil.
|
||||
function ADNames.forPlaceable(placeable)
|
||||
if placeable == nil or placeable.uniqueId == nil then return nil end
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
return (ADNames.byId or {})[tostring(placeable.uniqueId)]
|
||||
end
|
||||
|
||||
function ADNames.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupNames", path, ADNames.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local ids = {}
|
||||
for id in pairs(ADNames.byId or {}) do table.insert(ids, id) end
|
||||
table.sort(ids)
|
||||
for index, id in ipairs(ids) do
|
||||
local key = string.format("%s.building(%d)", ADNames.ROOT, index - 1)
|
||||
xmlFile:setString(key .. "#id", id)
|
||||
xmlFile:setString(key .. "#name", ADNames.byId[id])
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Omdøb (knappen i bygningsfanerne): gemmes straks; tomt navn = tilbage til butiksnavnet.
|
||||
function ADNames.set(id, name)
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
local trimmed = type(name) == "string" and name:match("^%s*(.-)%s*$") or ""
|
||||
ADNames.byId[id] = trimmed ~= "" and trimmed or nil
|
||||
pcall(ADNames.save)
|
||||
Logging.info("ADSmartPickup: bygning %s omdøbt til '%s'", tostring(id), trimmed)
|
||||
if ADBuildings ~= nil and ADBuildings.invalidate ~= nil then ADBuildings.invalidate() end
|
||||
end
|
||||
|
||||
local function position(placeable)
|
||||
local node = placeable ~= nil and placeable.rootNode or nil
|
||||
if node == nil then return nil end
|
||||
local x, _, z = getWorldTranslation(node)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function animalTypeName(placeable)
|
||||
local ok, name = pcall(function()
|
||||
local index = placeable:getAnimalTypeIndex()
|
||||
local animalType = g_currentMission.animalSystem:getTypeByIndex(index)
|
||||
return animalType ~= nil and animalType.name or nil
|
||||
end)
|
||||
return ok and name or nil
|
||||
end
|
||||
|
||||
local function rawName(placeable)
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
return ok and name ~= nil and tostring(name) or nil
|
||||
end
|
||||
|
||||
-- Mark-markører ("F118") fra AutoDrive: {{name, x, z}}
|
||||
local function fieldMarkers(adEnv)
|
||||
local result = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if tostring(marker.name):match("^F%d+$") then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
if wayPoint ~= nil then table.insert(result, {name = marker.name, x = wayPoint.x, z = wayPoint.z}) end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Nye bygninger (uden navn) får et navn efter ADNaming; kaldes når bygningslisten bygges. Retter navnene i
|
||||
-- listen med det samme og gemmer filen, hvis der kom nye.
|
||||
function ADNames.autoName(adEnv, buildings)
|
||||
if ADNames.byId == nil then pcall(ADNames.load) end
|
||||
local existing = ADNames.byId or {}
|
||||
local missing = false
|
||||
for _, building in ipairs(buildings) do if existing[building.id] == nil and building.kind ~= "sell" then missing = true break end end
|
||||
if not missing then return end
|
||||
-- gårdens klynge: valgte gårdlagre i marksilo-flowet + siloer der allerede hedder "Gårdsilo …", og naboer ≤ 60 m
|
||||
local silos, farm, queue = {}, {}, {}
|
||||
for _, building in ipairs(buildings) do
|
||||
if building.kind == "silo" then
|
||||
local x, z = position(building.placeable)
|
||||
if x ~= nil then
|
||||
local silo = {id = building.id, x = x, z = z}
|
||||
table.insert(silos, silo)
|
||||
local isStore = ADFieldSilos ~= nil and ADFieldSiloConfig ~= nil and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id)
|
||||
if isStore or tostring(existing[building.id] or ""):match("^Gårdsilo") then farm[building.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
end
|
||||
end
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue)
|
||||
for _, silo in ipairs(silos) do
|
||||
if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= 3600 then farm[silo.id] = true; table.insert(queue, silo) end
|
||||
end
|
||||
end
|
||||
local fields = fieldMarkers(adEnv)
|
||||
local inputs = {}
|
||||
for _, building in ipairs(buildings) do
|
||||
if existing[building.id] == nil then
|
||||
local x, z = position(building.placeable)
|
||||
local nearest = nil
|
||||
if x ~= nil and building.kind == "silo" then
|
||||
for _, field in ipairs(fields) do
|
||||
local distance = math.sqrt((field.x - x) ^ 2 + (field.z - z) ^ 2)
|
||||
if nearest == nil or distance < nearest.distance then nearest = {name = field.name, distance = distance} end
|
||||
end
|
||||
end
|
||||
table.insert(inputs, {id = building.id, kind = building.kind, animal = building.kind == "husbandry" and animalTypeName(building.placeable) or nil,
|
||||
storeName = rawName(building.placeable), inFarmCluster = farm[building.id] == true, nearestField = nearest})
|
||||
end
|
||||
end
|
||||
local added = ADNaming.assign(inputs, existing)
|
||||
local count = 0
|
||||
for id, name in pairs(added) do
|
||||
existing[id] = name
|
||||
count = count + 1
|
||||
Logging.info("ADSmartPickup: ny bygning navngivet '%s' (%s)", name, id)
|
||||
end
|
||||
if count == 0 then return end
|
||||
ADNames.byId = existing
|
||||
for _, building in ipairs(buildings) do
|
||||
if added[building.id] ~= nil then building.name = added[building.id] end
|
||||
end
|
||||
pcall(ADNames.save)
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
-- AD Naming
|
||||
-- Regler for automatiske navne på bygninger uden eget navn (rene funktioner; spil-delen samler input i adNames).
|
||||
-- Stalde efter dyretype, siloer efter gårdens klynge/nærmeste mark, palleskure nummereret, fabrikker efter
|
||||
-- butiksnavnet. Numrene fortsætter fra de navne, der allerede findes ("Svinestald 1-5" -> "Svinestald 6").
|
||||
|
||||
ADNaming = {}
|
||||
|
||||
ADNaming.ANIMAL_PREFIX = {COW = "Kostald", PIG = "Svinestald", SHEEP = "Fårestald", GOAT = "Gedestald",
|
||||
CHICKEN = "Hønsehus", HORSE = "Hestestald"}
|
||||
ADNaming.FIELD_SILO_DISTANCE = 200
|
||||
|
||||
local function escape(text) return (tostring(text):gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")) end
|
||||
|
||||
-- Højeste "prefix N" blandt navnene + 1.
|
||||
function ADNaming.nextNumber(names, prefix)
|
||||
local highest, pattern = 0, "^" .. escape(prefix) .. " (%d+)$"
|
||||
for _, name in ipairs(names) do
|
||||
local number = tonumber(tostring(name):match(pattern))
|
||||
if number ~= nil and number > highest then highest = number end
|
||||
end
|
||||
return highest + 1
|
||||
end
|
||||
|
||||
local function contains(names, wanted)
|
||||
for _, name in ipairs(names) do if name == wanted then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Et fast navn der kan være brugt: "X", ellers "X 2", "X 3" ...
|
||||
local function uniqueFixed(names, base)
|
||||
if not contains(names, base) then return base end
|
||||
return string.format("%s %d", base, math.max(2, ADNaming.nextNumber(names, base)))
|
||||
end
|
||||
|
||||
-- Navn for én bygning (nil = navngives ikke, fx salgssteder).
|
||||
-- b = {kind, animal, storeName, inFarmCluster, nearestField = {name, distance}}
|
||||
local function nameFor(b, names)
|
||||
if b.kind == "husbandry" then
|
||||
local prefix = ADNaming.ANIMAL_PREFIX[tostring(b.animal or ""):upper()] or "Stald"
|
||||
return string.format("%s %d", prefix, ADNaming.nextNumber(names, prefix))
|
||||
elseif b.kind == "silo" then
|
||||
if b.inFarmCluster then return string.format("Gårdsilo %d", ADNaming.nextNumber(names, "Gårdsilo")) end
|
||||
local field = b.nearestField
|
||||
if field ~= nil and field.name ~= nil and (field.distance or math.huge) <= ADNaming.FIELD_SILO_DISTANCE then
|
||||
return uniqueFixed(names, field.name .. " marksilo")
|
||||
end
|
||||
return string.format("Silo %d", ADNaming.nextNumber(names, "Silo"))
|
||||
elseif b.kind == "objectStorage" then
|
||||
return string.format("Palleskur %d", ADNaming.nextNumber(names, "Palleskur"))
|
||||
elseif b.kind == "sell" then
|
||||
return nil
|
||||
end
|
||||
if b.storeName == nil or b.storeName == "" then return nil end
|
||||
return uniqueFixed(names, b.storeName)
|
||||
end
|
||||
|
||||
-- Navne til bygninger der ikke har et endnu. existing = {[id] = navn}; returnerer kun de nye {[id] = navn}.
|
||||
function ADNaming.assign(buildings, existing)
|
||||
local names, result = {}, {}
|
||||
for _, name in pairs(existing or {}) do table.insert(names, name) end
|
||||
for _, b in ipairs(buildings or {}) do
|
||||
if b.id ~= nil and (existing or {})[b.id] == nil then
|
||||
local name = nameFor(b, names)
|
||||
if name ~= nil then
|
||||
result[b.id] = name
|
||||
table.insert(names, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Markmaskiner: navn efter rolle (høstere efter type: "beet" = roe-/kartoffeloptager, "forage" = finsnitter).
|
||||
ADNaming.ROLE_PREFIX = {collector = "Læssevogn", mower = "Skårlægger", tedder = "Vender", rake = "River",
|
||||
baler = "Ballepresser", baleCollector = "Ballesamler", seeder = "Såmaskine", roller = "Tromle",
|
||||
weeder = "Ukrudtsmaskine", fertilizer = "Gødningsspreder", limer = "Kalkspreder", herbicide = "Sprøjte",
|
||||
mulcher = "Stubhakker", cultivator = "Kultivator", plow = "Plov", unloader = "Tømmevogn"}
|
||||
ADNaming.HARVESTER_PREFIX = {beet = "Roeoptager", forage = "Finsnitter"}
|
||||
|
||||
function ADNaming.fleetName(role, subtype, names)
|
||||
local prefix
|
||||
if role == "harvester" then
|
||||
prefix = ADNaming.HARVESTER_PREFIX[subtype] or "Mejetærsker"
|
||||
else
|
||||
prefix = ADNaming.ROLE_PREFIX[role]
|
||||
end
|
||||
if prefix == nil then return nil end
|
||||
return string.format("%s %d", prefix, ADNaming.nextNumber(names or {}, prefix))
|
||||
end
|
||||
@@ -14,6 +14,8 @@ ADOutbound = {}
|
||||
ADOutbound.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
|
||||
ADOutbound.CHECK_INTERVAL_MS = 5000
|
||||
-- spred over HELE intervallet, så genplanlægningerne (og cache-genberegninger) fordeles jævnt
|
||||
ADOutbound.CHECK_JITTER_MS = 5000
|
||||
-- "Kør uanset mængde": stiger vognens fyldning ikke så længe ved kilden, køres der med det der er.
|
||||
ADOutbound.NO_PROGRESS_MS = 15000
|
||||
-- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl).
|
||||
@@ -25,6 +27,9 @@ ADOutbound.NOTIFICATION_MS = 8000
|
||||
-- spawnet, forkert farm, ...): kilden udelukkes for varen i EMPTY_SOURCE_TTL_MS og turen afsluttes.
|
||||
ADOutbound.EMPTY_SOURCE_MS = 30000
|
||||
ADOutbound.EMPTY_SOURCE_TTL_MS = 300000
|
||||
-- kilde der gav lidt og så intet mere (fx paller uden for Universal Autoloads rækkevidde): hviler så længe,
|
||||
-- så de andre kilder kommer til (in-game 26/9: æg-vognen kørte til samme hønsehus tre gange i træk)
|
||||
ADOutbound.THIN_SOURCE_REST_MS = 180000
|
||||
-- Autoload-bil på vej til aflæsning: stropperne løsnes så mange meter FØR markøren. Markøren står
|
||||
-- hvor næsen stopper, og en lastbils lad har passeret salgstriggeren dér; AutoDrive løsner selv
|
||||
-- først ved markøren ("AL unload = center"), hvilket er for sent for en lastbil.
|
||||
@@ -42,6 +47,11 @@ local warned = setmetatable({}, {__mode = "k"})
|
||||
local emptyUntil = {}
|
||||
local isInstalled = false
|
||||
|
||||
-- Glem alle "tom/hvilende kilde"-markeringer (tests; og når runs genindlæses).
|
||||
function ADOutbound.resetEmptySources()
|
||||
emptyUntil = {}
|
||||
end
|
||||
|
||||
local function now()
|
||||
if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
|
||||
return g_currentMission.time
|
||||
@@ -176,6 +186,12 @@ local function findSellQuote(adEnv, vehicle, product, sellMarkers, sellAtPercent
|
||||
if unreachableName ~= nil then
|
||||
return nil, unreachableName
|
||||
end
|
||||
-- ingen pris over grænsen: det bedste salgssted der kan nås (bruges af "sælg altid over X %")
|
||||
for _, quote in ipairs(quotes) do
|
||||
if homeId == nil or ADWaitPool.hasRouteBothWays(adEnv, vehicle, quote.wayPointId, homeId) then
|
||||
return quote, nil
|
||||
end
|
||||
end
|
||||
return quotes[1], nil
|
||||
end
|
||||
|
||||
@@ -241,7 +257,9 @@ local function findStoreMarker(adEnv, vehicle, product, capacity, sources, liter
|
||||
return {wayPointId = chosen.id, name = tostring(chosen.name), free = liters}
|
||||
end
|
||||
local avoid = ADRunsController ~= nil and ADRunsController.getPickupMarkers ~= nil and ADRunsController.getPickupMarkers() or nil
|
||||
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources), avoid, true)
|
||||
-- flydende varer (gylle, digestat, mælk ...) kun i tanke/siloer — ikke automatisk i en fabriks rist
|
||||
local allowFactories = not (ADLoadSwap.isLiquid ~= nil and ADLoadSwap.isLiquid(product.fillType))
|
||||
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources), avoid, allowFactories)
|
||||
if marker == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -346,6 +364,7 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle)
|
||||
hasAutoload = hasAutoload,
|
||||
sellAtPercent = run.sellAtPercent,
|
||||
storeAbovePercent = run.storeAbovePercent,
|
||||
sellAbovePercent = run.sellAbovePercent,
|
||||
collectRound = run.collectRound == true,
|
||||
products = products,
|
||||
}
|
||||
@@ -474,7 +493,17 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||||
local isReachable = function(candidate)
|
||||
return ADWaitPool.hasRouteTo(adEnv, vehicle, candidate.marker.id)
|
||||
end
|
||||
local candidate = ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable)
|
||||
-- "All": nærmest flowets egne kilder, ikke nærmest traktoren (in-game 29/9: Milk Store ved sukkerroeknuseren)
|
||||
local anchorX, anchorZ = nil, nil
|
||||
if run.waitPoolGroup == nil or run.waitPoolGroup == "" then
|
||||
local points = {}
|
||||
for _, id in ipairs(run.sourceWayPointIds or {}) do
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(id)
|
||||
if wayPoint ~= nil then table.insert(points, {x = wayPoint.x, z = wayPoint.z}) end
|
||||
end
|
||||
anchorX, anchorZ = ADWaitPool.anchorOf(points)
|
||||
end
|
||||
local candidate = ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable, anchorX, anchorZ)
|
||||
local marker, source = nil, nil
|
||||
if candidate ~= nil then
|
||||
ADWaitPool.claim(candidate.marker.id, vehicle)
|
||||
@@ -490,7 +519,8 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||||
warnOnce(adEnv, vehicle, "noWait", string.format("'%s' har intet ledigt Wait-punkt med vejnet (run '%s'); holder ved kilden %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(run.name), tostring(marker.name)))
|
||||
end
|
||||
local previous = holds[vehicle]
|
||||
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = 0}
|
||||
-- tilfældig start (0-2 s) så holdende vogne ikke genplanlægger i samme frame (hak hvert 5. s, 26/9)
|
||||
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = math.random(0, ADOutbound.CHECK_JITTER_MS)}
|
||||
local message = string.format("'%s' holder ved %s: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker.name), table.concat(reasons, "; "))
|
||||
if previous == nil or previous.lastLogged ~= message then
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
|
||||
@@ -753,7 +783,12 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||||
return true
|
||||
end
|
||||
if state.noProgressMs > ADOutbound.NO_PROGRESS_MS and level > 0 and not spawnPending then
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), level)
|
||||
local rests = now() + ADOutbound.THIN_SOURCE_REST_MS
|
||||
for _, fillType in ipairs(state.trip.fillTypes) do
|
||||
local key = tostring(state.current) .. ":" .. tostring(fillType)
|
||||
if emptyUntil[key] == nil or emptyUntil[key] < rests then emptyUntil[key] = rests end
|
||||
end
|
||||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l (kilden hviler %d min, så de andre kommer til)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), level, math.floor(ADOutbound.THIN_SOURCE_REST_MS / 60000))
|
||||
state.noProgressMs, state.completed = 0, true
|
||||
task:finished()
|
||||
return true
|
||||
|
||||
@@ -68,6 +68,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -123,7 +125,7 @@ local function collectCandidates(input)
|
||||
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 and canSellWithoutReserve(e, input) then
|
||||
if wantsSell(product.policy) and (e.priceOk or e.overflow) and product.quote ~= nil and canSellWithoutReserve(e, input) 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 or (input.feedMode and e.available >= ADOutboundPlanner.FEED_MIN_LITERS)) and product.store ~= nil then
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
-- 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
|
||||
|
||||
-- Butikskategori: markredskaber kun bag traktorer (tractorsS/M/L …) — ikke lastbil/Unimog, læssere o.l.
|
||||
-- Tømmevogne må alle trække. Ukendt kategori tillades (intet må gå i stå på manglende data).
|
||||
ADPool.ANY_PULLER_ROLES = {unloader = true}
|
||||
|
||||
function ADPool.categoryOk(category, role)
|
||||
if category == nil or ADPool.ANY_PULLER_ROLES[role] then return true end
|
||||
return tostring(category):lower():find("^tractors") ~= nil
|
||||
end
|
||||
|
||||
local function suits(tractor, implement, role)
|
||||
return ADPool.powerOk(tractor.hp, implement.neededHp) and ADPool.categoryOk(tractor.category, role)
|
||||
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 == nil or 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
|
||||
|
||||
-- Når opgaven slutter, sættes puljeredskabet af — også når AutoDrive ellers selv ville parkere (påfyldte
|
||||
-- sæt): AD parkerer ved redskabets plads med redskabet på. Aldrig ved manuel transport.
|
||||
-- Bedste pasform: den mindste traktor med hk nok; afstand afgør kun mellem traktorer inden for FIT_BAND af den
|
||||
-- mindste. Over OVERSIZE_FACTOR x behovet kun når intet andet passer; ukendt traktor-hk efter de kendte.
|
||||
-- Redskab uden hk-data: nærmeste (intet at matche på). tractors = allerede filtrerede kandidater.
|
||||
ADPool.FIT_BAND = 1.15
|
||||
ADPool.OVERSIZE_FACTOR = 2.5
|
||||
|
||||
function ADPool.bestFit(tractors, implement, role)
|
||||
local fitting = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if suits(tractor, implement, role) then table.insert(fitting, tractor) end
|
||||
end
|
||||
local needed = implement.neededHp
|
||||
if needed == nil or needed <= 0 then return nearest(fitting, implement.x, implement.z) end
|
||||
local sized, oversized, unknown = {}, {}, {}
|
||||
for _, tractor in ipairs(fitting) do
|
||||
if tractor.hp == nil or tractor.hp <= 0 then table.insert(unknown, tractor)
|
||||
elseif tractor.hp > needed * ADPool.OVERSIZE_FACTOR then table.insert(oversized, tractor)
|
||||
else table.insert(sized, tractor) end
|
||||
end
|
||||
local pool = #sized > 0 and sized or (#unknown > 0 and unknown or oversized)
|
||||
if pool ~= unknown then
|
||||
local smallest = math.huge
|
||||
for _, tractor in ipairs(pool) do smallest = math.min(smallest, tractor.hp) end
|
||||
local band = {}
|
||||
for _, tractor in ipairs(pool) do
|
||||
if tractor.hp <= smallest * ADPool.FIT_BAND then table.insert(band, tractor) end
|
||||
end
|
||||
pool = band
|
||||
end
|
||||
return nearest(pool, implement.x, implement.z)
|
||||
end
|
||||
|
||||
function ADPool.shouldReturnHome(assignment)
|
||||
return not assignment.manual
|
||||
end
|
||||
|
||||
-- Kun maskiner uden egen arbejdsrolle (almindelige traktorer) henter puljeredskaber — ikke mejetærskere,
|
||||
-- selvkørende sprøjter o.l., selv om de har koblingspunkter.
|
||||
function ADPool.canPull(role)
|
||||
return role == nil
|
||||
end
|
||||
|
||||
-- Hvem fik koblingen til at fejle? Kom traktoren ikke frem, er det traktoren (ingen AD-rute), ellers redskabet.
|
||||
function ADPool.failureBlames(reason)
|
||||
if tostring(reason):find("^stoppedAway") ~= nil then return "tractor" end
|
||||
return "implement"
|
||||
end
|
||||
|
||||
-- Efter koblingen: refill = markarbejdets påfyldningsplan (nil = intet at fylde).
|
||||
function ADPool.nextDrive(refill)
|
||||
if refill == nil or not refill.empty then return "drive" end
|
||||
if refill.refillId ~= nil then return "load" end
|
||||
return "empty"
|
||||
end
|
||||
|
||||
-- Puljen bruges til opgaver med ét sæt (også tømmevogne ved høst), ikke når marken har egne faste maskiner
|
||||
-- til rollen.
|
||||
function ADPool.mayUsePool(decision, hasOwnForRole)
|
||||
return (decision.count or 1) == 1 and not hasOwnForRole
|
||||
end
|
||||
|
||||
-- Kan puljevognen bære varen? (implement.carries = {[fillType] = true}; nil vare = intet krav)
|
||||
function ADPool.canCarry(implement, fillType)
|
||||
if fillType == nil then return true end
|
||||
return implement.carries ~= nil and implement.carries[fillType] == true
|
||||
end
|
||||
|
||||
-- Status til flådefanen. s = {parkName, hasConnect, attachedTo, quarantineMin, reserved} -> nøgle, argument
|
||||
function ADPool.implementStatus(s)
|
||||
if s.parkName == nil then return "noPark" end
|
||||
if not s.hasConnect then return "noConnect", s.parkName end
|
||||
if s.attachedTo ~= nil then return "attached", s.attachedTo end
|
||||
if s.flowName ~= nil then return "flow", s.flowName end
|
||||
if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end
|
||||
if s.reserved then return "reserved" end
|
||||
return "parked", s.parkName
|
||||
end
|
||||
|
||||
-- s = {hasPark, enabled, controlled, returning, fieldId, busy, offNetwork, hitched} -> nøgle, argument
|
||||
function ADPool.tractorStatus(s)
|
||||
if not s.hasPark then return "noPark" end
|
||||
if s.controlled then return "player" end
|
||||
if s.returning then return "returning" end
|
||||
if s.fieldId ~= nil then return "job", s.fieldId end
|
||||
if s.busy then return "busy" end
|
||||
if not s.enabled then return "notInFleet" end
|
||||
if s.offNetwork then return "offNetwork" end
|
||||
if s.hitched then return "hitched" end
|
||||
return "free"
|
||||
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] and not tractor.offNetwork 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 = ADPool.bestFit(free, implement, role)
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
if weakest == nil or (implement.neededHp or 0) < (weakest.neededHp or 0) then weakest = implement end
|
||||
end
|
||||
return nil, "tooWeak", {weakest ~= nil and weakest.neededHp or 0, weakest ~= nil and weakest.name or "?"}
|
||||
end
|
||||
|
||||
-- Alle frie puljeredskaber med rollen, hver parret med den nærmeste endnu ubrugte traktor med nok hk
|
||||
-- (marksilo-flowet planlægger over mange vogne på én gang).
|
||||
function ADPool.pairAll(role, implements, tractors, blocked)
|
||||
blocked = blocked or {}
|
||||
local used, result = {}, {}
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if hasRole(implement, role) and not blocked[implement.id] then
|
||||
local open = {}
|
||||
for _, t in ipairs(tractors or {}) do
|
||||
if not used[t.id] and not blocked[t.id] and not t.offNetwork then table.insert(open, t) end
|
||||
end
|
||||
local tractor = ADPool.bestFit(open, implement, role)
|
||||
if tractor ~= nil then
|
||||
used[tractor.id] = true
|
||||
table.insert(result, {implement = implement, tractor = tractor})
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Ventende marker prøver igen på fælles taktslag (ikke hver sin 30 s fra eget forsøg): så udløber de i samme
|
||||
-- tick, og køens rækkefølge (længst ventende først) afgør hvem der får den næste ledige traktor.
|
||||
function ADPool.alignedRetry(nowMs, periodMs)
|
||||
return (math.floor(nowMs / periodMs) + 1) * periodMs
|
||||
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.quarantineMinutes(id, nowMs)
|
||||
local untilMs = quarantined[id]
|
||||
if untilMs == nil or nowMs >= untilMs then return 0 end
|
||||
return math.ceil((untilMs - nowMs) / 60000)
|
||||
end
|
||||
|
||||
function ADPool.isReserved(id)
|
||||
for _, ids in pairs(reservations) do
|
||||
for _, reserved in ipairs(ids) do if reserved == id then return true end end
|
||||
end
|
||||
return false
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-- ---- 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 neededHpCache = {} -- configFileName -> hk eller false (butiksdata ændrer sig ikke i et spil)
|
||||
|
||||
local function neededHpOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local cached = neededHpCache[vehicle.configFileName]
|
||||
if cached ~= nil then return cached or nil end
|
||||
local value = ADPool.readNeededHp(vehicle)
|
||||
neededHpCache[vehicle.configFileName] = value or false
|
||||
return value
|
||||
end
|
||||
|
||||
function ADPool.readNeededHp(vehicle)
|
||||
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
|
||||
|
||||
local tractorHpCache = setmetatable({}, {__mode = "k"}) -- vehicle -> hk eller false
|
||||
|
||||
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. Gemmes pr. traktor.
|
||||
local function tractorHpOf(vehicle)
|
||||
local cached = tractorHpCache[vehicle]
|
||||
if cached ~= nil then return cached or nil end
|
||||
local value = ADPool.readTractorHp(vehicle)
|
||||
tractorHpCache[vehicle] = value or false
|
||||
return value
|
||||
end
|
||||
|
||||
function ADPool.readTractorHp(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
|
||||
|
||||
-- Butikskategori (fx "TRACTORSM", "TRUCKS") eller nil.
|
||||
local function categoryOf(vehicle)
|
||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||
return item ~= nil and item.categoryName or nil
|
||||
end
|
||||
|
||||
-- Varer og samlet kapacitet i redskabets fyldenheder (tømmevogne).
|
||||
function ADPool.cargoOf(vehicle)
|
||||
local carries, capacity = {}, 0
|
||||
local units = vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits or {}
|
||||
for index in ipairs(units) do
|
||||
capacity = capacity + (vehicle:getFillUnitCapacity(index) or 0)
|
||||
for fillType in pairs(vehicle:getFillUnitSupportedFillTypes(index) or {}) do carries[fillType] = true end
|
||||
end
|
||||
return carries, capacity
|
||||
end
|
||||
|
||||
-- Ikke bundet af et kørende flow (traktorer i stoppede flows er ledige).
|
||||
function ADPool.isFreeFromFlows(vehicle)
|
||||
if ADFlowWagons ~= nil then return ADFlowWagons.isFreeTractor(vehicle) end
|
||||
return ADRunsController.getRunForVehicle(vehicle) == nil
|
||||
end
|
||||
|
||||
local function isAttached(vehicle)
|
||||
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||
end
|
||||
|
||||
-- Traktorens egen rolle uden de redskaber der hænger på (mejetærsker/selvkørende har en, en traktor ikke).
|
||||
function ADPool.ownRole(vehicle)
|
||||
local specs = {}
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if vehicle[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
return ADFieldFleet.roleFromSpecs(specs)
|
||||
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 pairs(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
|
||||
local held = role ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(ADRunsController.getVehicleId(vehicle))
|
||||
if role ~= nil and not held then
|
||||
local x, z = positionOf(vehicle)
|
||||
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||
table.insert(implements, {carries = carries, capacity = capacity,
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), 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 ADPool.isFreeFromFlows(vehicle) and not controlled and not isBusy(vehicle)
|
||||
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)
|
||||
and ADPool.canPull(ADPool.ownRole(vehicle)) then
|
||||
local x, z = positionOf(vehicle)
|
||||
-- uden for AD-vejnettet: den kan ikke køre ud efter redskabet (ville sende redskaber i karantæne)
|
||||
local networkDistance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||
hp = tractorHpOf(vehicle), category = categoryOf(vehicle), parkId = park.id,
|
||||
offNetwork = networkDistance ~= nil and networkDistance > ADFieldWork.MAX_OFF_NETWORK})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
|
||||
-- Oversigt til flådefanen: alle redskaber med en markrolle (også dem der mangler parkering/Connect, så
|
||||
-- spilleren kan se hvorfor) og alle traktorer uden egen arbejdsrolle, der ikke hører til et flow.
|
||||
function ADPool.overview(adEnv)
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local nowMs = ADFieldJobs.now or 0
|
||||
local implements, tractors = {}, {}
|
||||
for _, vehicle in pairs(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 then
|
||||
local role = ADPool.roleOf(ADCpBridge.getSpecs(vehicle))
|
||||
if role ~= nil then
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local attacher = isAttached(vehicle) and vehicle:getAttacherVehicle() or nil
|
||||
local key, arg = ADPool.implementStatus({
|
||||
parkName = park ~= nil and park.name or nil,
|
||||
hasConnect = park ~= nil and ADPool.connectFor(park.name, markers) ~= nil,
|
||||
attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil,
|
||||
quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0,
|
||||
reserved = id ~= nil and ADPool.isReserved(id),
|
||||
flowName = id ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(id)
|
||||
and tostring(ADFlowWagons.ownerOf(ADRunsController.store.runs, id).name) or nil,
|
||||
})
|
||||
table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
-- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination
|
||||
if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADPool.isFreeFromFlows(vehicle)
|
||||
and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then
|
||||
local x, z = positionOf(vehicle)
|
||||
local distance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||
local assignment = state.assignments[id]
|
||||
local rear = ADHitch.rearImplementsOf(vehicle)
|
||||
local key, arg = ADPool.tractorStatus({
|
||||
hasPark = parkMarker(adEnv, vehicle) ~= nil,
|
||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled(),
|
||||
returning = ADPool.isReturning(vehicle),
|
||||
fieldId = assignment ~= nil and assignment.fieldId or nil,
|
||||
busy = ADFieldWork.isBusyVehicle(vehicle, id),
|
||||
offNetwork = distance ~= nil and distance > ADFieldWork.MAX_OFF_NETWORK,
|
||||
hitched = #rear > 0,
|
||||
})
|
||||
table.insert(tractors, {id = id, label = ADRuns.vehicleLabel(vehicle), hp = tractorHpOf(vehicle), status = key, arg = arg,
|
||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)})
|
||||
end
|
||||
end
|
||||
return implements, tractors
|
||||
end
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
-- AD Profiler
|
||||
-- Måler Smart Pickups indgange (AutoDrive-hooks, update-løkken, GUI) i spillet: kald, samlet tid og det
|
||||
-- værste enkeltkald pr. minut, plus det værste frame i alt. Tager noget over REPORT_MIN_MS, skrives top-listen
|
||||
-- i log.txt ("ADSmartPickup: profil …"), så et hak kan føres tilbage til præcis hvilken del der gjorde det.
|
||||
-- Indpakningen sker på modul-tabellernes felter; hooksene slår funktionerne op ved hvert kald, så de måles.
|
||||
|
||||
ADProfiler = {}
|
||||
|
||||
ADProfiler.WINDOW_MS = 60000
|
||||
ADProfiler.REPORT_MIN_MS = 2
|
||||
ADProfiler.TOP = 6
|
||||
ADProfiler.stats = {}
|
||||
ADProfiler.frameMs = 0
|
||||
ADProfiler.worstFrameMs = 0
|
||||
ADProfiler.windowStart = nil
|
||||
ADProfiler.depth = 0
|
||||
ADProfiler.installed = false
|
||||
|
||||
function ADProfiler.clock()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function record(name, started, ...)
|
||||
local finished = ADProfiler.clock()
|
||||
ADProfiler.depth = ADProfiler.depth - 1
|
||||
if started ~= nil and finished ~= nil then
|
||||
local spent = finished - started
|
||||
local stat = ADProfiler.stats[name]
|
||||
if stat == nil then
|
||||
stat = {calls = 0, total = 0, max = 0}
|
||||
ADProfiler.stats[name] = stat
|
||||
end
|
||||
stat.calls = stat.calls + 1
|
||||
stat.total = stat.total + spent
|
||||
if spent > stat.max then stat.max = spent end
|
||||
-- kun yderste måling tæller i frame-summen (indlejrede måles også hver for sig)
|
||||
if ADProfiler.depth == 0 then ADProfiler.frameMs = ADProfiler.frameMs + spent end
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
-- Pak tbl[key] ind under navnet name (gør intet hvis funktionen ikke findes).
|
||||
function ADProfiler.wrap(tbl, key, name)
|
||||
if type(tbl) ~= "table" or type(tbl[key]) ~= "function" then return end
|
||||
local original = tbl[key]
|
||||
tbl[key] = function(...)
|
||||
ADProfiler.depth = ADProfiler.depth + 1
|
||||
return record(name, ADProfiler.clock(), original(...))
|
||||
end
|
||||
end
|
||||
|
||||
-- Kaldes én gang pr. frame fra ADSmartPickup:update: afslutter frame-summen og rapporterer pr. minut.
|
||||
function ADProfiler.frame(now)
|
||||
if ADProfiler.frameMs > ADProfiler.worstFrameMs then ADProfiler.worstFrameMs = ADProfiler.frameMs end
|
||||
ADProfiler.frameMs = 0
|
||||
-- (en fejl i en indpakket funktion springer record over; dybden nulstilles derfor hvert frame)
|
||||
ADProfiler.depth = 0
|
||||
ADProfiler.windowStart = ADProfiler.windowStart or now
|
||||
if now - ADProfiler.windowStart < ADProfiler.WINDOW_MS then return end
|
||||
ADProfiler.report()
|
||||
ADProfiler.windowStart = now
|
||||
end
|
||||
|
||||
function ADProfiler.report()
|
||||
local rows = {}
|
||||
for name, stat in pairs(ADProfiler.stats) do
|
||||
table.insert(rows, {name = name, stat = stat})
|
||||
end
|
||||
table.sort(rows, function(a, b) return a.stat.max > b.stat.max end)
|
||||
local worst = rows[1] ~= nil and rows[1].stat.max or 0
|
||||
if worst >= ADProfiler.REPORT_MIN_MS or ADProfiler.worstFrameMs >= ADProfiler.REPORT_MIN_MS then
|
||||
local parts = {}
|
||||
for index = 1, math.min(ADProfiler.TOP, #rows) do
|
||||
local stat = rows[index].stat
|
||||
table.insert(parts, string.format("%s max %.1f ms (%d kald, i alt %.0f ms)", rows[index].name, stat.max, stat.calls, stat.total))
|
||||
end
|
||||
Logging.info("ADSmartPickup: profil sidste minut — værste frame %.1f ms: %s", ADProfiler.worstFrameMs, table.concat(parts, "; "))
|
||||
end
|
||||
ADProfiler.stats = {}
|
||||
ADProfiler.worstFrameMs = 0
|
||||
end
|
||||
|
||||
-- Indpak alle kendte indgange. Kaldes én gang efter at hooksene er installeret.
|
||||
function ADProfiler.install()
|
||||
if ADProfiler.installed then return end
|
||||
ADProfiler.installed = true
|
||||
local targets = {
|
||||
{ADUnloadWait, "beforeUpdate", "aflæsning/Wait (pr. vogn)"},
|
||||
{ADUnloadWait, "adoptOrphans", "genoptag ventende"},
|
||||
{ADUnloadWait, "releaseInactive", "slip standsede"},
|
||||
{ADOutbound, "beforeLoadUpdate", "udkørsel pålæsning (pr. vogn)"},
|
||||
{ADOutbound, "beforeUnloadUpdate", "udkørsel aflæsning (pr. vogn)"},
|
||||
{ADOutbound, "beforeLoadFinished", "udkørsel runde"},
|
||||
{ADOutbound, "choosePickup", "udkørsel turvalg"},
|
||||
{ADOutbound, "holdSupplyVehicle", "forsyning hold"},
|
||||
{ADOutbound, "releaseInactive", "udkørsel slip"},
|
||||
{ADLoadSwap, "tryFromWait", "læs-bytte fra Wait"},
|
||||
{ADSmartPickup, "shouldWaitForLoading", "vent ved pålæsning"},
|
||||
{ADSmartPickup, "choosePickup", "forsyning kildevalg"},
|
||||
{ADSmartPickup, "findSupplyPickup", "forsyning kildesøgning"},
|
||||
{ADRunsController, "applyVehicleNames", "traktornavne"},
|
||||
{ADFieldJobs, "tick", "markarbejde tick"},
|
||||
{ADFields, "scanStep", "markscanning"},
|
||||
{ADFieldWork, "follow", "markarbejde sæt"},
|
||||
{ADFieldWork, "dispatch", "markarbejde udsendelse"},
|
||||
{ADFieldWork, "getRigs", "markflåde"},
|
||||
{ADBuildings, "list", "bygningsliste"},
|
||||
{ADSources, "getFarmInventory", "gårdens lagre"},
|
||||
{ADWaitPool, "hasRouteTo", "rutetjek"},
|
||||
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
|
||||
}
|
||||
for _, target in ipairs(targets) do
|
||||
ADProfiler.wrap(target[1], target[2], target[3])
|
||||
end
|
||||
if SmartPickupFrame ~= nil then
|
||||
ADProfiler.wrap(SmartPickupFrame, "refreshLive", "menu opdatering")
|
||||
ADProfiler.wrap(SmartPickupFrame, "rebuild", "menu genopbygning")
|
||||
end
|
||||
end
|
||||
@@ -17,6 +17,9 @@ 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
|
||||
-- "Sælg altid over X %": kildens lager over grænsen sælges uanset pris (0 = fra). Så hober f.eks. gylle
|
||||
-- ved BGA'en sig ikke op, når prisen er lav.
|
||||
ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT = 0, 0, 100
|
||||
|
||||
local function copyList(list)
|
||||
local copy = {}
|
||||
@@ -39,6 +42,37 @@ local function clampRange(value, default, min, max)
|
||||
return math.max(min, math.min(max, math.floor(number)))
|
||||
end
|
||||
|
||||
-- Flow-vogne: {[traktorId] = vognId} (maskinpulje — flowet henter sin egen vogn).
|
||||
-- Start alle pr. flow: isRunning (traktorer kører AD), stopped (pause fra Stop alt / Stop).
|
||||
-- -> "queue" (start traktorerne), "resume" (kører allerede: kun pausen ophæves), "skip".
|
||||
-- In-game 29/9: flows på pause hvis traktorer kørte videre på en gemt AD-tur blev talt som 'kørte allerede',
|
||||
-- forblev stoppede, og traktoren kørte vognen hjem lige efter at have hentet den.
|
||||
function ADRuns.startAllAction(isRunning, stopped)
|
||||
if not isRunning then return "queue" end
|
||||
return stopped and "resume" or "skip"
|
||||
end
|
||||
|
||||
function ADRuns.copyWagons(wagons)
|
||||
local copy = {}
|
||||
for tractorId, wagonId in pairs(wagons or {}) do copy[tractorId] = wagonId end
|
||||
return copy
|
||||
end
|
||||
|
||||
function ADRuns.serializeWagons(wagons)
|
||||
local keys = {}
|
||||
for tractorId in pairs(wagons or {}) do table.insert(keys, tractorId) end
|
||||
table.sort(keys)
|
||||
local parts = {}
|
||||
for _, tractorId in ipairs(keys) do table.insert(parts, tractorId .. "=" .. wagons[tractorId]) end
|
||||
return table.concat(parts, ";")
|
||||
end
|
||||
|
||||
function ADRuns.parseWagons(text)
|
||||
local wagons = {}
|
||||
for tractorId, wagonId in string.gmatch(tostring(text or ""), "([^=;]+)=([^=;]+)") do wagons[tractorId] = wagonId end
|
||||
return wagons
|
||||
end
|
||||
|
||||
local function copyRun(run)
|
||||
return {
|
||||
id = run.id,
|
||||
@@ -51,6 +85,7 @@ local function copyRun(run)
|
||||
products = copyProducts(run.products),
|
||||
sellAtPercent = run.sellAtPercent,
|
||||
storeAbovePercent = run.storeAbovePercent,
|
||||
sellAbovePercent = run.sellAbovePercent,
|
||||
collectRound = run.collectRound,
|
||||
targetWayPointId = run.targetWayPointId,
|
||||
targetWayPointIds = copyList(run.targetWayPointIds),
|
||||
@@ -60,6 +95,8 @@ local function copyRun(run)
|
||||
loops = run.loops,
|
||||
returnBelowPercent = run.returnBelowPercent,
|
||||
vehicleIds = copyList(run.vehicleIds),
|
||||
stopped = run.stopped == true,
|
||||
wagons = ADRuns.copyWagons(run.wagons),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -98,6 +135,7 @@ function ADRuns.createRun(store, name, kind)
|
||||
products = {},
|
||||
sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT,
|
||||
storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT,
|
||||
sellAbovePercent = ADRuns.DEFAULT_SELL_ABOVE_PERCENT,
|
||||
collectRound = false,
|
||||
targetWayPointId = nil,
|
||||
targetWayPointIds = {},
|
||||
@@ -164,6 +202,7 @@ function ADRuns.updateRun(store, runId, changes)
|
||||
if changes.products ~= nil then run.products = copyProducts(changes.products) end
|
||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
||||
if changes.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
|
||||
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
||||
return new
|
||||
end
|
||||
@@ -308,10 +347,43 @@ function ADRuns.unassignVehicle(store, vehicleId)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- traktoren forlader flowet: dens huskede vogn hører ikke længere til flowet
|
||||
if run.wagons ~= nil then run.wagons[vehicleId] = nil end
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
-- Traktorer der ikke findes længere (solgt/byttet) fjernes fra deres flow. exists(vehicleId) -> bool.
|
||||
-- Mangler ALLE flowenes traktorer, er indlæsningen nok ikke færdig/gået galt: intet fjernes.
|
||||
-- -> ny store, {{runName, vehicleId}} over de fjernede
|
||||
function ADRuns.pruneMissingVehicles(store, exists)
|
||||
local missing, total = {}, 0
|
||||
for _, run in ipairs(store.runs) do
|
||||
for _, id in ipairs(run.vehicleIds) do
|
||||
total = total + 1
|
||||
if not exists(id) then table.insert(missing, {runName = run.name, vehicleId = id}) end
|
||||
end
|
||||
end
|
||||
if #missing == 0 or #missing == total then return store, {} end
|
||||
local new = store
|
||||
for _, entry in ipairs(missing) do new = ADRuns.unassignVehicle(new, entry.vehicleId) end
|
||||
return new, missing
|
||||
end
|
||||
|
||||
-- Én besked pr. berørt flow efter pruneMissingVehicles: {runName, removed, left, critical} (critical = flowet
|
||||
-- står uden traktorer og virker ikke). I samme rækkefølge som flowene.
|
||||
function ADRuns.pruneNotices(storeAfter, removed)
|
||||
local byName = {}
|
||||
for _, entry in ipairs(removed or {}) do byName[entry.runName] = (byName[entry.runName] or 0) + 1 end
|
||||
local notices = {}
|
||||
for _, run in ipairs(storeAfter.runs) do
|
||||
if byName[run.name] ~= nil then
|
||||
table.insert(notices, {runName = run.name, removed = byName[run.name], left = #run.vehicleIds, critical = #run.vehicleIds == 0})
|
||||
end
|
||||
end
|
||||
return notices
|
||||
end
|
||||
|
||||
-- En traktor er kun i ét run. Returnerer ny store + id på det run den blev flyttet FRA (nil = ingen).
|
||||
function ADRuns.assignVehicle(store, runId, vehicleId)
|
||||
local previous = ADRuns.findRunByVehicle(store, vehicleId)
|
||||
@@ -364,7 +436,10 @@ function ADRuns.toRecords(store)
|
||||
products = ADRuns.serializeProducts(run.products),
|
||||
sellAt = run.sellAtPercent,
|
||||
storeAbove = run.storeAbovePercent,
|
||||
sellAbove = run.sellAbovePercent or 0,
|
||||
collectRound = run.collectRound and 1 or 0,
|
||||
stopped = run.stopped and 1 or 0,
|
||||
wagons = ADRuns.serializeWagons(run.wagons),
|
||||
}
|
||||
end
|
||||
return records
|
||||
@@ -407,6 +482,7 @@ function ADRuns.fromRecords(records)
|
||||
products = ADRuns.parseProducts(record.products),
|
||||
sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT),
|
||||
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT),
|
||||
collectRound = tonumber(record.collectRound) == 1,
|
||||
targetWayPointId = targets[1],
|
||||
targetWayPointIds = targets,
|
||||
@@ -416,6 +492,8 @@ function ADRuns.fromRecords(records)
|
||||
loops = clampLoops(record.loops),
|
||||
returnBelowPercent = clampPercent(record.returnBelow),
|
||||
vehicleIds = copyList(record.vehicleIds),
|
||||
stopped = tonumber(record.stopped) == 1,
|
||||
wagons = ADRuns.parseWagons(record.wagons),
|
||||
})
|
||||
store.nextId = math.max(store.nextId, id + 1)
|
||||
end
|
||||
|
||||
@@ -86,10 +86,11 @@ end
|
||||
|
||||
function ADRunsController.deleteRun(runId, adEnv)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if run ~= nil and adEnv ~= nil then
|
||||
if run ~= nil and adEnv ~= nil and not run.stopped then
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet")
|
||||
end
|
||||
if ADFlowWagons ~= nil then pcall(ADFlowWagons.sendHome, adEnv, run) end
|
||||
end
|
||||
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
|
||||
end
|
||||
@@ -256,6 +257,11 @@ function ADRunsController.applyVehicleNamesThrottled(adEnv, dt)
|
||||
if nameTimerMs < ADRunsController.NAME_INTERVAL_MS then return end
|
||||
nameTimerMs = 0
|
||||
ADRunsController.applyVehicleNames(adEnv)
|
||||
-- markmaskiner uden eget navn (stadig modelnavnet) får et rollenavn
|
||||
if ADFieldWork ~= nil and ADFieldWork.autoNameFleet ~= nil and ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||
local ok, err = pcall(ADFieldWork.autoNameFleet, adEnv)
|
||||
if not ok then Logging.warning(ADRunsController.LOG_PREFIX .. "navne til markflåden fejlede: %s", tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- varer ----
|
||||
@@ -372,24 +378,49 @@ function ADRunsController.chooseTarget(adEnv, run, vehicle)
|
||||
end
|
||||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local bestId, bestNeed = nil, nil
|
||||
local bestId, bestNeed, bestInbound = nil, nil, 0
|
||||
for _, entry in ipairs(getTargetStations(adEnv, run, farmId)) do
|
||||
-- læs andre traktorer er på vej med til stalden, tæller med — så fordeler de sig på staldene
|
||||
local pending = ADSmartPickup.getPending ~= nil and ADSmartPickup.getPending(entry.wayPointId, vehicle) or nil
|
||||
local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId, pending)
|
||||
local need = ranked[1]
|
||||
if need ~= nil and need.freeSpace >= (ADSupplyPlanner.MIN_FREE_LITERS or 1000) then
|
||||
-- færrest andre vogne på vej først: et drop (drive-on-bunker, trug) tager én ad gangen, så tre
|
||||
-- vogne mod det mindst fyldte mål holder i kø mens de andre mål står tomme (in-game 26/9)
|
||||
local inbound = ADSmartPickup.countEnRoute ~= nil and ADSmartPickup.countEnRoute(entry.wayPointId, vehicle) or 0
|
||||
local isBetter = bestNeed == nil
|
||||
or (need.fits == true and bestNeed.fits ~= true)
|
||||
or (need.fits == bestNeed.fits and need.score > bestNeed.score)
|
||||
or inbound < bestInbound
|
||||
or (inbound == bestInbound and need.fits == true and bestNeed.fits ~= true)
|
||||
or (inbound == bestInbound and need.fits == bestNeed.fits and need.score > bestNeed.score)
|
||||
if isBetter then
|
||||
bestId, bestNeed = entry.wayPointId, need
|
||||
bestId, bestNeed, bestInbound = entry.wayPointId, need, inbound
|
||||
end
|
||||
end
|
||||
end
|
||||
return bestId or targets[1]
|
||||
end
|
||||
|
||||
-- AutoDrive genstarter aktive traktorer efter indlæsning uden om run-starten: fyldte run-traktorer
|
||||
-- registreres "på vej" til deres nuværende mål (ellers ser mål-valget 0 vogne på vej overalt).
|
||||
ADRunsController.ADOPT_INTERVAL_MS = 5000
|
||||
local adoptTimerMs = 0
|
||||
function ADRunsController.adoptLoadedThrottled(adEnv, dt)
|
||||
adoptTimerMs = adoptTimerMs + (dt or 0)
|
||||
if adEnv == nil or adoptTimerMs < ADRunsController.ADOPT_INTERVAL_MS then return end
|
||||
adoptTimerMs = 0
|
||||
for _, vehicle in pairs(getFarmVehiclesById(adEnv)) do
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
if run ~= nil and (run.kind or "supply") == "supply" and stateModule ~= nil and stateModule:isActive()
|
||||
and ADSmartPickup.getEnRoute(vehicle) == nil then
|
||||
local second = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
|
||||
if second ~= nil and ADSmartPickup.registerCargo(adEnv, vehicle, second.id, ADRunsController.getRunIngredients(adEnv, run, vehicle)) then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "'%s' har læs på vej til %s (overtaget efter start/indlæsning)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(second.name or second.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fire traktorer kan hedde det samme; løbenummeret er pladsen i farmens AD-køretøjsliste.
|
||||
function ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local label, model = tostring(ADRuns.vehicleLabel(vehicle)), tostring(vehicle:getName())
|
||||
@@ -812,7 +843,9 @@ function ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
return result
|
||||
end
|
||||
if not intersects(ADRunsController.getRunIngredients(adEnv, run, vehicle), carriable) then
|
||||
result.reason = "cannotCarry"
|
||||
-- AutoLoad slået til: AutoDrive regner vognen som ren palle-/ballevogn (in-game 27/9: tipvogn med UAL
|
||||
-- aktiv blev afvist i Goat Food som "kan ikke bære") — sig det, så spilleren kan slå det fra
|
||||
result.reason = result.isAutoload and "autoloadOn" or "cannotCarry"
|
||||
return result
|
||||
end
|
||||
if isFarFromNetwork(adEnv, vehicle) then
|
||||
@@ -886,6 +919,7 @@ local function setSetting(adEnv, vehicle, settingName, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
ADRunsController.setSetting = setSetting
|
||||
|
||||
-- Nærmeste markør med en læssestation: kun en gyldig start — moden vælger den reelle kilde pr. tur.
|
||||
local function findNearestLoadMarker(adEnv, vehicle, farmId)
|
||||
@@ -969,6 +1003,10 @@ local function applyRunToVehicle(adEnv, run, vehicle, ingredients)
|
||||
local targetId = ADRunsController.chooseTarget(adEnv, run, vehicle) or run.targetWayPointId
|
||||
local dump = ADRunsController.findWrongCargoDump(adEnv, run, vehicle, targetId)
|
||||
stateModule:setSecondMarkerByWayPointId(targetId)
|
||||
-- allerede fyldt: registrér læsset på vej, så næste traktor i samme start vælger et andet drop
|
||||
if dump == nil and ADSmartPickup.registerCargo ~= nil then
|
||||
ADSmartPickup.registerCargo(adEnv, vehicle, targetId, ingredients)
|
||||
end
|
||||
if dump ~= nil then
|
||||
if ADUnloadWait ~= nil and ADUnloadWait.setDumpTarget ~= nil then
|
||||
ADUnloadWait.setDumpTarget(vehicle, dump.marker.id)
|
||||
@@ -1035,43 +1073,176 @@ end
|
||||
|
||||
-- Pr. tildelt traktor: validér, skriv AD-tilstand, start. Afviste traktorer springes over
|
||||
-- og logges med årsag (samme nøgle som fanen viser).
|
||||
-- Start én traktor i et run: valider, anvend run'et og start AutoDrive. Returnerer resultat-posten.
|
||||
function ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
-- solo-traktor med en husket vogn: hent den først (valideringen ville afvise en traktor uden vogn)
|
||||
if ADFlowWagons ~= nil then
|
||||
local okWagon, handled, wagonResult = pcall(ADFlowWagons.beforeStart, adEnv, run, vehicle)
|
||||
if okWagon and handled then return wagonResult end
|
||||
if not okWagon then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogn: %s", tostring(handled)) end
|
||||
end
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
|
||||
if not validation.ok then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
|
||||
return result
|
||||
end
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
|
||||
if ok then
|
||||
ADRunsController.runningRuns[run.id] = true
|
||||
local second = vehicle.ad.stateModule.getSecondMarker ~= nil and vehicle.ad.stateModule:getSecondMarker() or nil
|
||||
local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1]))
|
||||
or ("markør " .. tostring(second ~= nil and second.id or run.targetWayPointId))
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), target)
|
||||
else
|
||||
result.ok, result.reason = false, "error"
|
||||
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADRunsController.startRun(adEnv, runId)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local results = {}
|
||||
if run == nil then
|
||||
return results
|
||||
end
|
||||
run.stopped = false
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
local started = 0
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
if vehicle ~= nil then
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
|
||||
if not validation.ok then
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
|
||||
end
|
||||
if validation.ok then
|
||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
||||
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
|
||||
if ok then
|
||||
started = started + 1
|
||||
local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1])) or ("markør " .. tostring(run.targetWayPointId))
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), target)
|
||||
else
|
||||
result.ok, result.reason = false, "error"
|
||||
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
||||
end
|
||||
end
|
||||
table.insert(results, result)
|
||||
table.insert(results, ADRunsController.startVehicle(adEnv, run, vehicle))
|
||||
end
|
||||
end
|
||||
if started > 0 then
|
||||
ADRunsController.runningRuns[runId] = true
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
-- ---- Start alle i kø ----
|
||||
-- Én traktor ad gangen med STAGGER_MS mellemrum: ellers kører de sammen fra parkeringspladsen (in-game 27/9).
|
||||
ADRunsController.STAGGER_MS = 10000
|
||||
ADRunsController.startQueue = {}
|
||||
local staggerTimerMs = 0
|
||||
|
||||
function ADRunsController.clearStartQueue()
|
||||
ADRunsController.startQueue = {}
|
||||
end
|
||||
|
||||
-- Lægger alle traktorer i flows der ikke kører i kø; ugyldige (fx uden vogn) springes over med det samme.
|
||||
-- Returnerer {queued, alreadyRunning, skipped = {resultater}}.
|
||||
function ADRunsController.queueStartAll(adEnv)
|
||||
local summary = {queued = 0, alreadyRunning = 0, skipped = {}}
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
local queue = {}
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if #run.vehicleIds > 0 then
|
||||
local action = ADRuns.startAllAction(ADRunsController.getRunStatus(adEnv, run).isRunning, run.stopped == true)
|
||||
if action ~= "queue" then
|
||||
summary.alreadyRunning = summary.alreadyRunning + 1
|
||||
if action == "resume" then run.stopped = false end
|
||||
else
|
||||
run.stopped = false
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
if vehicle ~= nil then
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
-- solo-traktor med husket vogn: henter den ved start (valideringen ser den uden vogn)
|
||||
if validation.ok or (ADFlowWagons ~= nil and (validation.reason == "noTrailer" or ADFlowWagons.hasWagonFor(run, vehicleId))) then
|
||||
table.insert(queue, {runId = run.id, vehicleId = vehicleId})
|
||||
else
|
||||
table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ADRunsController.startQueue = queue
|
||||
ADRunsController.startQueueTotal = #queue
|
||||
staggerTimerMs = ADRunsController.STAGGER_MS -- første startes ved næste tick
|
||||
summary.queued = #queue
|
||||
return summary
|
||||
end
|
||||
|
||||
-- Stop alt og parkér (Oversigt): flows, markarbejde og marksilo-lån stoppes, køretøjerne sendes til deres
|
||||
-- AD-parkeringspunkt, og moden sættes på pause (gemmes), så intet starter igen før "Start alle".
|
||||
-- Høstere med manuel transport parkeres ikke (må ikke køre på vejen med skærebord). Returnerer
|
||||
-- {stopped, parked, noPark = {navne}, manual = {navne}}.
|
||||
function ADRunsController.stopAllAndPark(adEnv)
|
||||
local summary = {stopped = 0, parked = 0, noPark = {}, manual = {}}
|
||||
ADRunsController.clearStartQueue()
|
||||
local toPark, seen = {}, {}
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
if vehicle ~= nil and not seen[vehicle] then
|
||||
seen[vehicle] = true
|
||||
if isVehicleActive(vehicle) then
|
||||
stopVehicle(vehicle)
|
||||
summary.stopped = summary.stopped + 1
|
||||
end
|
||||
table.insert(toPark, vehicle)
|
||||
end
|
||||
end
|
||||
ADRunsController.runningRuns[run.id] = nil
|
||||
run.stopped = true
|
||||
end
|
||||
if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then
|
||||
for _, stopped in ipairs(ADFieldWork.stopAllAssignments(adEnv) or {}) do
|
||||
summary.stopped = summary.stopped + 1
|
||||
if stopped.pooled then
|
||||
summary.parked = summary.parked + 1
|
||||
elseif stopped.manual then
|
||||
table.insert(summary.manual, tostring(ADRuns.vehicleLabel(stopped.vehicle)))
|
||||
elseif not seen[stopped.vehicle] then
|
||||
seen[stopped.vehicle] = true
|
||||
table.insert(toPark, stopped.vehicle)
|
||||
end
|
||||
end
|
||||
end
|
||||
if ADFieldSilos ~= nil and ADFieldSilos.stopAll ~= nil then pcall(ADFieldSilos.stopAll, adEnv) end
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end
|
||||
for _, vehicle in ipairs(toPark) do
|
||||
if ADHitch ~= nil and ADHitch.stow ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil then ADHitch.stow(vehicle, "stop alt") end
|
||||
-- flow-traktor: tøm vognen, sæt den af på sin plads og kør hjem (ADFlowWagons klarer hele turen)
|
||||
local okHome, pooled = false, false
|
||||
if ADFlowWagons ~= nil and ADRunsController.getRunForVehicle(vehicle) ~= nil then
|
||||
okHome = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle)
|
||||
pooled = okHome
|
||||
elseif ADHitch ~= nil then
|
||||
okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
|
||||
end
|
||||
local park = not (okHome and pooled) and ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
|
||||
if okHome and pooled then
|
||||
summary.parked = summary.parked + 1
|
||||
elseif park ~= nil then
|
||||
local ok = pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil)
|
||||
if ok then summary.parked = summary.parked + 1 end
|
||||
else
|
||||
table.insert(summary.noPark, tostring(ADRuns.vehicleLabel(vehicle)))
|
||||
end
|
||||
end
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "Stop alt og parkér: %d stoppet, %d til parkering, %d uden parkeringspunkt, %d høstere hentes manuelt — pause",
|
||||
summary.stopped, summary.parked, #summary.noPark, #summary.manual)
|
||||
return summary
|
||||
end
|
||||
|
||||
-- Starter næste traktor i køen, når der er gået STAGGER_MS siden forrige.
|
||||
function ADRunsController.processStartQueue(adEnv, dt)
|
||||
if #ADRunsController.startQueue == 0 then return end
|
||||
staggerTimerMs = staggerTimerMs + (dt or 0)
|
||||
if staggerTimerMs < ADRunsController.STAGGER_MS then return end
|
||||
staggerTimerMs = 0
|
||||
local item = table.remove(ADRunsController.startQueue, 1)
|
||||
local run = ADRuns.getRun(ADRunsController.store, item.runId)
|
||||
local vehicle = run ~= nil and getFarmVehiclesById(adEnv)[item.vehicleId] or nil
|
||||
if run ~= nil and vehicle ~= nil and not isVehicleActive(vehicle) then
|
||||
ADRunsController.startVehicle(adEnv, run, vehicle)
|
||||
end
|
||||
end
|
||||
|
||||
-- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job —
|
||||
-- den stoppes, så den ikke fortsætter uden run. Returnerer true hvis den blev stoppet.
|
||||
function ADRunsController.stopIfRunning(vehicle, reason)
|
||||
@@ -1089,6 +1260,11 @@ function ADRunsController.stopRun(adEnv, runId)
|
||||
if run == nil then
|
||||
return stopped
|
||||
end
|
||||
-- allerede stoppet: traktorerne kan være lånt ud af maskinpuljen — de må ikke trækkes væk midt i et job
|
||||
if run.stopped then
|
||||
ADRunsController.runningRuns[runId] = nil
|
||||
return stopped
|
||||
end
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
local vehicle = vehiclesById[vehicleId]
|
||||
@@ -1099,6 +1275,12 @@ function ADRunsController.stopRun(adEnv, runId)
|
||||
end
|
||||
end
|
||||
ADRunsController.runningRuns[runId] = nil
|
||||
-- stoppet flow: vognene på plads og traktorerne hjem (ledige i maskinpuljen, vognene er stadig flowets)
|
||||
run.stopped = true
|
||||
if ADFlowWagons ~= nil then
|
||||
local okHome, err = pcall(ADFlowWagons.sendHome, adEnv, run)
|
||||
if not okHome then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogne hjem: %s", tostring(err)) end
|
||||
end
|
||||
return stopped
|
||||
end
|
||||
|
||||
@@ -1113,11 +1295,15 @@ function ADRunsController.getRunStatus(adEnv, run)
|
||||
running = running + 1
|
||||
end
|
||||
end
|
||||
local isRunning = ADRunsController.runningRuns[run.id] == true and running > 0
|
||||
-- startet fra fanen i denne session ELLER ikke stoppet af spilleren (efter genindlæsning er runningRuns tom,
|
||||
-- men traktorerne kører videre — in-game 29/9: listen viste [Start] ved kørende flows)
|
||||
local isRunning = (ADRunsController.runningRuns[run.id] == true or run.stopped == false) and running > 0
|
||||
if ADRunsController.runningRuns[run.id] and running == 0 then
|
||||
ADRunsController.runningRuns[run.id] = nil
|
||||
end
|
||||
return {running = running, total = #run.vehicleIds, isRunning = isRunning}
|
||||
-- canStop: Stop-knappen — også når et ikke-stoppet flow holder stille med vogne på / henter / venter på sin vogn
|
||||
local canStop = isRunning or (not run.stopped and ADFlowWagons ~= nil and ADFlowWagons.hasActivity(run, vehiclesById))
|
||||
return {running = running, total = #run.vehicleIds, isRunning = isRunning, canStop = canStop}
|
||||
end
|
||||
|
||||
-- Start alle (Oversigt): flows der allerede kører røres ikke, så et halvt kørende gårdbillede kan
|
||||
@@ -1126,8 +1312,10 @@ function ADRunsController.startAllRuns(adEnv)
|
||||
local summary = {runsStarted = 0, alreadyRunning = 0, vehiclesStarted = 0, results = {}}
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if #run.vehicleIds > 0 then
|
||||
if ADRunsController.getRunStatus(adEnv, run).isRunning then
|
||||
local action = ADRuns.startAllAction(ADRunsController.getRunStatus(adEnv, run).isRunning, run.stopped == true)
|
||||
if action ~= "queue" then
|
||||
summary.alreadyRunning = summary.alreadyRunning + 1
|
||||
if action == "resume" then run.stopped = false end
|
||||
else
|
||||
local startedHere = 0
|
||||
for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do
|
||||
@@ -1287,6 +1475,12 @@ function ADRunsController.save()
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||
pcall(ADFieldJobs.save)
|
||||
end
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver
|
||||
-- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem)
|
||||
if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
|
||||
-- maskiner hvor CP kører nu, så de kan genstartes efter indlæsning (samme gem-regel som navnefilen)
|
||||
if ADCpResume ~= nil and ADFieldSilos ~= nil and ADFieldSilos.adEnv ~= nil then pcall(ADCpResume.save, ADFieldSilos.adEnv) end
|
||||
return saved
|
||||
end
|
||||
|
||||
@@ -1298,7 +1492,10 @@ function ADRunsController.load(adEnv)
|
||||
return
|
||||
end
|
||||
ADRunsController.store = store
|
||||
if ADNames ~= nil then pcall(ADNames.load) end
|
||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
||||
if ADCpResume ~= nil then pcall(ADCpResume.load) end
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, run in ipairs(store.runs) do
|
||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||
@@ -1309,6 +1506,27 @@ function ADRunsController.load(adEnv)
|
||||
end
|
||||
end
|
||||
|
||||
-- Én gang efter indlæsning (køretøjerne er klar): traktorer der ikke findes længere fjernes fra flowene.
|
||||
function ADRunsController.pruneMissingVehicles(adEnv)
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
if next(vehiclesById) == nil then return end
|
||||
local store, removed = ADRuns.pruneMissingVehicles(ADRunsController.store, function(id) return vehiclesById[id] ~= nil end)
|
||||
if #removed == 0 then return end
|
||||
ADRunsController.store = store
|
||||
for _, entry in ipairs(removed) do
|
||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': køretøj %s findes ikke længere — fjernet fra flowet", tostring(entry.runName), tostring(entry.vehicleId))
|
||||
end
|
||||
ADRunsController.save()
|
||||
-- besked på skærmen (højre side); rød når flowet står uden traktorer
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil and g_i18n ~= nil then
|
||||
for _, notice in ipairs(ADRuns.pruneNotices(store, removed)) do
|
||||
local text = notice.critical and string.format(g_i18n:getText("spu_prune_empty"), notice.runName)
|
||||
or string.format(g_i18n:getText("spu_prune_removed"), notice.runName, notice.removed, notice.left)
|
||||
ADFieldJobs.notify(text, notice.critical)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADRunsController.installSaveHook()
|
||||
if ItemSystem == nil or ItemSystem.save == nil or Utils == nil then
|
||||
return
|
||||
|
||||
@@ -46,7 +46,11 @@ function ADRunsStorage.writeRecords(xmlFile, records)
|
||||
xmlFile:setString(key .. "#products", record.products or "")
|
||||
xmlFile:setInt(key .. "#sellAt", record.sellAt or ADRuns.DEFAULT_SELL_AT_PERCENT)
|
||||
xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT)
|
||||
xmlFile:setInt(key .. "#sellAbove", record.sellAbove or 0)
|
||||
xmlFile:setInt(key .. "#collectRound", record.collectRound or 0)
|
||||
-- maskinpulje: flowet stoppet + husket vogn pr. traktor
|
||||
xmlFile:setInt(key .. "#stopped", record.stopped or 0)
|
||||
xmlFile:setString(key .. "#wagons", record.wagons or "")
|
||||
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||
end
|
||||
@@ -74,7 +78,10 @@ function ADRunsStorage.readRecords(xmlFile)
|
||||
products = xmlFile:getString(key .. "#products"),
|
||||
sellAt = xmlFile:getInt(key .. "#sellAt"),
|
||||
storeAbove = xmlFile:getInt(key .. "#storeAbove"),
|
||||
sellAbove = xmlFile:getInt(key .. "#sellAbove"),
|
||||
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
||||
stopped = xmlFile:getInt(key .. "#stopped"),
|
||||
wagons = xmlFile:getString(key .. "#wagons"),
|
||||
vehicleIds = {},
|
||||
}
|
||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||
|
||||
@@ -10,14 +10,29 @@
|
||||
|
||||
ADSmartPickup = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADSmartPickup.dir = g_currentModDirectory
|
||||
if ADSmartPickup.dir ~= nil and source ~= nil then
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupStorageView.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupAttentionFlow.lua")
|
||||
end
|
||||
|
||||
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
|
||||
@@ -216,14 +231,21 @@ local function getUnloadStationAtMarker(adEnv, wayPoint, farmId)
|
||||
end
|
||||
|
||||
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
|
||||
-- Markør -> station er stabil (stationer og markører flytter sig sjældent): caches 10 s (ADCache).
|
||||
ADSmartPickup.STATION_CACHE_MS = 30000
|
||||
|
||||
function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
end)
|
||||
end
|
||||
|
||||
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||
end)
|
||||
end
|
||||
|
||||
-- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen".
|
||||
@@ -243,7 +265,7 @@ function ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, m
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
return best, best ~= nil and bestDistance or nil
|
||||
end
|
||||
|
||||
-- Siloens EGET indhold. Udvidelses-siloer (isExtension) deler lager med naboer
|
||||
@@ -352,18 +374,26 @@ end
|
||||
-- Bedste kilde der har varen: mindst fyldte først for lagervarer, fyldeste først for
|
||||
-- varer hvis tanke løber fulde (ADSupplyPlanner). Siloer med varen i EGET lager går
|
||||
-- altid forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil.
|
||||
local function findBestSource(markerStations, fillTypes, farmId)
|
||||
local getLoadCapacity -- defineret længere nede (læs-kapacitet til kildevalget)
|
||||
|
||||
local function findBestSource(markerStations, fillTypes, farmId, loadCapacity)
|
||||
local prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes))
|
||||
local ample = ADSupplyPlanner.ampleLevel(loadCapacity)
|
||||
local best = {[true] = {}, [false] = {}}
|
||||
for _, entry in ipairs(markerStations) do
|
||||
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
|
||||
if level >= ADSmartPickup.MIN_USEFUL_LITERS and not isProtectedProductionInput(entry.station, fillTypes)
|
||||
and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest) then
|
||||
and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest, ample) then
|
||||
best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn}
|
||||
end
|
||||
end
|
||||
|
||||
local choice = best[true].marker ~= nil and best[true] or best[false]
|
||||
-- eget lager går forud — men ikke en næsten tom egen tank frem for et rigeligt nabolager
|
||||
local own, other = best[true], best[false]
|
||||
local choice = own
|
||||
if own.marker == nil or (not prefersFullest and other.marker ~= nil and own.level < ample and other.level >= ample) then
|
||||
choice = other
|
||||
end
|
||||
if choice.marker == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -392,7 +422,7 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
|
||||
return nil
|
||||
end
|
||||
local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)
|
||||
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId)
|
||||
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId, getLoadCapacity(adEnv, driver))
|
||||
end
|
||||
|
||||
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
|
||||
@@ -452,7 +482,7 @@ end
|
||||
local factoryFallbackLogged = setmetatable({}, {__mode = "k"})
|
||||
local function logFactoryFallbackOnce(driver, station)
|
||||
local placeable = station.owningPlaceable
|
||||
local name = placeable ~= nil and placeable.getName ~= nil and placeable:getName() or "fabrik"
|
||||
local name = (ADNames ~= nil and ADNames.forPlaceable(placeable)) or (placeable ~= nil and placeable.getName ~= nil and placeable:getName()) or "fabrik"
|
||||
local message = string.format("'%s': %s har ingen ønskede produktioner (tændte eller auto-slukkede pga. mangel) med varer vognen kan bære; bruger dit varevalg", tostring(ADRuns.vehicleLabel(driver)), tostring(name))
|
||||
if factoryFallbackLogged[driver] ~= message then
|
||||
factoryFallbackLogged[driver] = message
|
||||
@@ -639,6 +669,20 @@ function ADSmartPickup.setEnRoute(vehicle, wayPointId, fillType, liters)
|
||||
enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters}
|
||||
end
|
||||
|
||||
function ADSmartPickup.getEnRoute(vehicle)
|
||||
return enRoute[vehicle]
|
||||
end
|
||||
|
||||
-- Fyldt run-traktor -> "på vej" til mål targetId, så mål-valget for de næste fordeler dem.
|
||||
function ADSmartPickup.registerCargo(adEnv, vehicle, targetId, ingredients)
|
||||
if targetId == nil or ADUnloadWait == nil or ADUnloadWait.getRestLoad == nil then return false end
|
||||
local restFillType, restLiters = ADUnloadWait.getRestLoad(adEnv, vehicle)
|
||||
local fillType, liters = ADSupplyPlanner.cargoFor(restFillType, restLiters, ingredients)
|
||||
if fillType == nil then return false end
|
||||
ADSmartPickup.setEnRoute(vehicle, targetId, fillType, liters)
|
||||
return true
|
||||
end
|
||||
|
||||
function ADSmartPickup.clearEnRoute(vehicle)
|
||||
enRoute[vehicle] = nil
|
||||
end
|
||||
@@ -658,6 +702,15 @@ function ADSmartPickup.getPending(wayPointId, excludeVehicle)
|
||||
return pending
|
||||
end
|
||||
|
||||
-- Antal ANDRE traktorer på vej til målet (målvalget fordeler vognene på flere drops).
|
||||
function ADSmartPickup.countEnRoute(wayPointId, excludeVehicle)
|
||||
local count = 0
|
||||
for vehicle, entry in pairs(enRoute) do
|
||||
if vehicle ~= excludeVehicle and entry.wayPointId == wayPointId then count = count + 1 end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- pending (valgfri): læs på vej til målet, lægges oven i (fanen og mål-valget mellem stalde).
|
||||
function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
||||
local needs = getTargetNeeds(station, fillTypes, farmId, {})
|
||||
@@ -676,7 +729,7 @@ function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
||||
end
|
||||
|
||||
-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den.
|
||||
local function getLoadCapacity(adEnv, driver)
|
||||
getLoadCapacity = function(adEnv, driver)
|
||||
if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -744,7 +797,7 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetWayPoi
|
||||
-- bedste foder først: lavere foder kun når truget er nødlidende (ADSupplyPlanner.isFoodAllowed)
|
||||
local bestFood = ADSupplyPlanner.bestFoodPriority ~= nil and ADSupplyPlanner.bestFoodPriority(ranked) or nil
|
||||
for _, need in ipairs(ranked) do
|
||||
local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId) or nil
|
||||
local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId, loadCapacity) or nil
|
||||
if choice ~= nil then
|
||||
return {
|
||||
markerIndex = choice.markerIndex,
|
||||
@@ -1095,6 +1148,9 @@ local function installHook(adEnv)
|
||||
if ADOutbound ~= nil and ADOutbound.install ~= nil then
|
||||
ADOutbound.install(adEnv)
|
||||
end
|
||||
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
||||
ADHitchCreep.install(adEnv)
|
||||
end
|
||||
|
||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
||||
end
|
||||
@@ -1146,10 +1202,33 @@ local function insertInGameMenuPage(frame, pageName, iconFile, iconUVs, predicat
|
||||
end
|
||||
inGameMenu[pageName] = frame
|
||||
inGameMenu.pagingElement:addElement(frame)
|
||||
-- Lige over spillets Indstillinger (brugerønske 29/9). Siden flyttes på ALLE tre lister med samme indeks
|
||||
-- (sider, pagerens sider, pageFrames) som FS25_FarmOperationsDashboard — at flytte kun nogle af dem gav
|
||||
-- tidligere forskudt fane↔side. Uden Indstillinger-side: sidst som før.
|
||||
local target = 0
|
||||
for i, child in ipairs(inGameMenu.pagingElement.elements) do
|
||||
if inGameMenu.pageSettings ~= nil and child == inGameMenu.pageSettings then
|
||||
target = i
|
||||
break
|
||||
end
|
||||
end
|
||||
local function moveTo(list, matches)
|
||||
if target == 0 or list == nil then return end
|
||||
for i, child in ipairs(list) do
|
||||
if matches(child) then
|
||||
table.remove(list, i)
|
||||
table.insert(list, target, child)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
moveTo(inGameMenu.pagingElement.elements, function(child) return child == frame end)
|
||||
moveTo(inGameMenu.pagingElement.pages, function(child) return child.element == frame end)
|
||||
inGameMenu.pagingElement:updateAbsolutePosition()
|
||||
inGameMenu.pagingElement:updatePageMapping()
|
||||
inGameMenu:registerPage(frame, position, predicate)
|
||||
inGameMenu:addPageTab(frame, iconFile, iconUVs)
|
||||
moveTo(inGameMenu.pageFrames, function(child) return child == frame end)
|
||||
if frame.updateAbsolutePosition ~= nil then
|
||||
frame:updateAbsolutePosition()
|
||||
end
|
||||
@@ -1184,11 +1263,17 @@ function ADSmartPickup:loadMap()
|
||||
end
|
||||
|
||||
function ADSmartPickup:update(dt)
|
||||
if ADProfiler ~= nil and ADProfiler.installed then
|
||||
local now = ADProfiler.clock()
|
||||
if now ~= nil then pcall(ADProfiler.frame, now) end
|
||||
end
|
||||
if not isHooked then
|
||||
local adEnv = getAutoDriveEnv()
|
||||
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
|
||||
installHook(adEnv)
|
||||
isHooked = true
|
||||
if ADProfiler ~= nil then pcall(ADProfiler.install) end
|
||||
if ADCache ~= nil then ADCache.enabled = true end
|
||||
if ADRunsController ~= nil then
|
||||
pcall(ADRunsController.load, adEnv)
|
||||
pcall(ADRunsController.installSaveHook)
|
||||
@@ -1206,6 +1291,9 @@ function ADSmartPickup:update(dt)
|
||||
if ADOutbound ~= nil and ADOutbound.releaseInactive ~= nil then
|
||||
pcall(ADOutbound.releaseInactive)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.processStartQueue ~= nil then
|
||||
pcall(ADRunsController.processStartQueue, getAutoDriveEnv(), dt)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
|
||||
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
|
||||
end
|
||||
@@ -1215,6 +1303,26 @@ function ADSmartPickup:update(dt)
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
||||
end
|
||||
end
|
||||
if ADFieldSilos ~= nil then
|
||||
local okSilos, errSilos = pcall(ADFieldSilos.tick, getAutoDriveEnv(), dt)
|
||||
if not okSilos then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
||||
end
|
||||
end
|
||||
if ADFlowWagons ~= nil then
|
||||
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
|
||||
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
|
||||
end
|
||||
if ADHitch ~= nil then
|
||||
local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt)
|
||||
if not okHitch then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kobling fejlede: %s", tostring(errHitch))
|
||||
end
|
||||
end
|
||||
if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
|
||||
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
|
||||
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
|
||||
end
|
||||
local ok, err = pcall(restoreStoppedVehicles)
|
||||
if not ok then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
ADSources = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADSources.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
|
||||
ADSources.MAX_DISTANCE = 40
|
||||
@@ -17,10 +24,6 @@ local function getHusbandryPalletSpawners(placeable)
|
||||
return spec ~= nil and spec.fillTypeIndexToPalletSpawner or nil
|
||||
end
|
||||
|
||||
local function firstSpawnerNode(spawner)
|
||||
return spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode or nil
|
||||
end
|
||||
|
||||
|
||||
local function distance2(ax, az, bx, bz)
|
||||
local dx, dz = ax - bx, az - bz
|
||||
@@ -36,6 +39,8 @@ local function getNodePosition(node)
|
||||
end
|
||||
|
||||
local function getPlaceableName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then
|
||||
@@ -123,7 +128,8 @@ local function findPlaceableNear(wayPoint, farmId, predicate)
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
-- (afstand i meter som 2. værdi)
|
||||
return best, best ~= nil and math.sqrt(bestDistance) or nil
|
||||
end
|
||||
|
||||
local function isObjectStorage(placeable)
|
||||
@@ -136,6 +142,12 @@ end
|
||||
|
||||
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
|
||||
function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
return cacheGet("identify", tostring(wayPointId) .. ":" .. tostring(farmId), 10000, function()
|
||||
return ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||||
end)
|
||||
end
|
||||
|
||||
function ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||
if wayPoint == nil then
|
||||
return nil
|
||||
@@ -164,9 +176,15 @@ function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
end
|
||||
-- Det strenge opslag (trigger bag næsen) passer til silorækker; en mælketank eller fabrik ligger
|
||||
-- ofte ved siden af markøren. Løst opslag inden for 40 m — men aldrig for siloer.
|
||||
-- stald med egne pallespawnere (hønsehus: æg) — markøren står ved spawneren, ingen læssestation
|
||||
local palletBarn, palletBarnDistance = findPlaceableNear(wayPoint, farmId, function(placeable) return placeable.getHusbandryFillLevel ~= nil and getHusbandryPalletSpawners(placeable) ~= nil end)
|
||||
if placeable == nil and ADSmartPickup.getLoadStationNearWayPointId ~= nil then
|
||||
local loose = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE)
|
||||
local loose, looseDistance = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE)
|
||||
local loosePlaceable = loose ~= nil and loose.owningPlaceable or nil
|
||||
-- et nærmere hønsehus vinder over en fjernere læssestation (gyllestation 23 m fra "Æg Pickup 1")
|
||||
if palletBarn ~= nil and looseDistance ~= nil and palletBarnDistance ~= nil and palletBarnDistance < looseDistance then
|
||||
loosePlaceable = nil
|
||||
end
|
||||
if loosePlaceable ~= nil and loosePlaceable.spec_silo == nil then
|
||||
if getProductionPoint(loosePlaceable) ~= nil then
|
||||
return {kind = "factory", placeable = loosePlaceable, station = loose, wayPointId = wayPointId, name = getPlaceableName(loosePlaceable, markerName)}
|
||||
@@ -175,8 +193,6 @@ function ADSources.identify(adEnv, wayPointId, farmId)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- stald med egne pallespawnere (hønsehus: æg) — markøren står ved spawneren, ingen læssestation
|
||||
local palletBarn = findPlaceableNear(wayPoint, farmId, function(placeable) return placeable.getHusbandryFillLevel ~= nil and getHusbandryPalletSpawners(placeable) ~= nil end)
|
||||
if palletBarn ~= nil then
|
||||
return {kind = "husbandry", placeable = palletBarn, station = nil, wayPointId = wayPointId, name = getPlaceableName(palletBarn, markerName)}
|
||||
end
|
||||
@@ -241,29 +257,99 @@ local function getPalletContent(vehicle)
|
||||
return nil, 0
|
||||
end
|
||||
|
||||
-- Liter af varen på paller der står ved fabrikkens spawner (pallespawneren tømmer lageret
|
||||
-- ud på jorden løbende, så det er dér varen fysisk er).
|
||||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawnerNode)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil
|
||||
local node = spawnerNode or (spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode) or placeable.rootNode
|
||||
local sx, sz = getNodePosition(node)
|
||||
if sx == nil then
|
||||
return 0
|
||||
-- Spawnerens område som segmenter {ax, az, bx, bz}: hver spawnPlace fra start til slut (startNode/endNode,
|
||||
-- eller PlacementUtil-felterne startX/startZ + dirX/dirZ*width). Et hønsehus har to rækker; måles kun
|
||||
-- til første række, ligger den anden række tættere på naboens start end husets egen.
|
||||
local function spawnerSegments(spawner)
|
||||
local segments = {}
|
||||
for _, place in ipairs(spawner ~= nil and spawner.spawnPlaces or {}) do
|
||||
local ax, az = getNodePosition(place.startNode)
|
||||
local bx, bz = getNodePosition(place.endNode)
|
||||
if ax == nil and place.startX ~= nil and place.startZ ~= nil then
|
||||
ax, az = place.startX, place.startZ
|
||||
if place.dirX ~= nil and place.dirZ ~= nil and place.width ~= nil then
|
||||
bx, bz = ax + place.dirX * place.width, az + place.dirZ * place.width
|
||||
end
|
||||
end
|
||||
if ax ~= nil then table.insert(segments, {ax, az, bx or ax, bz or az}) end
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Kvadreret afstand fra (x, z) til nærmeste punkt på et af segmenterne.
|
||||
local function segmentsDistance2(segments, x, z)
|
||||
local best = math.huge
|
||||
for _, s in ipairs(segments) do
|
||||
local ax, az, bx, bz = s[1], s[2], s[3], s[4]
|
||||
local dx, dz = bx - ax, bz - az
|
||||
local len2 = dx * dx + dz * dz
|
||||
local t = len2 > 0 and math.max(0, math.min(1, ((x - ax) * dx + (z - az) * dz) / len2)) or 0
|
||||
local d = distance2(x, z, ax + dx * t, az + dz * t)
|
||||
if d < best then best = d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Alle pallespawnere der kan lave varen (stalde med spawner for fillType, fabrikkers pallespawner) som
|
||||
-- {placeable, segments}. Står bygningerne tættere end PALLET_RADIUS (hønsehuse på række), tilhører en
|
||||
-- palle den spawner hvis område er nærmest — ellers tæller et tomt hus naboernes paller med.
|
||||
local function findRivalSpawners(fillType)
|
||||
local rivals = {}
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
local husbandrySpawners = getHusbandryPalletSpawners(placeable)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local spawner = husbandrySpawners ~= nil and husbandrySpawners[fillType] or (productionPoint ~= nil and productionPoint.palletSpawner or nil)
|
||||
local segments = spawnerSegments(spawner)
|
||||
if #segments > 0 then table.insert(rivals, {placeable = placeable, segments = segments}) end
|
||||
end
|
||||
return rivals
|
||||
end
|
||||
|
||||
local function isNearestSpawner(rivals, placeable, own, x, z)
|
||||
for _, rival in ipairs(rivals) do
|
||||
if rival.placeable ~= placeable and segmentsDistance2(rival.segments, x, z) < own then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
ADSources.lastPalletNote = {}
|
||||
|
||||
-- Liter af varen på paller ved spawneren (pallespawneren tømmer lageret ud på jorden løbende, så
|
||||
-- det er dér varen fysisk er). spawner = staldens spawner for varen; nil = fabrikkens pallespawner.
|
||||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawner)
|
||||
local productionPoint = getProductionPoint(placeable)
|
||||
local segments = spawnerSegments(spawner or (productionPoint ~= nil and productionPoint.palletSpawner or nil))
|
||||
if #segments == 0 then
|
||||
local rx, rz = getNodePosition(placeable.rootNode)
|
||||
if rx == nil then return 0, 0 end
|
||||
segments = {{rx, rz, rx, rz}}
|
||||
end
|
||||
local rivals = cacheGet("palletSpawners", fillType, 10000, function() return findRivalSpawners(fillType) end)
|
||||
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
|
||||
local total, count = 0, 0
|
||||
local total, count, lost = 0, 0, 0
|
||||
local radius2 = ADSources.PALLET_RADIUS * ADSources.PALLET_RADIUS
|
||||
for _, vehicle in pairs(vehicles) do
|
||||
if isPalletVehicle(vehicle) and (vehicle.getOwnerFarmId == nil or vehicle:getOwnerFarmId() == farmId) then
|
||||
local palletFillType, level = getPalletContent(vehicle)
|
||||
local x, z = getNodePosition(vehicle.rootNode)
|
||||
if palletFillType == fillType and x ~= nil and distance2(sx, sz, x, z) <= radius2 then
|
||||
total = total + level
|
||||
count = count + 1
|
||||
local own = (palletFillType == fillType and x ~= nil) and segmentsDistance2(segments, x, z) or math.huge
|
||||
if own <= radius2 then
|
||||
if isNearestSpawner(rivals, placeable, own, x, z) then
|
||||
total = total + level
|
||||
count = count + 1
|
||||
else
|
||||
lost = lost + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- diagnose: paller inden for rækkevidde, men alle tildelt en nabo (højst én gang pr. minut pr. bygning)
|
||||
local now = g_currentMission ~= nil and g_currentMission.time or nil
|
||||
if count == 0 and lost > 0 and now ~= nil and (ADSources.lastPalletNote[placeable] or -math.huge) + 60000 < now then
|
||||
ADSources.lastPalletNote[placeable] = now
|
||||
Logging.info(ADSources.LOG_PREFIX .. "%s: %d paller inden for %d m tilhører nabo-spawnere (0 egne)", getPlaceableName(placeable, "?"), lost, ADSources.PALLET_RADIUS)
|
||||
end
|
||||
return total, count
|
||||
end
|
||||
|
||||
@@ -395,7 +481,7 @@ local function getHusbandryPalletProducts(source, farmId)
|
||||
local products = {}
|
||||
local spec = source.placeable.spec_husbandryPallets
|
||||
for fillType, spawner in pairs(getHusbandryPalletSpawners(source.placeable) or {}) do
|
||||
local liters, count = getPalletsNearSpawner(source.placeable, fillType, farmId, firstSpawnerNode(spawner))
|
||||
local liters, count = getPalletsNearSpawner(source.placeable, fillType, farmId, spawner)
|
||||
local capacity = spec ~= nil and spec.capacities ~= nil and spec.capacities[fillType] or 0
|
||||
local product = newProduct(fillType, liters, liters, math.max(capacity or 0, liters), true)
|
||||
product.palletLiters, product.palletCount = liters, count
|
||||
@@ -552,6 +638,10 @@ end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||||
function ADSources.getSellMarkers(adEnv, farmId)
|
||||
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
||||
end
|
||||
|
||||
function ADSources.findSellMarkers(adEnv, farmId)
|
||||
local result = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||
@@ -852,16 +942,17 @@ local function addAnimalProduction(rates, placeable)
|
||||
end
|
||||
|
||||
-- {[fillType] = {consumption, production}} i liter pr. døgn for gårdens fabrikker og stalde.
|
||||
function ADSources.getFarmRates(adEnv, farmId)
|
||||
-- feedOnly: kun staldenes forbrug (foder, halm, vand) — marksilo-flowets foderreserve.
|
||||
function ADSources.getFarmRates(adEnv, farmId, feedOnly)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
local rates = {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
if isOwnedBy(placeable, farmId) then
|
||||
if getProductionPoint(placeable) ~= nil then
|
||||
addFactoryRates(rates, placeable)
|
||||
if not feedOnly then addFactoryRates(rates, placeable) end
|
||||
elseif placeable.getHusbandryFillLevel ~= nil then
|
||||
addHusbandryRates(rates, placeable)
|
||||
pcall(addAnimalProduction, rates, placeable)
|
||||
if not feedOnly then pcall(addAnimalProduction, rates, placeable) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -999,6 +1090,10 @@ end
|
||||
-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
|
||||
-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
|
||||
function ADSources.getFarmInventory(adEnv, farmId)
|
||||
return cacheGet("inventory", tostring(farmId), 2000, function() return ADSources.buildFarmInventory(adEnv, farmId) end)
|
||||
end
|
||||
|
||||
function ADSources.buildFarmInventory(adEnv, farmId)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
local result = {}
|
||||
for _, placeable in pairs(placeables) do
|
||||
|
||||
@@ -108,3 +108,215 @@ function ADStorageStats.filterKind(inventory, kind)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- vareoversigt (v1.52): én række pr. vare, der folder sig ud til bygningerne ----
|
||||
|
||||
-- Tærskler for farverne: døgn til tom (gårdens netto), fyldning i % (produceret vare / stald-trug).
|
||||
ADStorageStats.DAYS_RED, ADStorageStats.DAYS_YELLOW = 1, 3
|
||||
ADStorageStats.FULL_RED, ADStorageStats.FULL_YELLOW = 95, 85
|
||||
ADStorageStats.BARN_LOW_RED, ADStorageStats.BARN_LOW_YELLOW = 10, 25
|
||||
-- "Sælg nu": prisen er mindst 90 % af årets top, og partiet er værd at køre med
|
||||
ADStorageStats.SELL_RATIO, ADStorageStats.SELL_MIN_VALUE = 0.9, 10000
|
||||
|
||||
local function percentOf(level, capacity)
|
||||
return (capacity or 0) > 0 and math.floor(100 * (level or 0) / capacity + 0.5) or nil
|
||||
end
|
||||
|
||||
-- inventory (fanens bygninger) -> { {fillType, title, level, capacity, percent, count, isOutput,
|
||||
-- buildings = {{name, placeable, level, capacity, percent, count, slots, isOutput}}} } — varer efter titel,
|
||||
-- bygninger med mest først. count (paller) summeres kun når alle bygninger melder paller.
|
||||
function ADStorageStats.groupByFillType(inventory, titleOf)
|
||||
local byType, groups = {}, {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
for _, item in ipairs(building.entries or {}) do
|
||||
local group = byType[item.fillType]
|
||||
if group == nil then
|
||||
group = {fillType = item.fillType, title = tostring(titleOf(item.fillType) or item.fillType),
|
||||
level = 0, capacity = 0, count = 0, isOutput = false, buildings = {}}
|
||||
byType[item.fillType] = group
|
||||
table.insert(groups, group)
|
||||
end
|
||||
group.level = group.level + (item.level or 0)
|
||||
group.capacity = group.capacity + (item.capacity or 0)
|
||||
group.count = (group.count ~= nil and item.count ~= nil) and group.count + item.count or nil
|
||||
group.isOutput = group.isOutput or item.isOutput == true
|
||||
-- skur uden pladsgrænse har kapacitet 0: så er varens samlede procent ukendt
|
||||
if (item.capacity or 0) <= 0 and (item.level or 0) > 0 then group.unbounded = true end
|
||||
local percent = percentOf(item.level, item.capacity)
|
||||
if item.count ~= nil and (item.slots or 0) > 0 then percent = percentOf(item.count, item.slots) end
|
||||
table.insert(group.buildings, {name = building.name, placeable = building.placeable, level = item.level or 0,
|
||||
capacity = item.capacity or 0, percent = percent, count = item.count, slots = item.slots, isOutput = item.isOutput == true})
|
||||
end
|
||||
end
|
||||
for _, group in ipairs(groups) do
|
||||
group.percent = not group.unbounded and percentOf(group.level, group.capacity) or nil
|
||||
table.sort(group.buildings, function(a, b)
|
||||
if a.level ~= b.level then return a.level > b.level end
|
||||
return tostring(a.name) < tostring(b.name)
|
||||
end)
|
||||
end
|
||||
table.sort(groups, function(a, b)
|
||||
if a.title ~= b.title then return a.title < b.title end
|
||||
return a.fillType < b.fillType
|
||||
end)
|
||||
return groups
|
||||
end
|
||||
|
||||
-- inventory -> {[fillType] = kapacitet i liter} for hele gården
|
||||
function ADStorageStats.farmCapacities(inventory)
|
||||
local totals = {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
for _, entry in ipairs(building.entries or {}) do
|
||||
totals[entry.fillType] = (totals[entry.fillType] or 0) + (entry.capacity or 0)
|
||||
end
|
||||
end
|
||||
return totals
|
||||
end
|
||||
|
||||
function ADStorageStats.emptyLevel(days)
|
||||
if days == nil then return nil end
|
||||
if days < ADStorageStats.DAYS_RED then return "red" end
|
||||
if days < ADStorageStats.DAYS_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Fuldt er kun et problem for noget der bliver produceret (fyldes løbende); en fuld lagersilo er bare lager.
|
||||
function ADStorageStats.fullLevel(percent, producing)
|
||||
if not producing or percent == nil then return nil end
|
||||
if percent >= ADStorageStats.FULL_RED then return "red" end
|
||||
if percent >= ADStorageStats.FULL_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
|
||||
local SEVERITY_RANK = {red = 2, yellow = 1}
|
||||
|
||||
function ADStorageStats.worst(a, b)
|
||||
return (SEVERITY_RANK[a] or 0) >= (SEVERITY_RANK[b] or 0) and a or b
|
||||
end
|
||||
|
||||
-- Én bygnings bjælke: stald-trug der tømmes (ikke mælk/gylle) er rødt når næsten tomt; produceret
|
||||
-- vare/fabriksoutput er rødt når næsten fuldt.
|
||||
function ADStorageStats.buildingLevel(building, kind, isStableOutput)
|
||||
local percent = building.percent
|
||||
if percent == nil then return nil end
|
||||
if kind == "husbandry" and not isStableOutput then
|
||||
if percent <= ADStorageStats.BARN_LOW_RED then return "red" end
|
||||
if percent <= ADStorageStats.BARN_LOW_YELLOW then return "yellow" end
|
||||
return nil
|
||||
end
|
||||
return ADStorageStats.fullLevel(percent, building.isOutput == true or isStableOutput)
|
||||
end
|
||||
|
||||
-- group fra groupByFillType; farm = {level, capacity} for varen på hele gården (nil = kun fanens tal);
|
||||
-- rate = {consumption, production} pr. døgn; quote = {price, top} pr. liter.
|
||||
function ADStorageStats.groupStats(group, farm, rate, quote)
|
||||
local farmLevel = farm ~= nil and farm.level or group.level
|
||||
local farmCapacity = farm ~= nil and farm.capacity or group.capacity
|
||||
local consumption = rate ~= nil and rate.consumption or 0
|
||||
local production = rate ~= nil and rate.production or 0
|
||||
local net = production - consumption
|
||||
local stat = {}
|
||||
for key, value in pairs(group) do stat[key] = value end
|
||||
stat.farmLevel, stat.farmCapacity = farmLevel, farmCapacity
|
||||
stat.consumption, stat.production, stat.net = consumption, production, net
|
||||
stat.days = consumption > production and farmLevel / (consumption - production) or nil
|
||||
stat.daysToFull = net > 0 and (farmCapacity or 0) > 0 and math.max(0, farmCapacity - farmLevel) / net or nil
|
||||
stat.producing = group.isOutput == true or net > 0
|
||||
if quote ~= nil and quote.price ~= nil then
|
||||
stat.valueNow = math.floor(group.level * quote.price + 0.5)
|
||||
stat.valueTop = math.floor(group.level * (quote.top or quote.price) + 0.5)
|
||||
stat.priceRatio = (quote.top or 0) > 0 and quote.price / quote.top or nil
|
||||
end
|
||||
stat.emptyLevel = ADStorageStats.emptyLevel(stat.days)
|
||||
stat.fullLevel = ADStorageStats.fullLevel(group.percent, stat.producing)
|
||||
stat.severity = ADStorageStats.worst(stat.emptyLevel, stat.fullLevel)
|
||||
return stat
|
||||
end
|
||||
|
||||
-- Rangering: rød tom > rød fuld > gul tom > gul fuld > sælg nu (største beløb først).
|
||||
-- Kortest tid til tom / mest fyldt vinder inden for samme trin.
|
||||
local function attentionEntries(stat)
|
||||
local entries = {}
|
||||
if stat.emptyLevel ~= nil then
|
||||
local base = stat.emptyLevel == "red" and 4000 or 2000
|
||||
table.insert(entries, {kind = "empty", level = stat.emptyLevel, stat = stat, score = base - math.min(999, (stat.days or 0) * 100)})
|
||||
end
|
||||
if stat.fullLevel ~= nil then
|
||||
local base = stat.fullLevel == "red" and 3000 or 1000
|
||||
table.insert(entries, {kind = "full", level = stat.fullLevel, stat = stat, score = base + (stat.percent or 0)})
|
||||
end
|
||||
-- sælg kun det gården ikke selv æder af
|
||||
if stat.priceRatio ~= nil and stat.priceRatio >= ADStorageStats.SELL_RATIO and (stat.valueNow or 0) >= ADStorageStats.SELL_MIN_VALUE
|
||||
and (stat.net or 0) >= 0 then
|
||||
table.insert(entries, {kind = "sell", level = "green", stat = stat, score = math.min(999, stat.valueNow / 1e6)})
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function ADStorageStats.attentionList(stats, max)
|
||||
local list = {}
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.score ~= b.score then return a.score > b.score end
|
||||
return tostring(a.stat.title) < tostring(b.stat.title)
|
||||
end)
|
||||
local result = {}
|
||||
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADStorageStats.kpis(stats)
|
||||
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
kpi.valueNow = kpi.valueNow + (stat.valueNow or 0)
|
||||
kpi.gain = kpi.gain + math.max(0, (stat.valueTop or 0) - (stat.valueNow or 0))
|
||||
if stat.emptyLevel ~= nil then kpi.emptyCount = kpi.emptyCount + 1 end
|
||||
if stat.fullLevel ~= nil then kpi.fullCount = kpi.fullCount + 1 end
|
||||
end
|
||||
return kpi
|
||||
end
|
||||
|
||||
-- procent -> bjælkens brøk i [0,1]
|
||||
function ADStorageStats.fraction(percent)
|
||||
if percent == nil then return 0 end
|
||||
return math.max(0, math.min(1, percent / 100))
|
||||
end
|
||||
|
||||
-- ---- "kræver handling" -> flow ----
|
||||
|
||||
-- Bygninger der BRUGER varen (fabrik-input, stald-foder) — silo/objektlager lagrer kun. -> {{name, placeable, level}}
|
||||
function ADStorageStats.consumersOf(inventory, fillType)
|
||||
local result = {}
|
||||
for _, building in ipairs(inventory or {}) do
|
||||
if building.kind == "factory" or building.kind == "husbandry" then
|
||||
for _, entry in ipairs(building.entries or {}) do
|
||||
if entry.fillType == fillType and not entry.isOutput then
|
||||
table.insert(result, {name = building.name, placeable = building.placeable, level = entry.level or 0})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Hjælper et flow? Et flow flytter kun varer: det nytter når varen ligger andre steder end hos forbrugerne.
|
||||
-- s = {farmLevel, consumerLevel, perDay, consumerCount, existingRunId} -> "open" | "flow" | "produce" | "none"
|
||||
ADStorageStats.MIN_FLOW_LITERS = 5000
|
||||
ADStorageStats.MIN_FLOW_DAY_SHARE = 0.25 -- mindst en kvart dags forbrug skal kunne flyttes
|
||||
|
||||
function ADStorageStats.flowAdvice(s)
|
||||
if (s.consumerCount or 0) == 0 then return "none" end
|
||||
if s.existingRunId ~= nil then return "open" end
|
||||
local elsewhere = (s.farmLevel or 0) - (s.consumerLevel or 0)
|
||||
local needed = math.max(ADStorageStats.MIN_FLOW_LITERS, (s.perDay or 0) * ADStorageStats.MIN_FLOW_DAY_SHARE)
|
||||
return elsewhere >= needed and "flow" or "produce"
|
||||
end
|
||||
|
||||
-- "Sælg nu" -> udkørsels-flow: s = {sourceCount, existingRunId} -> "open" | "flow" | "none"
|
||||
function ADStorageStats.sellAdvice(s)
|
||||
if (s.sourceCount or 0) == 0 then return "none" end
|
||||
if s.existingRunId ~= nil then return "open" end
|
||||
return "flow"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
-- AD Stuck Diag
|
||||
-- Diagnose: et aktivt AutoDrive-køretøj der har holdt stille længe, logges med AutoDrives egen grund
|
||||
-- (forhindring, AD-trafik på ruten + hvem, pause, mål nået) og om Smart Pickup holder det (Wait/udkørsel).
|
||||
-- Kun læsning — ændrer intet. Logges én gang pr. ændret tilstand.
|
||||
|
||||
ADStuckDiag = {}
|
||||
|
||||
ADStuckDiag.CHECK_INTERVAL_MS = 10000
|
||||
ADStuckDiag.STILL_MS = 20000
|
||||
ADStuckDiag.STILL_SPEED = 0.00028 -- samme grænse som AD's egen stoppedTimer (m/ms)
|
||||
|
||||
local stillMs = setmetatable({}, {__mode = "k"})
|
||||
local lastText = setmetatable({}, {__mode = "k"})
|
||||
local timerMs = 0
|
||||
|
||||
local function name(vehicle)
|
||||
if vehicle == nil then return "-" end
|
||||
local ok, text = pcall(vehicle.getName, vehicle)
|
||||
return ok and tostring(text) or "?"
|
||||
end
|
||||
|
||||
local function taskName(vehicle)
|
||||
local taskModule = vehicle.ad.taskModule
|
||||
local task = taskModule ~= nil and taskModule.activeTask or nil
|
||||
if task == nil then return "ingen" end
|
||||
return tostring(task.taskType or "ukendt")
|
||||
end
|
||||
|
||||
-- Tekst der beskriver hvorfor køretøjet står (nil hvis ukendt/ikke relevant).
|
||||
function ADStuckDiag.describe(vehicle)
|
||||
local ad = vehicle.ad
|
||||
local cdm, dpm, sdm = ad.collisionDetectionModule, ad.drivePathModule, ad.specialDrivingModule
|
||||
local wayPoints, current = nil, nil
|
||||
if dpm ~= nil and dpm.getWayPoints ~= nil then wayPoints, current = dpm:getWayPoints() end
|
||||
local parts = {
|
||||
string.format("opgave=%s", taskName(vehicle)),
|
||||
string.format("mål=%s", tostring(ad.stateModule.getFirstMarkerId ~= nil and ad.stateModule:getFirstMarkerId() or "?")),
|
||||
string.format("wp=%s/%s", tostring(current), tostring(wayPoints ~= nil and #wayPoints or nil)),
|
||||
string.format("pause=%s", tostring(dpm ~= nil and dpm.isPaused)),
|
||||
string.format("målNået=%s", tostring(dpm ~= nil and dpm.isTargetReached ~= nil and dpm:isTargetReached())),
|
||||
string.format("forhindring=%s", tostring(cdm ~= nil and cdm.detectedObstable)),
|
||||
string.format("trafik=%s", cdm ~= nil and cdm.trafficVehicle ~= nil and name(cdm.trafficVehicle) or "-"),
|
||||
string.format("stopper=%s blokeret=%s", tostring(sdm ~= nil and sdm.isStoppingVehicle ~= nil and sdm:isStoppingVehicle()),
|
||||
tostring(sdm ~= nil and sdm.isBlocked)),
|
||||
}
|
||||
local wait = ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) or nil
|
||||
local hold = ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) or nil
|
||||
local pickup = ADSmartPickup ~= nil and ADSmartPickup.getActivePickup ~= nil and ADSmartPickup.getActivePickup(vehicle) or nil
|
||||
table.insert(parts, string.format("SP: wait=%s hold=%s pickup=%s",
|
||||
wait ~= nil and tostring(wait.markerId or wait.wayPointId or "ja") or "-",
|
||||
hold ~= nil and tostring(hold.markerId or "ja") or "-",
|
||||
pickup ~= nil and tostring(pickup.wayPointId or pickup.markerId or "ja") or "-"))
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
function ADStuckDiag.tick(dt)
|
||||
timerMs = timerMs + (dt or 0)
|
||||
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
|
||||
local elapsed = timerMs
|
||||
timerMs = 0
|
||||
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
|
||||
for _, vehicle in pairs(vehicles) do
|
||||
local ad = vehicle.ad
|
||||
local active = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive()
|
||||
if active and (vehicle.lastSpeedReal or 0) < ADStuckDiag.STILL_SPEED then
|
||||
stillMs[vehicle] = (stillMs[vehicle] or 0) + elapsed
|
||||
else
|
||||
stillMs[vehicle], lastText[vehicle] = nil, nil
|
||||
end
|
||||
if (stillMs[vehicle] or 0) >= ADStuckDiag.STILL_MS then
|
||||
local ok, text = pcall(ADStuckDiag.describe, vehicle)
|
||||
text = ok and text or ("diagnose fejlede: " .. tostring(text))
|
||||
if text ~= lastText[vehicle] then
|
||||
lastText[vehicle] = text
|
||||
Logging.info("ADSmartPickup: STÅR STILLE %d s '%s': %s", math.floor(stillMs[vehicle] / 1000), name(vehicle), text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,15 +29,32 @@ function ADSupplyPlanner.prefersFullestSource(fillTypeNames)
|
||||
return true
|
||||
end
|
||||
|
||||
-- "Rigelig" kilde: kan levere mindst MIN_LOAD_SHARE af et læs (mindst MIN_AMPLE_LITERS).
|
||||
ADSupplyPlanner.MIN_LOAD_SHARE = 0.25
|
||||
ADSupplyPlanner.MIN_AMPLE_LITERS = 5000
|
||||
|
||||
function ADSupplyPlanner.ampleLevel(loadCapacity)
|
||||
return math.max(ADSupplyPlanner.MIN_AMPLE_LITERS, (loadCapacity or 0) * ADSupplyPlanner.MIN_LOAD_SHARE)
|
||||
end
|
||||
|
||||
-- Er en kilde med dette niveau bedre end den hidtil bedste (nil = ingen endnu)?
|
||||
function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest)
|
||||
-- Lagervarer: mindst fyldte først, men kun blandt rigelige kilder — en næsten tom tank der fyldes løbende
|
||||
-- (gæringssilo) ville ellers altid vinde og give et par tusind liter pr. tur (in-game 29/9). Ingen rigelige:
|
||||
-- den fyldeste. ample = nil: gammel regel.
|
||||
function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest, ample)
|
||||
if bestLevel == nil then
|
||||
return true
|
||||
end
|
||||
if prefersFullest then
|
||||
return level > bestLevel
|
||||
end
|
||||
return level < bestLevel
|
||||
if ample == nil then
|
||||
return level < bestLevel
|
||||
end
|
||||
local isAmple, bestIsAmple = level >= ample, bestLevel >= ample
|
||||
if isAmple ~= bestIsAmple then return isAmple end
|
||||
if isAmple then return level < bestLevel end
|
||||
return level > bestLevel
|
||||
end
|
||||
|
||||
function ADSupplyPlanner.copyList(list)
|
||||
@@ -345,3 +362,14 @@ function ADSupplyPlanner.parallelFoodShare(groups, fillLevels, capacity, fillTyp
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Læs der allerede ligger på vognen, når et run startes (eller AutoDrive genstarter traktoren efter
|
||||
-- indlæsning): tæller som "på vej" til målet, hvis varen hører til run'et. Returnerer fillType, liter
|
||||
-- eller nil. (in-game 27/9: tre fyldte biogas-traktorer kørte alle til Drop 1 — ingen var registreret.)
|
||||
function ADSupplyPlanner.cargoFor(fillType, liters, ingredients)
|
||||
if fillType == nil or type(liters) ~= "number" or liters <= 0 then return nil end
|
||||
for _, ingredient in ipairs(ingredients or {}) do
|
||||
if ingredient == fillType then return fillType, liters end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- AD Tip Side
|
||||
-- Tipvogne med flere tip-sider (fx Farmtech EDK 650 Unrealistic: bag / kornluge / venstre / højre) tillader kun
|
||||
-- AI-aflæsning (allowAIDischarge) på nogle af udløbene. Står vognen på en side uden, åbner AD aflæsningen ved
|
||||
-- målet, men intet løber ud — vognen holder med fuldt læs (in-game 29/9: Chicken Food 2 med 786.712 l hvede på
|
||||
-- højre side ved en Farma med 10 mio. l fri; Disponibel Vogn 3 fuld af løg). Når AD kører en vogn, skiftes den til
|
||||
-- en side, AI må aflæsse fra.
|
||||
|
||||
ADTipSide = {}
|
||||
ADTipSide.LOG_PREFIX = "ADSmartPickup: tip-side: "
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- sides = {{node = udløbsindeks}}, allowAI = {[udløbsindeks] = true}, current = nuværende side
|
||||
-- -> sideindeks der skal vælges, eller nil (nuværende er fin / ingen side tilladt / ukendt)
|
||||
function ADTipSide.choose(sides, allowAI, current)
|
||||
local currentSide = (sides or {})[current]
|
||||
if currentSide ~= nil and (allowAI or {})[currentSide.node] then return nil end
|
||||
for index, side in ipairs(sides or {}) do
|
||||
if (allowAI or {})[side.node] then return index end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local warned = setmetatable({}, {__mode = "k"})
|
||||
|
||||
local function sidesOf(object)
|
||||
local spec = object.spec_trailer
|
||||
local dis = object.spec_dischargeable
|
||||
if spec == nil or dis == nil or spec.tipSides == nil or #spec.tipSides < 2 then return nil end
|
||||
local sides, allowAI = {}, {}
|
||||
for index, side in ipairs(spec.tipSides) do sides[index] = {node = side.dischargeNodeIndex} end
|
||||
for index, node in ipairs(dis.dischargeNodes or {}) do
|
||||
if node.allowAIDischarge then allowAI[index] = true end
|
||||
end
|
||||
return sides, allowAI, spec.preferedTipSideIndex
|
||||
end
|
||||
|
||||
-- Alle tipvogne bag traktoren: skift til en AI-tilladt side (kun mens vognen ikke tipper).
|
||||
function ADTipSide.ensure(vehicle)
|
||||
local children = vehicle.getChildVehicles ~= nil and vehicle:getChildVehicles() or {}
|
||||
for _, object in pairs(children) do
|
||||
if object ~= vehicle and object.setPreferedTipSide ~= nil then
|
||||
local sides, allowAI, current = sidesOf(object)
|
||||
local tipping = Trailer ~= nil and object.getTipState ~= nil and object:getTipState() ~= Trailer.TIPSTATE_CLOSED
|
||||
local index = sides ~= nil and not tipping and ADTipSide.choose(sides, allowAI, current) or nil
|
||||
if index ~= nil then
|
||||
object:setPreferedTipSide(index)
|
||||
if not warned[object] then
|
||||
warned[object] = true
|
||||
Logging.info(ADTipSide.LOG_PREFIX .. "'%s': tip-side %s -> %d (AutoDrive må kun aflæse fra den side)",
|
||||
tostring(object.getName ~= nil and object:getName() or "?"), tostring(current), index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -255,6 +255,9 @@ end
|
||||
-- AD ser — vognens indhold, målets fri plads, AD's aflæsningstilstand og vognens dischargeState.
|
||||
ADUnloadWait.DEBUG_INTERVAL_MS = 15000
|
||||
local debugTimers = setmetatable({}, {__mode = "k"})
|
||||
local debugStuck = setmetatable({}, {__mode = "k"}) -- vehicle -> {level, since}: læsset uændret siden
|
||||
ADUnloadWait.STUCK_WITH_ROOM_MS = 30000
|
||||
|
||||
local function debugAtTarget(adEnv, task, dt)
|
||||
local vehicle = task.vehicle
|
||||
local targetReached = vehicle.ad.drivePathModule:isTargetReached()
|
||||
@@ -268,8 +271,15 @@ local function debugAtTarget(adEnv, task, dt)
|
||||
return
|
||||
end
|
||||
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
|
||||
-- kun den tilstand vi vil se: målet fuldt/afvist, og traktoren venter ikke ved et Wait-punkt
|
||||
if waits[vehicle] ~= nil or (free ~= nil and free >= ADUnloadWait.MIN_FREE_TO_UNLOAD) then
|
||||
-- ved målet MED plads, men læsset falder ikke (in-game 29/9: EDK 650 ved TMR Unload/Biogas Drop) — også det
|
||||
-- vil vi se. Ellers kun: målet fuldt/afvist, og traktoren venter ikke ved et Wait-punkt.
|
||||
local stuck = debugStuck[vehicle]
|
||||
if stuck == nil or rest < stuck.level - 1 then
|
||||
stuck = {level = rest, since = g_time or 0}
|
||||
debugStuck[vehicle] = stuck
|
||||
end
|
||||
local stuckWithRoom = targetReached and (g_time or 0) - stuck.since >= ADUnloadWait.STUCK_WITH_ROOM_MS
|
||||
if waits[vehicle] ~= nil or (free ~= nil and free >= ADUnloadWait.MIN_FREE_TO_UNLOAD and not stuckWithRoom) then
|
||||
return
|
||||
end
|
||||
local trailerModule = vehicle.ad.trailerModule
|
||||
@@ -295,6 +305,12 @@ function ADUnloadWait.beforeUpdate(adEnv, task, dt)
|
||||
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle ~= nil and ADOutbound.isOutboundVehicle(task.vehicle) then
|
||||
return
|
||||
end
|
||||
-- Tømning efter stop (ADFlowWagons): ingen Wait-pulje eller læs-bytte — den skal tømme og hjem
|
||||
-- (in-game 29/9: Goat Food blev sendt til Cow Wait efter Stop alt)
|
||||
if ADFlowWagons ~= nil and ADFlowWagons.isEmptying ~= nil and ADFlowWagons.isEmptying(task.vehicle) then
|
||||
if waits[task.vehicle] ~= nil then release(task.vehicle) end
|
||||
return
|
||||
end
|
||||
pcall(debugAtTarget, adEnv, task, dt)
|
||||
local wait = waits[task.vehicle]
|
||||
if wait ~= nil then
|
||||
@@ -309,7 +325,7 @@ end
|
||||
-- stående for evigt. Her genoptages ventningen for aktive traktorer i et forsynings-run hvis
|
||||
-- aflæsningsopgave peger på en Wait-markør: hjem = run'ets mål, og reservationen sættes igen.
|
||||
-- Kaldes fra ADSmartPickup:update (throttlet).
|
||||
ADUnloadWait.ADOPT_INTERVAL_MS = 5000
|
||||
ADUnloadWait.ADOPT_INTERVAL_MS = 5300
|
||||
local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS
|
||||
|
||||
function ADUnloadWait.adoptOrphans(adEnv)
|
||||
@@ -410,6 +426,9 @@ function ADUnloadWait.getClaims(adEnv)
|
||||
return ADWaitPool.getClaims(adEnv)
|
||||
end
|
||||
|
||||
ADUnloadWait.TIP_CHECK_MS = 2000
|
||||
local tipChecked = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADUnloadWait.install(adEnv)
|
||||
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
||||
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
|
||||
@@ -422,6 +441,15 @@ function ADUnloadWait.install(adEnv)
|
||||
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
|
||||
|
||||
task.update = function(self, dt)
|
||||
-- alle aflæsningsture (også udkørsel og tømning): tipvognen på en side AD må aflæsse fra (EDK 650, 29/9)
|
||||
if ADTipSide ~= nil and self.vehicle ~= nil then
|
||||
local now = g_time or 0
|
||||
if (tipChecked[self.vehicle] or -math.huge) + ADUnloadWait.TIP_CHECK_MS <= now then
|
||||
tipChecked[self.vehicle] = now
|
||||
local okTip, errTip = pcall(ADTipSide.ensure, self.vehicle)
|
||||
if not okTip then Logging.warning(ADUnloadWait.LOG_PREFIX .. "tip-side: %s", tostring(errTip)) end
|
||||
end
|
||||
end
|
||||
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
|
||||
if not ok then
|
||||
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
ADWaitPool = {}
|
||||
|
||||
-- ADCache-opslag der også virker uden adCache (tests)
|
||||
local function cacheGet(bucket, key, ttlMs, fn)
|
||||
if ADCacheGet == nil then return fn() end
|
||||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||||
end
|
||||
|
||||
|
||||
ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
|
||||
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
|
||||
ADWaitPool.STATE_DRIVING = 2
|
||||
@@ -14,6 +21,10 @@ ADWaitPool.claims = setmetatable({}, {__mode = "v"})
|
||||
|
||||
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
|
||||
function ADWaitPool.getCandidates(adEnv, poolGroup)
|
||||
return cacheGet("waitCandidates", tostring(poolGroup or ""), 10000, function() return ADWaitPool.findCandidates(adEnv, poolGroup) end)
|
||||
end
|
||||
|
||||
function ADWaitPool.findCandidates(adEnv, poolGroup)
|
||||
local candidates = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
|
||||
@@ -57,26 +68,47 @@ ADWaitPool.OCCUPIED_RADIUS = 6
|
||||
|
||||
-- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et
|
||||
-- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning).
|
||||
-- Alle motoriserede køretøjers positioner, delt mellem opslag i 0,5 s (ikke én runde pr. vogn pr. opslag).
|
||||
local function allVehiclePositions()
|
||||
return cacheGet("vehiclePositions", "all", 500, function()
|
||||
local list = {}
|
||||
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
||||
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
||||
if other.spec_motorized ~= nil and other.rootNode ~= nil then
|
||||
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
||||
if ok and x ~= nil then table.insert(list, {vehicle = other, x = x, z = z}) end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end)
|
||||
end
|
||||
|
||||
local function otherVehiclePositions(vehicle)
|
||||
local positions = {}
|
||||
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
||||
local own = {}
|
||||
if vehicle.getChildVehicles ~= nil then
|
||||
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
||||
for _, child in pairs(ok and children or {}) do own[child] = true end
|
||||
end
|
||||
own[vehicle] = true
|
||||
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
||||
if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then
|
||||
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
||||
if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
for _, entry in ipairs(allVehiclePositions()) do
|
||||
if not own[entry.vehicle] then table.insert(positions, entry) end
|
||||
end
|
||||
return positions
|
||||
end
|
||||
|
||||
function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable)
|
||||
-- Midtpunkt af punkter {x, z} (fx flowets kilde-markører) -> x, z eller nil.
|
||||
function ADWaitPool.anchorOf(points)
|
||||
if points == nil or #points == 0 then return nil end
|
||||
local sx, sz = 0, 0
|
||||
for _, point in ipairs(points) do sx, sz = sx + point.x, sz + point.z end
|
||||
return sx / #points, sz / #points
|
||||
end
|
||||
|
||||
-- anchorX/anchorZ (valgfri): vælg nærmest dette punkt i stedet for traktoren (flow med "All": nærmest kilderne).
|
||||
function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable, anchorX, anchorZ)
|
||||
local x, z = getVehiclePosition(vehicle)
|
||||
if anchorX ~= nil and anchorZ ~= nil then x, z = anchorX, anchorZ end
|
||||
if x == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -124,11 +156,42 @@ local function graphReaches(adEnv, vehicle, targetId)
|
||||
return false
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil then
|
||||
return true
|
||||
-- Rutesvar gemmes ROUTE_CACHE_MS pr. mål og 10 m-område: holdende vogne spørger hvert 5. s for hver
|
||||
-- kandidat, og AD's stifinding (+ reserve-søgningen) gennem hele vejnettet gav et hak hvert 5. s
|
||||
-- (in-game 26/9). Vejnettet ændrer sig ikke så hurtigt.
|
||||
ADWaitPool.ROUTE_CACHE_MS = 30000
|
||||
ADWaitPool.ROUTE_CELL_M = 10
|
||||
ADWaitPool.SLOW_ROUTE_MS = 3
|
||||
local routeCache = {}
|
||||
local routeCacheSize = 0
|
||||
ADWaitPool.ROUTE_CACHE_MAX = 2000
|
||||
|
||||
local function remember(key, result, at)
|
||||
if routeCache[key] == nil then
|
||||
routeCacheSize = routeCacheSize + 1
|
||||
if routeCacheSize > ADWaitPool.ROUTE_CACHE_MAX then
|
||||
routeCache, routeCacheSize = {}, 1
|
||||
end
|
||||
end
|
||||
routeCache[key] = {result = result, at = at}
|
||||
end
|
||||
|
||||
local function clockMs()
|
||||
local ok, ms = pcall(function() return netGetTime() end)
|
||||
if ok and type(ms) == "number" then return ms end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function routeKey(vehicle, wayPointId)
|
||||
if vehicle.rootNode == nil or getWorldTranslation == nil then return nil end
|
||||
local ok, x, _, z = pcall(getWorldTranslation, vehicle.rootNode)
|
||||
if not ok or x == nil then return nil end
|
||||
local cell = ADWaitPool.ROUTE_CELL_M
|
||||
return string.format("%s:%d:%d", tostring(wayPointId), math.floor(x / cell), math.floor(z / cell))
|
||||
end
|
||||
|
||||
local function computeRoute(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId) then
|
||||
return true
|
||||
end
|
||||
@@ -142,13 +205,73 @@ function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
return false
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil then
|
||||
return true
|
||||
end
|
||||
local now = clockMs()
|
||||
local key = now ~= nil and routeKey(vehicle, wayPointId) or nil
|
||||
local cached = key ~= nil and routeCache[key] or nil
|
||||
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = computeRoute(adEnv, vehicle, wayPointId)
|
||||
if key ~= nil then
|
||||
local spent = (clockMs() or now) - now
|
||||
remember(key, result, now)
|
||||
if spent > ADWaitPool.SLOW_ROUTE_MS and Logging ~= nil and Logging.info ~= nil
|
||||
and (ADWaitPool.lastSlowLog == nil or now - ADWaitPool.lastSlowLog > 60000) then
|
||||
ADWaitPool.lastSlowLog = now
|
||||
Logging.info("ADSmartPickup: rutetjek til markør %s tog %.1f ms (%s) — gemmes nu i %d s", tostring(wayPointId), spent, result and "vej" or "ingen vej", ADWaitPool.ROUTE_CACHE_MS / 1000)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Rute mellem to markører: afhænger ikke af vognens position, så svaret gemmes længe (5 min). Profiler
|
||||
-- 26/9: "rutetjek frem+tilbage" tog 30-39 ms pr. kald og blev regnet igen hver gang vognen havde flyttet sig
|
||||
-- 10 m — det gav hak på 44-67 ms hvert minut.
|
||||
ADWaitPool.MARKER_ROUTE_MS = 300000
|
||||
local markerRoutes = {}
|
||||
function ADWaitPool.hasMarkerRoute(adEnv, fromWayPointId, toWayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.pathFromTo == nil then return true end
|
||||
local key = tostring(fromWayPointId) .. ">" .. tostring(toWayPointId)
|
||||
local now = clockMs()
|
||||
local cached = markerRoutes[key]
|
||||
if cached ~= nil and now ~= nil and now - cached.at < ADWaitPool.MARKER_ROUTE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = pathReaches(graph:pathFromTo(fromWayPointId, toWayPointId), toWayPointId)
|
||||
if now ~= nil then markerRoutes[key] = {result = result, at = now + math.random() * ADWaitPool.MARKER_ROUTE_MS * 0.5} end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADWaitPool.resetMarkerRoutes()
|
||||
markerRoutes = {}
|
||||
end
|
||||
|
||||
function ADWaitPool.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
|
||||
local graph = adEnv.ADGraphManager
|
||||
if graph.getPathTo == nil or graph.pathFromTo == nil then
|
||||
return true
|
||||
end
|
||||
return pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
|
||||
-- kendt hjem (kilden): vognen kører hjem -> mål -> hjem; markør til markør, gemt længe
|
||||
if homeWayPointId ~= nil then
|
||||
return ADWaitPool.hasMarkerRoute(adEnv, homeWayPointId, toWayPointId)
|
||||
and ADWaitPool.hasMarkerRoute(adEnv, toWayPointId, homeWayPointId)
|
||||
end
|
||||
local now = clockMs()
|
||||
local key = now ~= nil and routeKey(vehicle, tostring(toWayPointId) .. ">" .. tostring(homeWayPointId)) or nil
|
||||
local cached = key ~= nil and routeCache[key] or nil
|
||||
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
||||
return cached.result
|
||||
end
|
||||
local result = pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
|
||||
and pathReaches(graph:pathFromTo(toWayPointId, homeWayPointId), homeWayPointId)
|
||||
if key ~= nil then remember(key, result, now) end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Står AD midt i en (blokeret) af-/pålæsning, stoppes den pænt først — ellers kører
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
-- Smart Pickup — "Kræver handling" -> flow (v1.60)
|
||||
-- En "løber tør"-linje i lagerfanerne kan klikkes: findes der et flow der leverer varen til forbrugerne, åbnes
|
||||
-- det; ligger varen andre steder på gården, åbnes "Nyt flow" udfyldt (ind til forbrugerne, varen valgt); ellers
|
||||
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
end
|
||||
|
||||
-- Forsyningsflow der allerede leverer varen til en af forbrugerne -> run eller nil.
|
||||
local function existingSupplyRun(consumerIds, name)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if run.kind ~= "outbound" then
|
||||
local carries = run.ingredientMode ~= "manual"
|
||||
for _, ingredient in ipairs(run.ingredients or {}) do
|
||||
if ingredient == name then carries = true end
|
||||
end
|
||||
if carries then
|
||||
for _, id in ipairs(run.targetBuildingIds or {}) do
|
||||
if consumerIds[id] then return run end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Udkørselsflow der allerede sælger varen fra en af bygningerne -> run eller nil.
|
||||
local function existingSellRun(sourceIds, name)
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
if run.kind == "outbound" then
|
||||
local sells = false
|
||||
for _, product in ipairs(run.products or {}) do
|
||||
if product.fillType == name and (product.policy == "sell" or product.policy == "sellElseStore") then sells = true end
|
||||
end
|
||||
if sells then
|
||||
for _, id in ipairs(run.sourceBuildingIds or {}) do
|
||||
if sourceIds[id] then return run end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- "Sælg nu": kilder = fanens bygninger med varen.
|
||||
local function sellAdviceFor(stat)
|
||||
local sourceIds, ids = {}, {}
|
||||
for _, building in ipairs(stat.buildings or {}) do
|
||||
local id = building.placeable ~= nil and ADBuildings.getId(building.placeable) or nil
|
||||
if id ~= nil and not sourceIds[id] then
|
||||
sourceIds[id] = true
|
||||
table.insert(ids, id)
|
||||
end
|
||||
end
|
||||
local name = fillTypeName(stat.fillType)
|
||||
local run = existingSellRun(sourceIds, name)
|
||||
local advice = ADStorageStats.sellAdvice({sourceCount = #ids, existingRunId = run ~= nil and run.id or nil})
|
||||
return advice, {buildingIds = ids, run = run, name = name, stat = stat, sell = true}
|
||||
end
|
||||
|
||||
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
|
||||
function SmartPickupFrame:attentionAdvice(entry)
|
||||
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
|
||||
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
|
||||
local stat = entry.stat
|
||||
local consumers = ADStorageStats.consumersOf(self.lastFullInventory, stat.fillType)
|
||||
local consumerIds, consumerLevel, ids = {}, 0, {}
|
||||
for _, consumer in ipairs(consumers) do
|
||||
consumerLevel = consumerLevel + consumer.level
|
||||
local id = consumer.placeable ~= nil and ADBuildings.getId(consumer.placeable) or nil
|
||||
if id ~= nil and not consumerIds[id] then
|
||||
consumerIds[id] = true
|
||||
table.insert(ids, id)
|
||||
end
|
||||
end
|
||||
local name = fillTypeName(stat.fillType)
|
||||
local run = existingSupplyRun(consumerIds, name)
|
||||
local advice = ADStorageStats.flowAdvice({farmLevel = stat.farmLevel or stat.level, consumerLevel = consumerLevel,
|
||||
perDay = stat.consumption, consumerCount = #ids, existingRunId = run ~= nil and run.id or nil})
|
||||
return advice, {consumers = consumers, buildingIds = ids, run = run, name = name, stat = stat}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:attentionHint(advice, info)
|
||||
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
|
||||
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
|
||||
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
|
||||
return ""
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onAttentionClick(entry)
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
local advice, info = frame:attentionAdvice(entry)
|
||||
if adEnv == nil or advice == "none" then return end
|
||||
if advice == "open" then
|
||||
frame.selectedRunId = info.run.id
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
frame:openEditPanel("flow", info.run.id)
|
||||
return
|
||||
end
|
||||
if advice == "produce" then
|
||||
local text = string.format(frame.i18n:getText("spu_attention_produce"), tostring(info.stat.title),
|
||||
SmartPickupFrame.formatCompact(info.stat.farmLevel or 0))
|
||||
frame.statusOverride, frame.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
|
||||
return
|
||||
end
|
||||
local guide
|
||||
if info.sell then
|
||||
-- udfyldt "Nyt flow": ud af bygningerne med varen, varen til salg
|
||||
local name = string.format(frame.i18n:getText("spu_attention_sellFlowName"), tostring(info.stat.title))
|
||||
guide = ADFlowGuide.openNew(name, {})
|
||||
guide.draft.kind = "outbound"
|
||||
guide.draft.sourceBuildingIds = info.buildingIds
|
||||
guide.draft.products = {{fillType = info.name, policy = "sell"}}
|
||||
else
|
||||
-- udfyldt "Nyt flow": ind til forbrugerne, kun denne vare; guiden åbnes på oversigten (Gem/Start eller [Ret])
|
||||
local name = string.format(frame.i18n:getText("spu_attention_flowName"), tostring(info.stat.title), tostring(info.consumers[1].name))
|
||||
guide = ADFlowGuide.openNew(name, {})
|
||||
guide.draft.targetBuildingIds = info.buildingIds
|
||||
guide.draft.ingredientMode = "manual"
|
||||
guide.draft.ingredients = {info.name}
|
||||
end
|
||||
guide = ADGuide.new(ADFlowGuide.DEF, guide.draft, frame:flowGuideContext(adEnv, guide.draft), {step = "summary"})
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
frame:openGuide("flow", guide)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,194 @@
|
||||
-- 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[kind] =
|
||||
-- {title, exists, fill, status, startStop, startStopText, canDelete, delete} (se Flows-/Fields-fanen).
|
||||
-- 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.80, 0.90 -- andel af listens flade
|
||||
SmartPickupFrame.EDIT_PANEL_HEADER_PX = 80
|
||||
SmartPickupFrame.EDIT_PANEL_ROW_PX = 860 -- rækkerne er ~835 px brede
|
||||
|
||||
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_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
|
||||
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
|
||||
self.panelButtonsNoDelete = {self.panelCloseButtonInfo, self.panelStartButtonInfo}
|
||||
self.panelGuideButtonCache = setmetatable({}, {__mode = "k"})
|
||||
end
|
||||
|
||||
-- Vinduets knaplinje (uden Slet for arter der ikke kan slettes).
|
||||
function SmartPickupFrame:getPanelButtons()
|
||||
local hooks = self:editPanelHooks()
|
||||
return (hooks ~= nil and hooks.canDelete) and self.panelButtons or self.panelButtonsNoDelete
|
||||
end
|
||||
|
||||
function SmartPickupFrame:editPanelHooks()
|
||||
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
|
||||
end
|
||||
|
||||
-- Guidens knapper i vinduet: Tilbage (ESC) lukker trinnet/vinduet i stedet for hele menuen.
|
||||
function SmartPickupFrame:panelGuideButtons()
|
||||
local source = self:getGuideButtons()
|
||||
local cached = self.panelGuideButtonCache[source]
|
||||
if cached == nil then
|
||||
cached = {}
|
||||
for index, info in ipairs(source) do
|
||||
cached[index] = info == self.backButtonInfo and self.panelCloseButtonInfo or info
|
||||
end
|
||||
self.panelGuideButtonCache[source] = cached
|
||||
end
|
||||
return cached
|
||||
end
|
||||
|
||||
-- Sæt størrelse FØR position: setSize kan genberegne den absolutte position ud fra forælderen (review 29/9).
|
||||
-- -> true hvis elementet flyttede sig.
|
||||
local function place(element, x, y, w, h)
|
||||
if w ~= nil then element:setSize(w, h) end
|
||||
local pos = element.absPosition
|
||||
local moved = pos == nil or math.abs(pos[1] - x) > 1e-6 or math.abs(pos[2] - y) > 1e-6
|
||||
element:setAbsolutePosition(x, y)
|
||||
return moved
|
||||
end
|
||||
|
||||
-- Vinduet dækker listens flade; panelet er centreret i den (absolutte koordinater, y opad). Kaldes ved åbning,
|
||||
-- ved hver opbygning og hvert sekund, så en layout-gennemgang i menuen ikke flytter det væk.
|
||||
function SmartPickupFrame:layoutEditPanel()
|
||||
local box = self.boxLayout
|
||||
if box == nil or box.absPosition == nil or box.absSize == nil then return end
|
||||
local x, y, w, h = box.absPosition[1], box.absPosition[2], box.absSize[1], box.absSize[2]
|
||||
place(self.editBackdrop, x, y, w, h)
|
||||
-- så bredt som rækkerne (~835 px) + margen, ikke bredere end listen
|
||||
local pw = math.min(w * SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_ROW_PX * g_pixelSizeScaledX)
|
||||
local ph = h * SmartPickupFrame.EDIT_PANEL_HEIGHT
|
||||
local px, py = x + (w - pw) / 2, y + (h - ph) / 2
|
||||
place(self.editPanel, px, py, pw, ph)
|
||||
local header = SmartPickupFrame.EDIT_PANEL_HEADER_PX * g_pixelSizeScaledY
|
||||
local margin = 20 * g_pixelSizeScaledX
|
||||
place(self.editTitle, px + margin, py + ph - header * 0.45)
|
||||
place(self.editStatus, px + margin, py + ph - header * 0.85)
|
||||
if place(self.editLayout, px, py, pw, ph - header) then
|
||||
self.editLayout:invalidateLayout()
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openEditPanel(kind, id)
|
||||
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil or self.editOverlay == nil then return end
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
|
||||
self:closeGuide()
|
||||
self.editOverlay:setVisible(true)
|
||||
-- listen skjules bag vinduet: dens tekster skinnede igennem panelet (in-game 29/9)
|
||||
self.boxLayout:setVisible(false)
|
||||
self:layoutEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Lukker uden genopbygning (fanebyt, lukket menu, slettet post).
|
||||
function SmartPickupFrame:hideEditPanel()
|
||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
||||
self:clearPanelRows()
|
||||
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
|
||||
if self.boxLayout ~= nil then self.boxLayout:setVisible(true) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:closeEditPanel()
|
||||
if not self.editPanelOpen then return end
|
||||
self:closeGuide()
|
||||
self:hideEditPanel()
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
|
||||
function SmartPickupFrame:fillEditPanel(adEnv)
|
||||
self:clearPanelRows()
|
||||
local hooks = self:editPanelHooks()
|
||||
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:closeGuide()
|
||||
self:hideEditPanel()
|
||||
return
|
||||
end
|
||||
self:layoutEditPanel()
|
||||
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 i niveaufarve + knaptekster.
|
||||
function SmartPickupFrame:refreshEditPanel(adEnv)
|
||||
local hooks = self:editPanelHooks()
|
||||
if hooks == nil or adEnv == 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:closeEditPanel()
|
||||
return
|
||||
end
|
||||
self:layoutEditPanel()
|
||||
local status = hooks.status(self, adEnv, self.editPanelId)
|
||||
self.editStatus:setText(tostring(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.panelStartButtonInfo.disabled = hooks.canStart ~= nil and not hooks.canStart(self, self.editPanelId)
|
||||
end
|
||||
|
||||
-- ESC mens vinduet er åbent: menuen spørger requestClose; vinduet lukkes (trin først), menuen bliver.
|
||||
-- Kalder spillet OGSÅ knappens callback for samme tryk, må det ikke lukke to trin (panelClosedAt).
|
||||
SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS = 250
|
||||
|
||||
function SmartPickupFrame:requestClose(callback)
|
||||
if self.editPanelOpen then
|
||||
self:onClickPanelClose()
|
||||
return false
|
||||
end
|
||||
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
|
||||
return false
|
||||
end
|
||||
return SmartPickupFrame:superClass().requestClose(self, callback)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelClose()
|
||||
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
|
||||
return
|
||||
end
|
||||
self.panelClosedAt = g_time
|
||||
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:editPanelHooks()
|
||||
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickPanelDelete()
|
||||
self:safe(function(frame)
|
||||
local hooks = frame:editPanelHooks()
|
||||
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,242 @@
|
||||
-- Smart Pickup — guiden "Opsæt mark" og fanen Markers knapper
|
||||
-- Henter det spil-afhængige (afgrøder der kan sås, slåafgrøder, halm, maskiner, marksiloer, siloer i nærheden)
|
||||
-- som `context` til ADFieldGuide og gemmer guidens resultat via ADFieldJobs. Selve trin-skærmen er fælles
|
||||
-- (gui/SmartPickupGuide.lua). Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
-- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer)
|
||||
SmartPickupFrame.FIELD_STORE_ROWS = 8
|
||||
|
||||
function SmartPickupFrame:initFieldButtons(i18n)
|
||||
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
|
||||
self.fieldSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_field_open"), callback = function() self:onClickFieldSetup() end}
|
||||
self.fieldFleetButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_open"), callback = function() self:onClickFieldFleet(true) end}
|
||||
self.fieldFleetCloseButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_close"), callback = function() self:onClickFieldFleet(false) end}
|
||||
-- v1.59: listen har Start/Stop pr. række; Opsæt mark ligger i redigeringsvinduet
|
||||
self.fieldButtons = {self.backButtonInfo, self.fieldFleetButtonInfo}
|
||||
self.fieldFleetButtons = {self.backButtonInfo, self.fieldFleetCloseButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:getFieldButtons()
|
||||
return self.fieldView == "fleet" and self.fieldFleetButtons or self.fieldButtons
|
||||
end
|
||||
|
||||
-- ---- context til ADFieldGuide ----
|
||||
|
||||
-- {[fieldId] = rigIds} for alle marker
|
||||
function SmartPickupFrame.rigIdsByField()
|
||||
local byField = {}
|
||||
for fieldId, field in pairs(ADFieldJobs.state.fields) do byField[fieldId] = field.plan ~= nil and field.plan.rigIds or {} end
|
||||
return byField
|
||||
end
|
||||
|
||||
local function fruitTypes()
|
||||
return g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
|
||||
end
|
||||
|
||||
-- Afgrøder der kan sås, slåafgrøder (navne) og afgrøder med halm (skår efter mejetærskeren).
|
||||
function SmartPickupFrame:fruitContext()
|
||||
local sowable, straw, mowable = {}, {}, {}
|
||||
for _, fruit in ipairs(fruitTypes()) do
|
||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
||||
table.insert(sowable, {name = fruit.name, title = tostring(fruit.fillType.title or fruit.name)})
|
||||
end
|
||||
local ok, hasStraw = pcall(function() return fruit.hasWindrow == true or fruit.windrowName ~= nil end)
|
||||
if ok and hasStraw and fruit.name ~= nil then straw[fruit.name] = true end
|
||||
end
|
||||
if ADFields ~= nil and g_fruitTypeManager ~= nil then
|
||||
for index in pairs(ADFields.getMowable()) do
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(index)
|
||||
if fruit ~= nil and fruit.name ~= nil then mowable[fruit.name] = true end
|
||||
end
|
||||
end
|
||||
return sowable, mowable, straw
|
||||
end
|
||||
|
||||
-- Markens vare ud fra kladdens plan (ensilage -> skår, hø -> tørskår, høst -> afgrøden); nil på bar mark.
|
||||
local function fieldFillType(info, plan)
|
||||
if info.kind == "mowable" then
|
||||
return ADFieldModel.product(plan, "mowable") == "hay" and info.hay or info.windrow
|
||||
end
|
||||
if info.kind == "harvest" then return info.harvestFillType end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Siloer der tager markens vare: marksilo først, gæringssiloer, så de nærmeste (samme regel som før).
|
||||
function SmartPickupFrame:fieldStoreContext(adEnv, info, plan, chosenIds)
|
||||
local fillType = fieldFillType(info, plan)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local fieldSilos, silosById = {}, {}
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
|
||||
table.insert(fieldSilos, {id = building.id, name = tostring(building.name),
|
||||
text = markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)})
|
||||
silosById[building.id] = true
|
||||
end
|
||||
if fillType == nil then return nil, 0, fieldSilos end
|
||||
local chosen = {}
|
||||
for _, id in ipairs(chosenIds or {}) do chosen[id] = true end
|
||||
local fermenterIds = {}
|
||||
if info.kind == "mowable" and ADFieldModel.product(plan, "mowable") == "silage" then
|
||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
||||
end
|
||||
local entries = {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
||||
local distance = nil
|
||||
local node = building.placeable ~= nil and building.placeable.rootNode or nil
|
||||
if node ~= nil and info.x ~= nil then
|
||||
local bx, _, bz = getWorldTranslation(node)
|
||||
distance = math.sqrt((bx - info.x) ^ 2 + (bz - info.z) ^ 2)
|
||||
end
|
||||
table.insert(entries, {id = building.id, building = building, distance = distance, chosen = chosen[building.id] == true,
|
||||
fieldSilo = silosById[building.id] == true, fermenter = fermenterIds[building.id] == true})
|
||||
end
|
||||
end
|
||||
local shown, hidden = ADFieldView.storeRows(entries, SmartPickupFrame.FIELD_STORE_ROWS)
|
||||
local stores = {}
|
||||
for _, entry in ipairs(shown) do
|
||||
local building = entry.building
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
|
||||
-- det der skelner rækkerne først (rækken afkorter bagfra): markør, afstand, art — så bygningens navn
|
||||
local label = (markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)) .. ", " .. ADFieldView.distanceText(entry.distance)
|
||||
if entry.fieldSilo then label = label .. " (" .. self.i18n:getText("spu_field_fieldSilo") .. ")" end
|
||||
if entry.fermenter then label = label .. " (" .. self.i18n:getText("spu_field_fermenter") .. ")" end
|
||||
if markers[1] ~= nil then label = label .. " — " .. tostring(building.name) end
|
||||
table.insert(stores, {id = building.id, text = label})
|
||||
end
|
||||
return stores, hidden, fieldSilos
|
||||
end
|
||||
|
||||
-- Markerne til trin 1 ("F22 — Hvede — Venter").
|
||||
function SmartPickupFrame:fieldPickList()
|
||||
local fields = {}
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do
|
||||
local crop = ADFieldView.cropText(row.info, function(key) return self.i18n:getText(key) end)
|
||||
table.insert(fields, {id = row.info.id, text = string.format("F%d — %s — %s", row.info.id, crop, self:fieldStatusText(row.field, row.info, true))})
|
||||
end
|
||||
return fields
|
||||
end
|
||||
|
||||
-- context for en mark (info) med kladdens plan; info nil = trin 1 uden valgt mark.
|
||||
function SmartPickupFrame:fieldGuideContext(adEnv, info, plan, chosenStoreIds)
|
||||
local sowable, mowable, straw = self:fruitContext()
|
||||
local context = {info = info or {}, sowable = sowable, mowable = mowable, straw = straw,
|
||||
rigIdsByField = SmartPickupFrame.rigIdsByField(), fieldSilos = {}, storeHidden = 0, fields = {}, rigs = {}}
|
||||
-- marklisten bruges kun i trin 1 (guiden åbnet uden valgt mark)
|
||||
if info == nil then context.fields = self:fieldPickList() end
|
||||
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do
|
||||
table.insert(context.rigs, {id = rig.id, role = rig.role, text = string.format("%s — %s", tostring(rig.label), self:roleText(rig.role))})
|
||||
end
|
||||
if info ~= nil then
|
||||
context.stores, context.storeHidden, context.fieldSilos = self:fieldStoreContext(adEnv, info, plan or {}, chosenStoreIds)
|
||||
end
|
||||
return context
|
||||
end
|
||||
|
||||
-- ---- hooks til den fælles guide-skærm ----
|
||||
|
||||
local function chosenStoreIds(guide)
|
||||
local ids = {}
|
||||
for _, id in ipairs(guide.draft.storeIds or {}) do table.insert(ids, id) end
|
||||
for _, id in ipairs(guide.draft.plan ~= nil and guide.draft.plan.storeBuildingIds or {}) do table.insert(ids, id) end
|
||||
return ids
|
||||
end
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
SmartPickupFrame.GUIDE_HOOKS.field = {
|
||||
model = ADFieldGuide,
|
||||
tab = SmartPickupFrame.TAB_FIELDS,
|
||||
cannotAdvanceKey = "spu_guide_pickField",
|
||||
heading = function(frame, guide)
|
||||
if guide.draft.fieldId == nil then return frame.i18n:getText("spu_guide_field_titleNone") end
|
||||
return string.format(frame.i18n:getText("spu_guide_field_title"), guide.draft.fieldId)
|
||||
end,
|
||||
context = function(frame, adEnv, guide)
|
||||
local fieldId = guide.draft.fieldId
|
||||
if fieldId == nil then return frame:fieldGuideContext(adEnv, nil) end
|
||||
local info = ADFields.getInfo(fieldId)
|
||||
-- marken er solgt/bygget over mens guiden var åben
|
||||
if info == nil or info.cultivable == false then return nil end
|
||||
return frame:fieldGuideContext(adEnv, info, guide.draft.plan, chosenStoreIds(guide))
|
||||
end,
|
||||
choose = function(frame, id, value)
|
||||
if id ~= "field" then return false end
|
||||
local info = ADFields.getInfo(value)
|
||||
if info == nil then return true end
|
||||
local field = ADFieldJobs.getField(value)
|
||||
frame.selectedFieldId = value
|
||||
frame.statusOverride = nil
|
||||
frame.guide = ADFieldGuide.pickField(frame.guide, field, frame:fieldGuideContext(frame.getAdEnv(), info, field.plan, field.plan.storeBuildingIds))
|
||||
return true
|
||||
end,
|
||||
canAdvance = function(guide) return guide.draft.fieldId ~= nil end,
|
||||
startKey = function() return "spu_guide_runAuto" end,
|
||||
-- mål-afgrøde kører altid automatisk: ingen "Gem uden at starte"
|
||||
showSaveOnly = function(guide) return (guide.draft.plan.targetFruit or "") == "" end,
|
||||
save = function(frame, guide, start) frame:saveFieldGuide(guide, start) end,
|
||||
afterRows = function(frame, adEnv, guide)
|
||||
if guide.step ~= "summary" or guide.draft.fieldId == nil then return end
|
||||
local info = guide.context.info
|
||||
if info ~= nil and info.markerId == nil then
|
||||
frame:addTextRow(" " .. string.format(frame.i18n:getText("spu_guide_noMarker"), guide.draft.fieldId))
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- Gem: mål-afgrøde (hvis ændret), planen, og faste maskiner (fjernes fra andre marker).
|
||||
function SmartPickupFrame:saveFieldGuide(guide, start)
|
||||
local result = ADFieldGuide.result(guide, start)
|
||||
if result.fieldId == nil then return end
|
||||
if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
|
||||
ADFieldJobs.setPlan(result.fieldId, result.plan)
|
||||
-- markens form ændret siden kurserne blev lavet: gemte Courseplay-kurser til marken glemmes
|
||||
local fieldInfo = ADFields.getInfo(result.fieldId)
|
||||
if fieldInfo ~= nil and ADCourseCache ~= nil then ADCourseCache.checkField(result.fieldId, fieldInfo.shapeSig) end
|
||||
local byField = SmartPickupFrame.rigIdsByField()
|
||||
for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
|
||||
if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
|
||||
ADFieldJobs.setPlan(fieldId, {rigIds = ids})
|
||||
end
|
||||
end
|
||||
ADFieldJobs.log(result.fieldId, "guide gemt: mode=%s mål=%s faste=%s lager=%s", tostring(result.plan.mode), result.targetFruit ~= "" and result.targetFruit or "-",
|
||||
table.concat(result.rigIds, " "), table.concat(result.plan.storeBuildingIds or {}, " "))
|
||||
self.selectedFieldId = result.fieldId
|
||||
self.statusOverride = string.format(self.i18n:getText("spu_guide_saved"), result.fieldId)
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
end
|
||||
|
||||
-- ---- knapper og [Ret] ----
|
||||
|
||||
-- [Opsæt mark]: den valgte mark med de nuværende valg, ellers trin 1 (vælg mark).
|
||||
function SmartPickupFrame:onClickFieldSetup()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local info = frame.selectedFieldId ~= nil and ADFields.getInfo(frame.selectedFieldId) or nil
|
||||
frame.fieldView = nil
|
||||
if info == nil then
|
||||
frame:openGuide("field", ADFieldGuide.openPicker(frame:fieldGuideContext(adEnv, nil)))
|
||||
return
|
||||
end
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
frame:openGuide("field", ADFieldGuide.open(field, frame:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)))
|
||||
end)
|
||||
end
|
||||
|
||||
-- [Ret] på en resumé-linje i fanens oversigt: kun det ene trin; [Gem] gemmer og går tilbage.
|
||||
function SmartPickupFrame:openFieldStep(fieldId, step)
|
||||
local adEnv = self.getAdEnv()
|
||||
local info = ADFields.getInfo(fieldId)
|
||||
if adEnv == nil or info == nil then return end
|
||||
local field = ADFieldJobs.getField(fieldId)
|
||||
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
|
||||
self:openGuide("field", ADFieldGuide.open(field, context, {step = step, single = true, returnTo = "close"}))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickFieldFleet(show)
|
||||
self:safe(function(frame)
|
||||
frame.fieldView = show and "fleet" or nil
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,117 @@
|
||||
-- Smart Pickup — fanen Marksiloer
|
||||
-- Opsætning (til/fra, maks. vogne, mindste mængde, kør til produktion + foderreserve, gårdlagre) og status for marksilo-flowet.
|
||||
-- Klik går til onClickFieldSiloRow; reglerne ligger i ADFieldSilos/ADFieldSiloPlanner.
|
||||
|
||||
SmartPickupFrame.FIELDSILO_ROW_KINDS = {fsEnabled = true, fsMaxVehicles = true, fsMinLiters = true, fsStore = true,
|
||||
fsToProduction = true, fsReserveDays = true}
|
||||
|
||||
local function numberTexts(from, to)
|
||||
local texts = {}
|
||||
for n = from, to do table.insert(texts, tostring(n)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function literTexts()
|
||||
local texts = {}
|
||||
for _, liters in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do table.insert(texts, string.format("%d l", liters)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function dayTexts(i18n)
|
||||
local texts = {}
|
||||
for _, days in ipairs(ADFieldSiloConfig.RESERVE_DAYS_CHOICES) do table.insert(texts, string.format(i18n:getText("spu_fs_days"), days)) end
|
||||
return texts
|
||||
end
|
||||
|
||||
local function indexOf(list, value)
|
||||
for index, entry in ipairs(list) do if entry == value then return index end end
|
||||
return 1
|
||||
end
|
||||
|
||||
-- Valg med flere værdier: productPrefab (MultiTextOption). ingredientPrefab er en BinaryOption med kun to sider.
|
||||
function SmartPickupFrame:addFieldSiloChoiceRow(kind, title, texts, state)
|
||||
local element = self:addRow(self.productPrefab, {kind = kind})
|
||||
local option = element:getDescendantByName("option")
|
||||
option:setTexts(texts)
|
||||
option:setState(state, false)
|
||||
element:getDescendantByName("title"):setText(title)
|
||||
local tooltip = option:getDescendantByName("ignore")
|
||||
if tooltip ~= nil then tooltip:setText("") end
|
||||
return option
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFieldSiloRows(adEnv)
|
||||
local i18n, cfg = self.i18n, ADFieldSilos.config
|
||||
local onOff = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||
self:addToggleRow("fsEnabled", i18n:getText("spu_fs_enabled"), onOff, cfg.enabled)
|
||||
self:addFieldSiloChoiceRow("fsMaxVehicles", i18n:getText("spu_fs_maxVehicles"), numberTexts(1, ADFieldSiloConfig.MAX_VEHICLES), cfg.maxVehicles)
|
||||
self:addFieldSiloChoiceRow("fsMinLiters", i18n:getText("spu_fs_minLiters"), literTexts(), indexOf(ADFieldSiloConfig.MIN_LITERS_CHOICES, cfg.minLiters))
|
||||
self:addToggleRow("fsToProduction", i18n:getText("spu_fs_toProduction"), onOff, cfg.toProduction)
|
||||
if cfg.toProduction then
|
||||
self:addFieldSiloChoiceRow("fsReserveDays", i18n:getText("spu_fs_reserveDays"), dayTexts(i18n), indexOf(ADFieldSiloConfig.RESERVE_DAYS_CHOICES, cfg.reserveDays))
|
||||
end
|
||||
|
||||
local plan = ADFieldSilos.lastPlan
|
||||
local waiting = 0
|
||||
for _, entry in ipairs(plan ~= nil and plan.status or {}) do if entry.state == "ok" then waiting = waiting + 1 end end
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_status"), ADFieldSilos.loanCount(), cfg.maxVehicles, waiting))
|
||||
if #cfg.storeIds == 0 then self:addTextRow(i18n:getText("spu_fs_noStores")) end
|
||||
local why = ADFieldSilos.lastVehicleReasons
|
||||
if waiting > 0 and ADFieldSilos.loanCount() < cfg.maxVehicles and why ~= nil and why.eligible == 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_noVehicle"), why.noPark, why.busy, why.controlled, why.notEnabled))
|
||||
end
|
||||
|
||||
if why ~= nil and (why.harvest or 0) > 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_fs_harvestHeld"), why.harvest))
|
||||
end
|
||||
|
||||
self:addTextRow(i18n:getText("spu_fs_silosTitle"))
|
||||
if plan == nil or #plan.status == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_fs_nothing"))
|
||||
else
|
||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_fs_col_silo"), i18n:getText("spu_col_crop"), i18n:getText("spu_fs_col_liters"),
|
||||
i18n:getText("spu_fs_col_onWay"), i18n:getText("spu_fs_col_state"), ""})
|
||||
for _, entry in ipairs(plan.status) do
|
||||
local desc = g_fillTypeManager:getFillTypeByIndex(entry.fillType)
|
||||
local title = desc ~= nil and (desc.title or desc.name) or tostring(entry.fillType)
|
||||
local state = i18n:getText("spu_fs_state_" .. entry.state)
|
||||
if entry.state == "noFarmStore" then state = string.format(state, title) end
|
||||
if entry.toProduction then state = state .. " " .. i18n:getText("spu_fs_toFactory") end
|
||||
self:addCellRow(self.fieldPrefab, {tostring(entry.siloName), title, SmartPickupFrame.formatCompact(entry.liters or 0),
|
||||
SmartPickupFrame.formatCompact(entry.onWay or 0), state, ""})
|
||||
end
|
||||
end
|
||||
|
||||
self:addTextRow(i18n:getText("spu_fs_storesTitle"))
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
if building.kind == "silo" then
|
||||
local option = self:addToggleRow("fsStore", tostring(building.name), onOff, ADFieldSiloConfig.hasStore(cfg, building.id))
|
||||
self.rowData[option] = {kind = "fsStore", buildingId = building.id}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickFieldSiloRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local cfg = ADFieldSilos.config
|
||||
if data.kind == "fsEnabled" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {enabled = state == SmartPickupFrame.STATE_ON})
|
||||
elseif data.kind == "fsMaxVehicles" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {maxVehicles = state})
|
||||
elseif data.kind == "fsMinLiters" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {minLiters = ADFieldSiloConfig.MIN_LITERS_CHOICES[state]})
|
||||
elseif data.kind == "fsToProduction" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {toProduction = state == SmartPickupFrame.STATE_ON})
|
||||
elseif data.kind == "fsReserveDays" then
|
||||
cfg = ADFieldSiloConfig.with(cfg, {reserveDays = ADFieldSiloConfig.RESERVE_DAYS_CHOICES[state]})
|
||||
elseif data.kind == "fsStore" then
|
||||
cfg = ADFieldSiloConfig.toggleStore(cfg, data.buildingId)
|
||||
end
|
||||
ADFieldSilos.setConfig(cfg)
|
||||
pcall(ADFieldSilos.save)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv ~= nil then pcall(ADFieldSilos.planNow, adEnv) end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -1,30 +1,20 @@
|
||||
-- Smart Pickup — fanen Marker
|
||||
-- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række
|
||||
-- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin,
|
||||
-- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben.
|
||||
-- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView.
|
||||
-- Oversigt: tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en
|
||||
-- række viser markens resumé (én linje pr. emne — klik på en linje for at rette netop det trin), hele
|
||||
-- statusteksten og markøren. [Opsæt mark] åbner guiden (gui/SmartPickupFieldGuide.lua + SmartPickupGuide.lua),
|
||||
-- [Markflåde] viser den fælles markflåde. Al logik ligger i ADFieldJobs/ADFieldModel/ADFieldView/ADFieldGuide.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.FIELD_ROW_KINDS = {
|
||||
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
|
||||
fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
|
||||
fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
|
||||
}
|
||||
-- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow.
|
||||
SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true, fleetManual = true}
|
||||
SmartPickupFrame.ROLE_KEYS = {
|
||||
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
|
||||
collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader",
|
||||
mulcher = "spu_field_role_mulcher", limer = "spu_field_role_limer", plow = "spu_field_role_plow",
|
||||
cultivator = "spu_field_role_cultivator", seeder = "spu_field_role_seeder", roller = "spu_field_role_roller",
|
||||
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
|
||||
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
|
||||
}
|
||||
SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
||||
-- simple Ja/Nej-valg på planen: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_TOGGLES = {
|
||||
fieldRake = "rake", fieldMulch = "mulch", fieldLime = "lime", fieldSow = "sow", fieldRoll = "roll",
|
||||
fieldFertilize = "fertilize", fieldWeed = "weed",
|
||||
}
|
||||
-- valg med flere muligheder: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit"}
|
||||
|
||||
-- ---- tekster ----
|
||||
|
||||
@@ -50,11 +40,32 @@ function SmartPickupFrame:soilText(info)
|
||||
return table.concat(texts, ", ")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldStatusText(field, info)
|
||||
-- Kort årsag til tabellens statuskolonne ("bredde ukendt" i stedet for hele forklaringen, som står i
|
||||
-- markens detaljer). Fritekst-årsager (fx "… er tom for X") forkortes til "se detaljer".
|
||||
function SmartPickupFrame:fieldReasonShort(reason)
|
||||
if type(reason) ~= "table" then return nil end
|
||||
if reason.text ~= nil then return self.i18n:getText("spu_field_reasonShort_text") end
|
||||
local shortKey = reason.key ~= nil and string.gsub(reason.key, "^spu_field_reason_", "spu_field_reasonShort_") or nil
|
||||
if shortKey ~= nil then
|
||||
local okHas, has = pcall(function() return self.i18n.hasText == nil or self.i18n:hasText(shortKey) end)
|
||||
if okHas and has then return self.i18n:getText(shortKey) end
|
||||
end
|
||||
return self:fieldReasonText(reason)
|
||||
end
|
||||
|
||||
-- short = tabellens korte udgave (trin + kort årsag); ellers hele teksten (markens detaljer).
|
||||
function SmartPickupFrame:fieldStatusText(field, info, short)
|
||||
local text = self.i18n:getText(ADFieldView.statusKey(field, info))
|
||||
if info.markerId == nil then
|
||||
return string.format(text, info.id)
|
||||
end
|
||||
if short then
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||
end
|
||||
local reason = self:fieldReasonShort(field.reason)
|
||||
return reason ~= nil and (text .. ": " .. reason) or text
|
||||
end
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||
end
|
||||
@@ -62,12 +73,24 @@ function SmartPickupFrame:fieldStatusText(field, info)
|
||||
if decision ~= nil and decision.care ~= nil and not ADFieldModel.isWorking(field) then
|
||||
text = text .. " — " .. string.format(self.i18n:getText("spu_field_careNeed"), self.i18n:getText("spu_field_stage_" .. decision.care))
|
||||
end
|
||||
local target = field.plan.targetFruit
|
||||
if target ~= nil and target ~= "" and info.kind == "bare" and field.stage == "idle" and not ADFieldJobs.environment().plantable(target) then
|
||||
text = text .. " — " .. string.format(self.i18n:getText("spu_field_waitSowing"), self:fruitTitle(target))
|
||||
end
|
||||
local reason = self:fieldReasonText(field.reason)
|
||||
if reason ~= nil then text = text .. ": " .. reason end
|
||||
return text
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fruitTitle(name)
|
||||
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldPlanText(field, info)
|
||||
if field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
||||
return "-> " .. self:fruitTitle(field.plan.targetFruit)
|
||||
end
|
||||
local text = self.i18n:getText(ADFieldView.planKey(field.plan, info.kind))
|
||||
if field.plan.rake and info.kind == "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_rake") end
|
||||
if field.plan.sow and info.kind ~= "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_sow") end
|
||||
@@ -81,187 +104,135 @@ function SmartPickupFrame:fieldCells(row)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
local name = string.format("%s%sF%d", info.id == self.selectedFieldId and "> " or "", info.markerId == nil and "! " or "", info.id)
|
||||
local crop = info.kind == "bare" and self.i18n:getText("spu_field_bare") or (info.cropTitle or "-")
|
||||
local name = string.format("%sF%d", info.markerId == nil and "! " or "", info.id)
|
||||
local crop = ADFieldView.cropText(info, function(key) return self.i18n:getText(key) end)
|
||||
return {
|
||||
name, crop, self:fieldPlanText(field, info),
|
||||
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
||||
self:fieldStatusText(field, info),
|
||||
self:fieldStatusText(field, info, true) .. ((ADFieldJobs.state.paused and not ADFieldJobs.isFieldResumed(info.id)
|
||||
and ADFieldModel.isWorking(field)) and (" " .. self.i18n:getText("spu_field_pausedSuffix")) or ""),
|
||||
ADFlowView.joinCounted(names, ", ", "-"),
|
||||
self:fieldActionText(info.id),
|
||||
}
|
||||
end
|
||||
|
||||
-- ---- rækker ----
|
||||
|
||||
-- Valg med flere muligheder (klon af productPrefab): values[i] er værdien for texts[i].
|
||||
function SmartPickupFrame:addChoiceRow(kind, title, texts, values, current)
|
||||
local row = self:addRow(self.productPrefab, {kind = kind, values = values})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts(texts)
|
||||
local index = 1
|
||||
for position, value in ipairs(values) do
|
||||
if value == current then index = position end
|
||||
end
|
||||
option:setState(index, false)
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
return option
|
||||
end
|
||||
|
||||
local function numberTexts(from, to)
|
||||
local texts, values = {}, {}
|
||||
for value = from, to do
|
||||
table.insert(texts, tostring(value))
|
||||
table.insert(values, value)
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- Afgrøder der kan sås (første valg: samme som den høstede).
|
||||
function SmartPickupFrame:sowFruitChoices()
|
||||
local texts, values = {self.i18n:getText("spu_field_sowSame")}, {""}
|
||||
for _, fruit in ipairs(g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}) do
|
||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
||||
table.insert(texts, tostring(fruit.fillType.title or fruit.name))
|
||||
table.insert(values, fruit.name)
|
||||
end
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- ---- opbygning ----
|
||||
|
||||
function SmartPickupFrame:fillFieldRows(adEnv)
|
||||
self.fieldRows = {}
|
||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FIELDS) and self:fillGuideRows(adEnv) then return end
|
||||
if self.fieldView == "fleet" then
|
||||
self:fillFleetRows(adEnv)
|
||||
return
|
||||
end
|
||||
local i18n = self.i18n
|
||||
local infos = ADFields.list()
|
||||
if #infos == 0 then
|
||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||
else
|
||||
self:addTextRow(i18n:getText("spu_field_hint"))
|
||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors")})
|
||||
-- Stop alt-pausen gemmes med savegamet: sig det tydeligt her (in-game 29/9: "jeg har ikke trykket stop")
|
||||
if ADFieldJobs.state.paused then self:addTextRow(i18n:getText("spu_field_pausedBanner")) end
|
||||
-- markflåden på "kun med flueben" uden flueben: intet kan sendes ud (in-game 26/9 — alle marker "no machine")
|
||||
if ADFieldJobs.state.fleetMode == "ticked" and next(ADFieldJobs.state.fleet or {}) == nil then
|
||||
self:addTextRow(i18n:getText("spu_field_fleetEmpty"))
|
||||
end
|
||||
self:addCellRow(self.fieldListPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors"), ""})
|
||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields)) do
|
||||
local cells = self:addCellRow(self.fieldPrefab, self:fieldCells(row))
|
||||
local cells = self:addCellRow(self.fieldListPrefab, self:fieldCells(row))
|
||||
local fieldId = row.info.id
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
local actionCell = cells[7]
|
||||
self:makeRowClickable(cells[1].parent, function(frame)
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:requestRebuild()
|
||||
-- klik på [Start]/[Stop]-feltet = handling (kun denne mark); ellers åbn redigeringsvinduet
|
||||
if frame.lastClickX ~= nil and actionCell ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||
frame:toggleField(fieldId)
|
||||
else
|
||||
frame.selectedFieldId = fieldId
|
||||
frame:openEditPanel("field", fieldId)
|
||||
end
|
||||
end)
|
||||
end
|
||||
table.insert(self.fieldRows, {id = fieldId, cells = cells})
|
||||
end
|
||||
end
|
||||
local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||
if selected ~= nil then
|
||||
self:fillFieldDetail(adEnv, selected)
|
||||
end
|
||||
self:fillFleetRows(adEnv)
|
||||
self:refreshFieldLevels()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFieldDetail(adEnv, info)
|
||||
-- Prik og statusfarve pr. mark i listen.
|
||||
function SmartPickupFrame:refreshFieldLevels()
|
||||
local unpackFn = table.unpack or unpack
|
||||
for _, entry in ipairs(self.fieldRows or {}) do
|
||||
local color = ADFlowView.LEVEL_COLORS[self:fieldLevel(entry.id)]
|
||||
local row = entry.cells[1] ~= nil and entry.cells[1].parent or nil
|
||||
local dot = row ~= nil and row:getDescendantByName("dot") or nil
|
||||
if dot ~= nil and dot.setImageColor ~= nil then dot:setImageColor(nil, unpackFn(color)) end
|
||||
if entry.cells[5] ~= nil and entry.cells[5].setTextColor ~= nil then entry.cells[5]:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Arbejder marken reelt (ikke bare et plan-trin under Stop alt-pause)?
|
||||
function SmartPickupFrame:fieldIsActive(id, field)
|
||||
field = field or ADFieldJobs.getField(id)
|
||||
return field ~= nil and ADFieldView.isActiveWork({working = ADFieldModel.isWorking(field),
|
||||
paused = ADFieldJobs.state.paused == true, resumed = ADFieldJobs.isFieldResumed(id)})
|
||||
end
|
||||
|
||||
-- [Start] kræver en markør (Stop/Overtag altid muligt når marken arbejder).
|
||||
function SmartPickupFrame:fieldCanStart(id)
|
||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
||||
if field == nil or info == nil then return false end
|
||||
return ADFieldView.canStart({hasMarker = info.markerId ~= nil,
|
||||
working = self:fieldIsActive(id, field) or ADFieldJobs.awaitingPlayer(id) ~= nil})
|
||||
end
|
||||
|
||||
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),
|
||||
paused = ADFieldJobs.state.paused == true, resumed = ADFieldJobs.isFieldResumed(id),
|
||||
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil,
|
||||
startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id) or false,
|
||||
reasonKey = reason})
|
||||
end
|
||||
|
||||
-- forButton: knaplinjens tekst (Start/Stop) i stedet for listens [Start]/[Stop].
|
||||
function SmartPickupFrame:fieldActionText(id, forButton)
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
|
||||
local isWorking = self:fieldIsActive(id, field)
|
||||
if forButton then return self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start") end
|
||||
return self.i18n:getText(isWorking and "spu_flowList_stop" or "spu_flowList_start")
|
||||
end
|
||||
|
||||
-- Den valgte marks resumé: én klikbar linje pr. emne ([Ret] åbner kun det trin), hele status, markør og
|
||||
-- advarsel om marksilo der ikke tømmes hjem.
|
||||
function SmartPickupFrame:fillFieldOverview(adEnv, info)
|
||||
local i18n = self.i18n
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
local plan = field.plan
|
||||
local yesNo = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||
local crop = info.kind == "bare" and i18n:getText("spu_field_bare") or tostring(info.cropTitle or "-")
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
|
||||
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
|
||||
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
|
||||
if info.kind == "mowable" then
|
||||
local isHay = ADFieldModel.product(plan, "mowable") == "hay"
|
||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
|
||||
else
|
||||
if info.kind == "harvest" then
|
||||
local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
|
||||
self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
|
||||
end
|
||||
self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
|
||||
self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
|
||||
local soilTexts = {}
|
||||
for _, value in ipairs(SmartPickupFrame.SOIL_VALUES) do table.insert(soilTexts, i18n:getText("spu_field_soil_" .. value)) end
|
||||
self:addChoiceRow("fieldSoil", i18n:getText("spu_field_soil"), soilTexts, SmartPickupFrame.SOIL_VALUES, plan.soil or "auto")
|
||||
self:addToggleRow("fieldSow", i18n:getText("spu_field_sow"), yesNo, plan.sow == true)
|
||||
if plan.sow then
|
||||
local fruitTexts, fruitValues = self:sowFruitChoices()
|
||||
self:addChoiceRow("fieldSowFruit", i18n:getText("spu_field_sowFruit"), fruitTexts, fruitValues, plan.sowFruit or "")
|
||||
self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
|
||||
end
|
||||
local crop = ADFieldView.cropText(info, function(key) return i18n:getText(key) end)
|
||||
if not self.editPanelOpen then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_field_section_care"))
|
||||
self:addToggleRow("fieldFertilize", i18n:getText("spu_field_fertilize"), yesNo, plan.fertilize ~= false)
|
||||
self:addToggleRow("fieldLime", i18n:getText("spu_field_lime"), yesNo, plan.lime ~= false)
|
||||
if info.kind ~= "mowable" then
|
||||
self:addToggleRow("fieldWeed", i18n:getText("spu_field_weed"), yesNo, plan.weed ~= false)
|
||||
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
|
||||
local guide = ADFieldGuide.open(field, context)
|
||||
local getText = function(key) return i18n:getText(key) end
|
||||
local fieldId = info.id
|
||||
for _, line in ipairs(ADFieldGuide.summary(guide)) do
|
||||
local step = line.step
|
||||
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||
frame:openFieldStep(fieldId, step)
|
||||
end)
|
||||
end
|
||||
local soil = self:soilText(info)
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_soilState"), soil ~= "" and soil or i18n:getText("spu_field_soilOk")))
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), self:fieldStatusText(field, info)))
|
||||
if info.markerId ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
|
||||
else
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_noMarker"), info.id))
|
||||
end
|
||||
if field.mowWidth ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_mowWidth"), field.mowWidth))
|
||||
end
|
||||
if field.plan.rake and field.windrowOffset ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_windrowOffset"), math.abs(field.windrowOffset),
|
||||
i18n:getText(math.abs(field.windrowOffset) <= 0.05 and "spu_field_side_center" or (field.windrowOffset > 0 and "spu_field_side_left" or "spu_field_side_right"))))
|
||||
end
|
||||
self:fillFieldStoreRows(adEnv, info, field)
|
||||
end
|
||||
|
||||
-- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer.
|
||||
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
local i18n = self.i18n
|
||||
local product = ADFieldModel.product(field.plan, info.kind)
|
||||
local fillType
|
||||
if info.kind == "mowable" then
|
||||
fillType = product == "hay" and info.hay or info.windrow
|
||||
else
|
||||
fillType = info.harvestFillType
|
||||
end
|
||||
if fillType == nil then return end
|
||||
self:addTextRow(i18n:getText("spu_field_section_store"))
|
||||
local chosen = {}
|
||||
for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local fieldSilos = {}
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do fieldSilos[building.id] = building end
|
||||
local fermenterIds = {}
|
||||
if info.kind == "mowable" and product == "silage" then
|
||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
||||
end
|
||||
local count = 0
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
||||
count = count + 1
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "fieldStore", buildingId = building.id})
|
||||
local suffix = fieldSilos[building.id] ~= nil and (" (" .. i18n:getText("spu_field_fieldSilo") .. ")") or ""
|
||||
if fermenterIds[building.id] then suffix = suffix .. " (" .. i18n:getText("spu_field_fermenter") .. ")" end
|
||||
row:getDescendantByName("title"):setText(" " .. tostring(building.name) .. suffix)
|
||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
if count == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_noStore"))
|
||||
elseif #field.plan.storeBuildingIds == 0 then
|
||||
self:addTextRow(" " .. i18n:getText((info.kind == "mowable" and product == "silage") and "spu_field_storeAutoSilage" or "spu_field_storeAuto"))
|
||||
end
|
||||
-- marksiloer skal tømmes hjem af et udkørselsflow
|
||||
for id, building in pairs(fieldSilos) do
|
||||
local emptied = false
|
||||
for _, run in ipairs(ADRunsController.store.runs) do
|
||||
for _, sourceId in ipairs(run.kind == "outbound" and run.sourceBuildingIds or {}) do
|
||||
if sourceId == id then emptied = true end
|
||||
end
|
||||
end
|
||||
if not emptied then
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_fieldSiloNotEmptied"), tostring(building.name)))
|
||||
end
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_guide_noMarker"), info.id))
|
||||
end
|
||||
-- marksilo-advarsler vises ikke her: de gælder ikke marken, og Field Silos-fanen tømmer marksiloerne selv
|
||||
end
|
||||
|
||||
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
||||
@@ -277,16 +248,28 @@ function SmartPickupFrame:rigDetails(rig)
|
||||
elseif rig.role == "weeder" then
|
||||
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
||||
end
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||
-- samme grænse som udsendelsen (50,0 m med afrunding er ikke "over 50 m")
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||
if (rig.width or 0) < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[rig.role] then table.insert(parts, i18n:getText("spu_field_widthUnknownShort")) end
|
||||
-- fast på en mark (valgt under markens "Faste maskiner") og hvor den arbejder nu
|
||||
for fieldId, field in pairs(ADFieldJobs.state.fields) do
|
||||
for _, id in ipairs(field.plan ~= nil and field.plan.rigIds or {}) do
|
||||
if id == rig.id then table.insert(parts, string.format(i18n:getText("spu_field_dedicatedElsewhere"), tostring(fieldId))) end
|
||||
end
|
||||
end
|
||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||
if assignment ~= nil then table.insert(parts, string.format("F%d", assignment.fieldId)) end
|
||||
if assignment ~= nil then table.insert(parts, string.format(i18n:getText("spu_field_rig_working"), tostring(assignment.fieldId))) end
|
||||
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
|
||||
end
|
||||
|
||||
-- Markflåden (egen visning): alle maskiner med flueben, "fast på F22", "i gang på F108", bredde-advarsler.
|
||||
function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_field_section_fleet"))
|
||||
local rigs = ADFieldJobs.getRigs(adEnv)
|
||||
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
|
||||
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
|
||||
local rigs = {}
|
||||
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do table.insert(rigs, rig) end
|
||||
table.sort(rigs, function(a, b)
|
||||
if a.role ~= b.role then return a.role < b.role end
|
||||
return ADFlowView.naturalLess(a.label, b.label)
|
||||
@@ -296,15 +279,55 @@ function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
local title = string.format(" %s — %s, %.1f m%s", tostring(rig.label), self:roleText(rig.role), rig.width or 0, self:rigDetails(rig))
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
row:getDescendantByName("option"):setState(rig.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
if rig.role == "harvester" then
|
||||
-- Ja = må køre selv på vejen (som resten af flåden: ja = må sendes ud); Nej = manuel transport —
|
||||
-- for bred med skærebord, spilleren kører den til marken (in-game 27/9: omvendt betydning forvirrede)
|
||||
local option = self:addToggleRow("fleetManual", string.format(i18n:getText("spu_field_manualTransport"), tostring(rig.label)),
|
||||
{i18n:getText("ui_no"), i18n:getText("ui_yes")}, not rig.manual)
|
||||
self.rowData[option] = {kind = "fleetManual", vehicleId = rig.id}
|
||||
end
|
||||
end
|
||||
if #rigs == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
|
||||
end
|
||||
if ADPool ~= nil and ADPool.overview ~= nil then self:fillPoolRows(adEnv) end
|
||||
end
|
||||
|
||||
-- Maskinpuljen: solo-traktorer (flueben som resten af flåden) og redskaber med status og hvorfor de ikke er med.
|
||||
function SmartPickupFrame:fillPoolRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
local ok, implements, tractors = pcall(ADPool.overview, adEnv)
|
||||
if not ok then return end
|
||||
local byLabel = function(a, b) return ADFlowView.naturalLess(a.label, b.label) end
|
||||
table.sort(tractors, byLabel)
|
||||
table.sort(implements, function(a, b)
|
||||
if a.role ~= b.role then return a.role < b.role end
|
||||
return byLabel(a, b)
|
||||
end)
|
||||
self:addTextRow(i18n:getText("spu_pool_section_tractors"))
|
||||
for _, tractor in ipairs(tractors) do
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
||||
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg))
|
||||
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", tostring(tractor.label), hp, status))
|
||||
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
||||
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
||||
for _, implement in ipairs(implements) do
|
||||
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg))
|
||||
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status))
|
||||
end
|
||||
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
||||
end
|
||||
|
||||
-- ---- live ----
|
||||
|
||||
function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
if (not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FIELDS)) or self.fieldView == "fleet" then
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
return
|
||||
end
|
||||
local rowsById = {}
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
|
||||
local working, total = 0, 0
|
||||
@@ -313,43 +336,32 @@ function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
if row ~= nil then
|
||||
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
||||
total = total + 1
|
||||
if ADFieldModel.isWorking(row.field) then working = working + 1 end
|
||||
if self:fieldIsActive(entry.id, row.field) then working = working + 1 end
|
||||
end
|
||||
end
|
||||
self:refreshFieldLevels()
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_field_status"), working, total))
|
||||
local selected = self.selectedFieldId ~= nil and ADFieldJobs.getField(self.selectedFieldId) or nil
|
||||
local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||
local isWorking = selected ~= nil and ADFieldModel.isWorking(selected)
|
||||
self.fieldStartButtonInfo.text = self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start")
|
||||
local awaiting = self.selectedFieldId ~= nil and ADFieldJobs.awaitingPlayer(self.selectedFieldId) ~= nil
|
||||
self.fieldStartButtonInfo.text = self.i18n:getText(awaiting and "spu_field_button_takeover"
|
||||
or (isWorking and "spu_field_button_stop" or "spu_field_button_start"))
|
||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
|
||||
end
|
||||
|
||||
-- ---- klik ----
|
||||
-- ---- klik (markflåden) ----
|
||||
|
||||
function SmartPickupFrame:onClickFieldRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local isOn = state == SmartPickupFrame.STATE_ON
|
||||
local id = frame.selectedFieldId
|
||||
if data.kind == "fleetVehicle" then
|
||||
ADFieldJobs.toggleFleet(data.vehicleId)
|
||||
elseif id == nil then
|
||||
return
|
||||
elseif SmartPickupFrame.PLAN_CHOICES[data.kind] ~= nil then
|
||||
local value = data.values ~= nil and data.values[state] or nil
|
||||
if value ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_CHOICES[data.kind]] = value}) end
|
||||
elseif data.kind == "fieldProduct" then
|
||||
ADFieldJobs.setPlan(id, {product = isOn and "hay" or "silage"})
|
||||
elseif data.kind == "fieldMode" then
|
||||
ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"})
|
||||
elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then
|
||||
ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn})
|
||||
elseif data.kind == "fieldStore" then
|
||||
local ids = {}
|
||||
for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do
|
||||
if buildingId ~= data.buildingId then table.insert(ids, buildingId) end
|
||||
end
|
||||
if isOn then table.insert(ids, data.buildingId) end
|
||||
ADFieldJobs.setPlan(id, {storeBuildingIds = ids})
|
||||
elseif data.kind == "fleetMode" then
|
||||
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
|
||||
elseif data.kind == "fleetManual" then
|
||||
local wantManual = not isOn -- Ja = kører selv på vejen
|
||||
if ((ADFieldJobs.state.manual or {})[data.vehicleId] == true) ~= wantManual then ADFieldJobs.toggleManual(data.vehicleId) end
|
||||
end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
@@ -357,13 +369,29 @@ end
|
||||
|
||||
-- Start nu: pleje hvis marken trænger (og vokser), ellers hele kæden (høst/slå, eller klargøring på bar mark).
|
||||
function SmartPickupFrame:onClickFieldStartStop()
|
||||
self:toggleField(self.selectedFieldId)
|
||||
end
|
||||
|
||||
-- Start/Stop/Overtag for én mark (rækkens felt, vinduets knap og knaplinjen).
|
||||
function SmartPickupFrame:toggleField(id)
|
||||
self:safe(function(frame)
|
||||
local id = frame.selectedFieldId
|
||||
if id == nil then return end
|
||||
if id == nil or not frame:fieldCanStart(id) then return end
|
||||
local field = ADFieldJobs.getField(id)
|
||||
if ADFieldModel.isWorking(field) then
|
||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then
|
||||
-- manuel transport: overtag nu (skærebordet tjekkes stadig)
|
||||
ADFieldJobs.requestTakeover(id)
|
||||
frame.statusOverride = string.format(frame.i18n:getText("spu_field_takeoverRequested"), id)
|
||||
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
elseif frame:fieldIsActive(id, field) then
|
||||
ADFieldJobs.stopField(frame.getAdEnv(), id)
|
||||
ADFieldJobs.setFieldResumed(id, false)
|
||||
elseif ADFieldJobs.state.paused and ADFieldModel.isWorking(field) then
|
||||
-- Stop alt-pause: kun denne mark genoptages fra sit trin (de andre venter på Start alle)
|
||||
ADFieldJobs.setFieldResumed(id, true)
|
||||
frame.statusOverride = string.format(frame.i18n:getText("spu_field_startRequested"), id)
|
||||
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
else
|
||||
if ADFieldJobs.state.paused then ADFieldJobs.setFieldResumed(id, true) end
|
||||
local decision = ADFieldJobs.lastDecision[id]
|
||||
if decision == nil or decision.care == nil then
|
||||
ADFieldJobs.forceReady(id)
|
||||
@@ -375,3 +403,25 @@ function SmartPickupFrame:onClickFieldStartStop()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Redigeringsvinduet for en mark (gui/SmartPickupEditPanel.lua).
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
||||
canDelete = false,
|
||||
exists = function(frame, id) return ADFields.getInfo(id) ~= nil and ADFieldJobs.getField(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))
|
||||
frame:addLinkRow(" " .. frame.i18n:getText("spu_guide_field_open"), function(f)
|
||||
f.selectedFieldId = id
|
||||
f:onClickFieldSetup()
|
||||
end)
|
||||
end,
|
||||
status = function(frame, adEnv, id)
|
||||
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), true), level = frame:fieldLevel(id)}
|
||||
end,
|
||||
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id, true) end,
|
||||
startStop = function(frame, id) frame:toggleField(id) end,
|
||||
canStart = function(frame, id) return frame:fieldCanStart(id) end,
|
||||
delete = function() end,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
-- Smart Pickup — fanen Flows: tavle + resumé, og guiden "Opsæt flow"
|
||||
-- Tavlen øverst (én række pr. flow), det valgte flows resumé (klik på en linje = ret ét trin; Navn = omdøb)
|
||||
-- og status. [Opsæt flow] / [Nyt flow] åbner guiden på den fælles trin-skærm (gui/SmartPickupGuide.lua).
|
||||
-- Henter det spil-afhængige som `context` til ADFlowGuide og gemmer via ADRunsController.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
function SmartPickupFrame:initFlowButtons(i18n)
|
||||
self.flowSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_flow_open"), callback = function() self:onClickFlowSetup() end}
|
||||
self.flowNewButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_new"), callback = function() self:onClickFlowNew() end}
|
||||
-- Flows-fanen: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; omdøb ligger på resuméets Navn-linje
|
||||
-- v1.58: listen har Start/Stop pr. række; Opsæt/Slet ligger i redigeringsvinduet
|
||||
self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}
|
||||
end
|
||||
|
||||
-- ---- traktor-tekst (også til guidens traktorliste) ----
|
||||
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — OK" / "… — X ingen vogn".
|
||||
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local parts = {label}
|
||||
if validation.isAutoload and validation.capacity == nil then
|
||||
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
|
||||
elseif validation.capacity ~= nil then
|
||||
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
|
||||
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
|
||||
if carriable ~= nil then
|
||||
table.insert(parts, carriable)
|
||||
end
|
||||
end
|
||||
if validation.ok then
|
||||
table.insert(parts, "OK")
|
||||
else
|
||||
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
|
||||
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
|
||||
reason = string.format(reason, tostring(validation.missingBuilding))
|
||||
end
|
||||
table.insert(parts, "X " .. reason)
|
||||
end
|
||||
return table.concat(parts, " — ")
|
||||
end
|
||||
|
||||
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
|
||||
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
|
||||
local canCarry = {}
|
||||
for _, fillType in ipairs(carriable or {}) do
|
||||
canCarry[fillType] = true
|
||||
end
|
||||
local titles = {}
|
||||
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
|
||||
if canCarry[fillType] then
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||||
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
|
||||
end
|
||||
end
|
||||
if #titles == 0 then
|
||||
return nil
|
||||
end
|
||||
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
|
||||
end
|
||||
|
||||
-- ---- context til ADFlowGuide ----
|
||||
|
||||
local function trim(text)
|
||||
return (string.gsub(tostring(text or ""), "^%s+", ""))
|
||||
end
|
||||
|
||||
local function idSet(list)
|
||||
local set = {}
|
||||
for _, id in ipairs(list or {}) do set[id] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowBuildingContext(adEnv, draft, farmId)
|
||||
local isOut = draft.kind == "outbound"
|
||||
local role = isOut and "load" or "unload"
|
||||
local chosen = idSet(isOut and draft.sourceBuildingIds or draft.targetBuildingIds)
|
||||
local buildings, found = {}, {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local fits = (isOut and building.canBeSource) or (not isOut and building.canBeTarget)
|
||||
if fits or chosen[building.id] then
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}
|
||||
found[building.id] = true
|
||||
table.insert(buildings, {id = building.id, name = tostring(building.name), label = markers[1] ~= nil and tostring(markers[1].name) or nil,
|
||||
hasMarker = markers[1] ~= nil, text = trim(self:describeBuilding(building, role))})
|
||||
end
|
||||
end
|
||||
-- valgte bygninger der ikke længere findes (solgt): kan fravælges
|
||||
for id in pairs(chosen) do
|
||||
if not found[id] then
|
||||
local gone = self.i18n:getText("spu_buildingGone")
|
||||
table.insert(buildings, {id = id, name = gone, hasMarker = false, text = gone})
|
||||
end
|
||||
end
|
||||
return buildings
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowStoreContext(adEnv, preview, farmId)
|
||||
local goods = ADRunsController.getStoreGoods(adEnv, preview)
|
||||
local chosen = idSet(preview.storeBuildingIds)
|
||||
local result = {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local isStoreKind = building.kind == "silo" or building.kind == "factory" or building.kind == "objectStorage"
|
||||
if isStoreKind and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, goods)) then
|
||||
table.insert(result, {id = building.id, name = tostring(building.name), text = trim(self:describeBuilding(building, "unload"))})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Varer (fillType-indeks) flowet kører med: udkørsel = valgte produkter, forsyning = valgte/målets varer.
|
||||
-- Tom liste = ukendt endnu (ingen mål valgt) -> enhver vogn med last-enheder tæller.
|
||||
local function flowFillTypes(adEnv, preview)
|
||||
local names = {}
|
||||
if preview.kind == "outbound" then
|
||||
for _, product in ipairs(preview.products or {}) do table.insert(names, product.fillType) end
|
||||
elseif preview.ingredientMode == "manual" then
|
||||
names = preview.ingredients or {}
|
||||
else
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do table.insert(names, fillType.name) end
|
||||
end
|
||||
local result = {}
|
||||
for _, name in ipairs(names) do
|
||||
local index = g_fillTypeManager:getFillTypeIndexByName(tostring(name))
|
||||
if index ~= nil then table.insert(result, index) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function carriesAny(carriable, wanted)
|
||||
if #carriable == 0 then return false end
|
||||
if #wanted == 0 then return true end
|
||||
for _, a in ipairs(carriable) do
|
||||
for _, b in ipairs(wanted) do if a == b then return true end end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Ledige vogne til solo-traktorer: frakoblet, egen AD-parkering med "Connect"-markør, ikke holdt af et andet flow
|
||||
-- og kan bære flowets varer. Bedste først (navnematch, så "Disponibel"). In-game 29/9: flowet kunne ikke oprettes
|
||||
-- uden at omdøbe en vogns parkering til flowets navn.
|
||||
function SmartPickupFrame:flowWagonContext(adEnv, preview)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||
local wanted = flowFillTypes(adEnv, preview)
|
||||
local candidates = {}
|
||||
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||
local ok, entry = pcall(function()
|
||||
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||
if any.spec_motorized ~= nil or any.spec_attachable == nil or type(index) ~= "number" or index < 1 then return nil end
|
||||
if any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil then return nil end
|
||||
if any.getOwnerFarmId == nil or any:getOwnerFarmId() ~= farmId then return nil end
|
||||
local id = ADRunsController.getVehicleId(any)
|
||||
local park = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
if id == nil or park == nil or ADPool.connectFor(park.name, markers) == nil then return nil end
|
||||
local owner = ADFlowWagons.ownerOf(ADRunsController.store.runs, id)
|
||||
if owner ~= nil and owner.id ~= preview.id then return nil end
|
||||
if not carriesAny(ADRunsController.getCarriableFillTypes(adEnv, any), wanted) then return nil end
|
||||
return {id = id, label = tostring(park.name)}
|
||||
end)
|
||||
if ok and entry ~= nil then table.insert(candidates, entry) end
|
||||
end
|
||||
return ADFlowWagons.rankWagons(preview.name, candidates)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:flowVehicleContext(adEnv, preview, wagons)
|
||||
local result = {}
|
||||
local hasWagons = wagons ~= nil and #wagons > 0
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local ok, entry = pcall(function()
|
||||
local text = self:describeVehicle(adEnv, preview, vehicle, false)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, preview, vehicle)
|
||||
local other = ADRunsController.getRunForVehicle(vehicle)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
-- solo-traktor (ingen vogn bagpå, kan trække): valgbar når en ledig vogn findes — den hentes ved Connect
|
||||
local solo = validation.reason == "noTrailer" and ADPool.canPull(ADPool.ownRole(vehicle))
|
||||
if solo and hasWagons then text = string.format(self.i18n:getText("spu_guide_flow_soloText"), tostring(label)) end
|
||||
local blocked = SmartPickupFrame.VEHICLE_BLOCKING_REASONS[validation.reason] == true and not (solo and hasWagons)
|
||||
return {id = ADRunsController.getVehicleId(vehicle), text = text, label = label, otherRunId = other ~= nil and other.id or nil,
|
||||
otherRunName = other ~= nil and other.name or nil, blocked = blocked, solo = solo}
|
||||
end)
|
||||
if ok and entry.id ~= nil then
|
||||
table.insert(result, entry)
|
||||
elseif not ok then
|
||||
Logging.warning(SmartPickupFrame.LOG_PREFIX .. "traktor '%s' kunne ikke vises i flowets traktorliste: %s",
|
||||
tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), tostring(entry))
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- draft: kladden (eller selve run'et — samme feltnavne).
|
||||
function SmartPickupFrame:flowGuideContext(adEnv, draft)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local preview = ADFlowGuide.previewRun(draft, function(buildingId, role)
|
||||
local building = ADBuildings.find(adEnv, farmId, buildingId)
|
||||
local ids = {}
|
||||
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}) do
|
||||
table.insert(ids, marker.id)
|
||||
end
|
||||
return ids
|
||||
end)
|
||||
local context = {buildings = self:flowBuildingContext(adEnv, draft, farmId), fillTypes = {}, products = {}, storeBuildings = {},
|
||||
waitPools = ADRunsController.getWaitPools(adEnv)}
|
||||
context.wagons = self:flowWagonContext(adEnv, preview)
|
||||
context.vehicles = self:flowVehicleContext(adEnv, preview, context.wagons)
|
||||
if draft.kind == "outbound" then
|
||||
for _, product in ipairs(ADRunsController.getRunProducts(adEnv, preview)) do
|
||||
local title = tostring(product.title)
|
||||
table.insert(context.products, {name = product.name, title = title,
|
||||
text = string.format("%s — %s l", title, SmartPickupFrame.formatCompact(product.available or 0))})
|
||||
end
|
||||
context.storeBuildings = self:flowStoreContext(adEnv, preview, farmId)
|
||||
else
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do
|
||||
table.insert(context.fillTypes, {name = fillType.name, title = tostring(fillType.title)})
|
||||
end
|
||||
end
|
||||
return context
|
||||
end
|
||||
|
||||
-- ---- fanen: tavle + resumé ----
|
||||
|
||||
-- Flows: det valgte flow øverst (indstillinger som "ret"-linjer + status), derunder en kort liste til at vælge
|
||||
-- et andet flow. Tavlen med alle flows står i Oversigt (in-game 27/9: de to faner så ens ud).
|
||||
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 (kun dette flow); ellers åbn redigeringsvinduet
|
||||
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
|
||||
|
||||
-- Listens celler, prik og statusfarve (hvert sekund og efter opbygning).
|
||||
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 color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
||||
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
|
||||
|
||||
-- Start/Stop for ét flow (rækkens felt 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)
|
||||
-- statuslinjen kan ligge uden for skærmen: sprunget over vises også som besked i højre side
|
||||
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)
|
||||
-- en gammel afvisning gælder ikke længere når flowet kører (fx startet fra Oversigt bagefter)
|
||||
if row.running > 0 and self.lastStartFailed ~= nil then self.lastStartFailed[run.id] = nil end
|
||||
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
|
||||
|
||||
function SmartPickupFrame:fillFlowSummary(adEnv, run)
|
||||
local i18n = self.i18n
|
||||
if not self.editPanelOpen then
|
||||
self:addTextRow(string.format(i18n:getText("spu_guide_flow_section"), tostring(run.name)))
|
||||
end
|
||||
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||
local guide = ADFlowGuide.open(run, self:flowGuideContext(adEnv, run))
|
||||
local getText = function(key) return i18n:getText(key) end
|
||||
local runId = run.id
|
||||
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||
local step = line.step
|
||||
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||
if step == "name" then frame:onClickRename() else frame:openFlowStep(runId, step) end
|
||||
end)
|
||||
end
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), tostring(cells[6])))
|
||||
self:addLinkRow(" " .. i18n:getText("spu_guide_flow_open"), function(frame)
|
||||
frame.selectedRunId = runId
|
||||
frame:onClickFlowSetup()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Redigeringsvinduet for et flow (gui/SmartPickupEditPanel.lua).
|
||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||
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,
|
||||
}
|
||||
|
||||
-- Hvert sekund: tavlens celler, statuslinje og knapper.
|
||||
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
|
||||
|
||||
-- ---- hooks til den fælles guide-skærm ----
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
SmartPickupFrame.GUIDE_HOOKS.flow = {
|
||||
model = ADFlowGuide,
|
||||
tab = SmartPickupFrame.TAB_FLOWS,
|
||||
heading = function(frame, guide)
|
||||
return string.format(frame.i18n:getText("spu_guide_flow_title"), tostring(guide.draft.name))
|
||||
end,
|
||||
context = function(frame, adEnv, guide)
|
||||
-- flowet er slettet mens guiden var åben
|
||||
if not guide.draft.isNew and ADRuns.getRun(ADRunsController.store, guide.draft.id) == nil then return nil end
|
||||
return frame:flowGuideContext(adEnv, guide.draft)
|
||||
end,
|
||||
choose = function(frame, id)
|
||||
if id ~= "rename" then return false end
|
||||
TextInputDialog.show(SmartPickupFrame.onGuideNameResult, frame, frame.guide.draft.name, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
||||
return true
|
||||
end,
|
||||
startKey = function() return "spu_guide_flow_start" end,
|
||||
save = function(frame, guide, start) frame:saveFlowGuide(guide, start) end,
|
||||
}
|
||||
|
||||
function SmartPickupFrame:onGuideNameResult(value, clickOk)
|
||||
self:safe(function(frame)
|
||||
if not clickOk or frame.guide == nil or frame.guideKind ~= "flow" then return end
|
||||
frame.guide = ADFlowGuide.choose(frame.guide, "name", value)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Gem: nyt flow oprettes først nu; felterne via updateRun; traktorer tildeles/fjernes; markører findes.
|
||||
-- start = true: flowet startes bagefter ([Start flow]).
|
||||
function SmartPickupFrame:saveFlowGuide(guide, start)
|
||||
local adEnv = self.getAdEnv()
|
||||
local result = ADFlowGuide.result(guide)
|
||||
local runId = result.runId
|
||||
local oldIds = {}
|
||||
if result.isNew then
|
||||
runId = ADRunsController.createRun(result.name, result.kind).id
|
||||
else
|
||||
local existing = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if existing == nil then return end
|
||||
oldIds = existing.vehicleIds
|
||||
end
|
||||
ADRunsController.updateRun(runId, result.changes)
|
||||
local added, removed = ADFlowGuide.vehicleChanges(oldIds, result.vehicleIds)
|
||||
for _, id in ipairs(removed) do
|
||||
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||
if vehicle ~= nil then
|
||||
ADRunsController.unassignVehicle(vehicle)
|
||||
else
|
||||
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, id)
|
||||
end
|
||||
end
|
||||
for _, id in ipairs(added) do
|
||||
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||
if vehicle ~= nil then ADRunsController.assignVehicle(runId, vehicle) end
|
||||
end
|
||||
-- solo-traktorernes vogne fra guiden: flowet husker dem straks og henter dem ved Connect ved start
|
||||
local savedRun = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if savedRun ~= nil then
|
||||
local wagons = ADRuns.copyWagons(savedRun.wagons)
|
||||
for tractorId, wagonId in pairs(result.wagons or {}) do wagons[tractorId] = wagonId end
|
||||
savedRun.wagons = wagons
|
||||
end
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, runId))
|
||||
self.selectedRunId = runId
|
||||
local saved = string.format(self.i18n:getText("spu_guide_flow_saved"), tostring(result.name))
|
||||
if start and adEnv ~= nil and not ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runId)).isRunning then
|
||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, runId))
|
||||
self:showOverride(saved .. " — " .. string.format(self.i18n:getText("spu_status_started"), started, skipped), reasons)
|
||||
else
|
||||
self:showOverride(saved)
|
||||
end
|
||||
ADRunsController.save()
|
||||
end
|
||||
|
||||
-- ---- knapper og [Ret] ----
|
||||
|
||||
function SmartPickupFrame:onClickFlowSetup()
|
||||
self:safe(function(frame)
|
||||
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||
if adEnv == nil or run == nil then return end
|
||||
frame:openGuide("flow", ADFlowGuide.open(run, frame:flowGuideContext(adEnv, run)))
|
||||
end)
|
||||
end
|
||||
|
||||
-- [Nyt flow] (også fra fanen Oversigt): guiden på Flows-fanen; intet oprettes før [Gem].
|
||||
function SmartPickupFrame:onClickFlowNew()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local name = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
||||
local guide = ADFlowGuide.openNew(name, {})
|
||||
guide = ADGuide.withContext(guide, frame:flowGuideContext(adEnv, guide.draft))
|
||||
if frame.currentTab ~= SmartPickupFrame.TAB_FLOWS then frame:switchTab(SmartPickupFrame.TAB_FLOWS) end
|
||||
frame:openGuide("flow", guide)
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openFlowStep(runId, step)
|
||||
local adEnv = self.getAdEnv()
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if adEnv == nil or run == nil then return end
|
||||
self:openGuide("flow", ADFlowGuide.open(run, self:flowGuideContext(adEnv, run), {step = step, single = true, returnTo = "close"}))
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
-- Smart Pickup — fanen Flows
|
||||
-- Tavle øverst: én tekstrække pr. flow (Vare · Fra → Til · Regel · Traktorer · Status), det valgte
|
||||
-- markeret med ">". Under tavlen: detail-blok for det valgte flow — Retning, Fra/Til som BYGNINGER
|
||||
-- (markøren findes automatisk), Varer, Regler, Vent ved, Traktorer, Status — og Wait-puljerne.
|
||||
-- Tavlen (én række pr. flow, det valgte markeret med ">") bruges af fanerne Oversigt og Flows; Flows-fanens
|
||||
-- resumé og guiden "Opsæt flow" ligger i gui/SmartPickupFlowGuide.lua. Her også bygnings-teksten og de
|
||||
-- gamle klik-callbacks for retning/bygninger.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
-- ---- tavlen ----
|
||||
@@ -49,7 +49,11 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
||||
end
|
||||
local rule
|
||||
if row.kind == "outbound" then
|
||||
rule = string.format(i18n:getText("spu_flow_ruleOut"), row.sellAtPercent or 90, row.storeAbovePercent or 80)
|
||||
local sells, stores = ADFlowView.policyUse(row.policies)
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
||||
rule = table.concat(parts, ", ")
|
||||
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
||||
else
|
||||
rule = row.loops ~= nil and row.loops > 0 and string.format(i18n:getText("spu_flow_ruleInLoops"), row.loops) or i18n:getText("spu_flow_ruleIn")
|
||||
@@ -80,8 +84,12 @@ function SmartPickupFrame:makeRowClickable(row, action)
|
||||
row.mouseEvent = function(element, posX, posY, isDown, isUp, button, eventUsed)
|
||||
local used = baseMouseEvent(element, posX, posY, isDown, isUp, button, eventUsed)
|
||||
-- også inden for listens synlige udsnit: rækker scrollet ud af syne må ikke fange klik
|
||||
if not used and not eventUsed and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
||||
and isInside(element, posX, posY) and isInside(frame.boxLayout, posX, posY) then
|
||||
-- klik tæller inde i rækkens eget layout (listen eller redigeringsvinduet); listen er død mens vinduet er åbent
|
||||
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
|
||||
frame.lastClickX = posX
|
||||
frame.pendingClick = action
|
||||
return true
|
||||
end
|
||||
@@ -137,115 +145,6 @@ function SmartPickupFrame:describeBuilding(building, role)
|
||||
return string.format(" %s — %s", building.name, table.concat(parts, " · "))
|
||||
end
|
||||
|
||||
-- Afkrydsning pr. bygning: kilder (udkørsel, role "load") eller mål (forsyning, role "unload").
|
||||
function SmartPickupFrame:fillBuildingRows(adEnv, run, role)
|
||||
if ADBuildings == nil then
|
||||
self:addTextRow(self.i18n:getText("spu_status_noAd"))
|
||||
return
|
||||
end
|
||||
local chosen = {}
|
||||
local ids = role == "load" and run.sourceBuildingIds or role == "store" and run.storeBuildingIds or run.targetBuildingIds
|
||||
local rowKind = role == "load" and "sourceBuilding" or role == "store" and "storeBuilding" or "targetBuilding"
|
||||
for _, id in ipairs(ids or {}) do chosen[id] = true end
|
||||
local count = 0
|
||||
-- kandidater -> valgte først, så med markør; uden markør kun med "Vis alle bygninger"
|
||||
local candidates = {}
|
||||
-- "Lager til": kun bygninger der kan tage flowets lagervarer (æg-paller -> palleskure der tager æg)
|
||||
local storeGoods = role == "store" and ADRunsController.getStoreGoods(adEnv, run) or nil
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, g_currentMission:getFarmId())) do
|
||||
local isStoreKind = building.kind == "silo" or building.kind == "factory" or building.kind == "objectStorage"
|
||||
local fits = (role == "load" and building.canBeSource) or (role == "unload" and building.canBeTarget)
|
||||
or (role == "store" and isStoreKind and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, storeGoods)))
|
||||
if fits then
|
||||
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role == "store" and "unload" or role) or {}
|
||||
table.insert(candidates, {id = building.id, name = building.name, hasMarker = markers[1] ~= nil, building = building})
|
||||
end
|
||||
end
|
||||
local shown = ADFlowView.buildingRows(candidates, chosen, self.showAllBuildings == true)
|
||||
local hiddenBuildings = ADFlowView.hiddenCount(candidates, shown)
|
||||
if hiddenBuildings > 0 or self.showAllBuildings then
|
||||
self:addToggleRow("showAllBuildings", string.format(self.i18n:getText("spu_showAllBuildings"), hiddenBuildings), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllBuildings == true)
|
||||
end
|
||||
for _, entry in ipairs(shown) do
|
||||
local building = entry.building
|
||||
count = count + 1
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = building.id})
|
||||
row:getDescendantByName("title"):setText(self:describeBuilding(building, role == "store" and "unload" or role))
|
||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
-- valgte bygninger der ikke længere findes (solgt)
|
||||
for _, id in ipairs(ids or {}) do
|
||||
if ADBuildings.find(adEnv, g_currentMission:getFarmId(), id) == nil then
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = id})
|
||||
row:getDescendantByName("title"):setText(string.format(" %s — %s", tostring(id), self.i18n:getText("spu_buildingGone")))
|
||||
row:getDescendantByName("option"):setState(SmartPickupFrame.STATE_ON, false)
|
||||
end
|
||||
end
|
||||
if count == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_noBuildings"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:rebuildFlowDetail(adEnv, run)
|
||||
ADRunsController.resolveMarkers(adEnv, run)
|
||||
run = self:getRun()
|
||||
local isOut = run.kind == "outbound"
|
||||
self:addTextRow(string.format(self.i18n:getText("spu_section_flow"), run.name))
|
||||
self.directionRowOption = self:addToggleRow("direction", self.i18n:getText("spu_direction"), {self.i18n:getText("spu_direction_in"), self.i18n:getText("spu_direction_out")}, isOut)
|
||||
if isOut then
|
||||
self:addTextRow(self.i18n:getText("spu_section_from"))
|
||||
self:fillBuildingRows(adEnv, run, "load")
|
||||
self:addTextRow(self.i18n:getText("spu_section_products"))
|
||||
self:fillProductRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_rules"))
|
||||
self.sellAtOption:setState(SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT), false)
|
||||
self.storeAboveOption:setState(SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT), false)
|
||||
self.collectRoundRowOption = self:addToggleRow("collectRound", self.i18n:getText("spu_collectRound"), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, run.collectRound == true)
|
||||
self:addTextRow(self.i18n:getText("spu_section_store"))
|
||||
self:fillBuildingRows(adEnv, run, "store")
|
||||
self:fillOutboundStatusRows(adEnv, run)
|
||||
else
|
||||
self:addTextRow(self.i18n:getText("spu_section_to"))
|
||||
self:fillBuildingRows(adEnv, run, "unload")
|
||||
self:addTextRow(self.i18n:getText("spu_section_targets"))
|
||||
self.ingredientModeRowOption = self:addToggleRow("ingredientMode", self.i18n:getText("spu_ingredientMode"), {self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")}, run.ingredientMode == "manual")
|
||||
self:fillIngredientRows(adEnv, run)
|
||||
self:fillNeedsRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_rules"))
|
||||
self.loopsOption:setState(run.loops + 1, false)
|
||||
self.returnBelowOption:setState(self:getReturnBelowIndex(run), false)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- callbacks ----
|
||||
|
||||
function SmartPickupFrame:onClickDirection(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {kind = state == SmartPickupFrame.STATE_ON and "outbound" or "supply"})
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickBuilding(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
if data.kind == "sourceBuilding" then
|
||||
ADRunsController.store = ADRuns.toggleSourceBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
elseif data.kind == "storeBuilding" then
|
||||
ADRunsController.store = ADRuns.toggleStoreBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
local updatedStore = ADRuns.getRun(ADRunsController.store, run.id)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': storeBuildings=%s", tostring(run.name), table.concat(updatedStore.storeBuildingIds or {}, " "))
|
||||
frame:requestRebuild()
|
||||
return
|
||||
else
|
||||
ADRunsController.store = ADRuns.toggleTargetBuilding(ADRunsController.store, run.id, data.buildingId)
|
||||
end
|
||||
local updated = ADRuns.getRun(ADRunsController.store, run.id)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': %s=%s", tostring(run.name), data.kind == "sourceBuilding" and "sourceBuildings" or "targetBuildings", table.concat(data.kind == "sourceBuilding" and updated.sourceBuildingIds or updated.targetBuildingIds, " "))
|
||||
ADRunsController.resolveMarkers(frame.getAdEnv(), updated)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -11,17 +11,21 @@ SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
|
||||
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
||||
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
||||
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
|
||||
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS = 1, 2, 3
|
||||
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5
|
||||
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
|
||||
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
||||
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
||||
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
||||
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
||||
SmartPickupFrame.TAB_KINDS = {[4] = "factory", [5] = "silo", [6] = "husbandry", [7] = "objectStorage"}
|
||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
||||
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
||||
|
||||
function SmartPickupFrame.getTabKind(tab)
|
||||
return SmartPickupFrame.TAB_KINDS[tab]
|
||||
end
|
||||
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||
-- Afvisningsgrunde der handler om VOGNEN (ikke run'ets opsætning): traktoren kan ikke vælges.
|
||||
SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true}
|
||||
SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true, autoloadOn = true}
|
||||
|
||||
function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
|
||||
@@ -32,6 +36,7 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
self.runIds, self.targetIds, self.poolGroups = {}, {}, {}
|
||||
self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...}
|
||||
self.clonedRows = {}
|
||||
self.panelRows = {}
|
||||
self.refreshTimer = 0
|
||||
self.statusOverride = nil
|
||||
self.statusRow = nil -- Text-elementet i den klonede statusrække
|
||||
@@ -49,11 +54,21 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
||||
-- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow.
|
||||
self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end}
|
||||
self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end}
|
||||
self.monthButtons = {self.backButtonInfo}
|
||||
-- bygningsfanerne (Siloer/Stalde/Fabrikker/Palleskure): omdøb den valgte bygning
|
||||
self.renameBuildingButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_renameBuilding"), callback = function() self:onClickRenameBuilding() end}
|
||||
self.storageButtons = {self.backButtonInfo, self.renameBuildingButtonInfo}
|
||||
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
|
||||
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
|
||||
-- Stop alt og parkér: flows + markarbejde + marksilo stoppes, køretøjer til parkering, pause til Start alle
|
||||
self.stopParkButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_stopPark"), callback = function() self:onClickStopAndPark() end}
|
||||
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.stopParkButtonInfo, self.newButtonInfo}
|
||||
-- Marker: Start nu / Stop for den valgte mark
|
||||
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
|
||||
self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo}
|
||||
-- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
|
||||
self:initFieldButtons(i18n)
|
||||
self:initGuideButtons(i18n)
|
||||
-- Flows: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet] (gui/SmartPickupFlowGuide.lua)
|
||||
self:initFlowButtons(i18n)
|
||||
self:initEditPanelButtons(i18n)
|
||||
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
|
||||
self.menuButtonInfo = self.overviewButtons
|
||||
self:setMenuButtonInfo(self.menuButtonInfo)
|
||||
@@ -71,7 +86,7 @@ end
|
||||
|
||||
-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker.
|
||||
function SmartPickupFrame:initialize()
|
||||
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "fieldPrefab"}) do
|
||||
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "flowListPrefab", "fieldPrefab", "fieldListPrefab", "monthPrefab", "kpiPrefab", "alertPrefab", "itemPrefab", "subPrefab"}) do
|
||||
local prefab = self[name]
|
||||
if prefab ~= nil then
|
||||
prefab:unlinkElement()
|
||||
@@ -197,6 +212,9 @@ end
|
||||
|
||||
function SmartPickupFrame:onFrameClose()
|
||||
self.frameOpen = false
|
||||
-- et åbent redigeringsvindue og en åben guide kasseres (intet er gemt før [Gem])
|
||||
self:hideEditPanel()
|
||||
self:closeGuide()
|
||||
SmartPickupFrame:superClass().onFrameClose(self)
|
||||
end
|
||||
|
||||
@@ -243,6 +261,11 @@ function SmartPickupFrame:requestRebuild()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:switchTab(tabIndex)
|
||||
if self.editPanelOpen then
|
||||
self:closeGuide()
|
||||
self:hideEditPanel()
|
||||
end
|
||||
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
|
||||
self.currentTab = tabIndex
|
||||
for index, tab in ipairs(self.subCategoryTabs) do
|
||||
tab:setSelected(index == tabIndex)
|
||||
@@ -263,7 +286,8 @@ end
|
||||
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
||||
-- Kør til lager for udkørsel. Fanen Lagre viser ingen.
|
||||
function SmartPickupFrame:setStaticRowsVisible(tab, run)
|
||||
local isFlows = tab == SmartPickupFrame.TAB_FLOWS
|
||||
-- Flows-fanen bruger tavle + resumé + guide (v1.32); de faste XML-rækker vises ikke længere
|
||||
local isFlows = false
|
||||
local hasRun = isFlows and run ~= nil
|
||||
local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound"
|
||||
local visibility = {
|
||||
@@ -312,6 +336,7 @@ function SmartPickupFrame:clearRows()
|
||||
self.productRows, self.outboundStatusRows = {}, {}
|
||||
self.storageRows = {}
|
||||
self.storageDetailRows = {}
|
||||
self.itemView = nil
|
||||
self.boardRows = {}
|
||||
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
|
||||
end
|
||||
@@ -328,9 +353,11 @@ function SmartPickupFrame:addToggleRow(kind, title, texts, isOn)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addRow(prefab, data)
|
||||
local row = prefab:clone(self.boxLayout)
|
||||
local layout = self.rowTarget or self.boxLayout
|
||||
local row = prefab:clone(layout)
|
||||
row.spuLayout = layout
|
||||
FocusManager:loadElementFromCustomValues(row)
|
||||
table.insert(self.clonedRows, 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")})
|
||||
@@ -339,23 +366,56 @@ function SmartPickupFrame:addRow(prefab, data)
|
||||
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()
|
||||
local focused = SmartPickupFrame.getFocusedElement()
|
||||
for _, row in ipairs(self.panelRows or {}) do
|
||||
if focused ~= nil and SmartPickupFrame.isDescendantOf(focused, row) then
|
||||
self.focusLost = true
|
||||
end
|
||||
FocusManager:removeElement(row)
|
||||
row:delete()
|
||||
end
|
||||
self.panelRows = {}
|
||||
end
|
||||
|
||||
-- Tekstrække uden valgmuligheder: sektionstitler, Wait-pulje-linjer og statuslinjen.
|
||||
-- Tegn pr. tekstrække før teksten ombrydes (rækken afkorter med "..." ved ca. 70 tegn).
|
||||
SmartPickupFrame.TEXT_ROW_CHARS = 90
|
||||
|
||||
-- Lange tekster fordeles på flere rækker i stedet for at blive afkortet. Returnerer første rækkes titel.
|
||||
function SmartPickupFrame:addTextRow(text)
|
||||
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||
title:setText(text)
|
||||
return title
|
||||
local first = nil
|
||||
for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||
title:setText(line)
|
||||
first = first or title
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
-- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning,
|
||||
-- fordi rækkerne først findes på det tidspunkt.
|
||||
function SmartPickupFrame:colorRows()
|
||||
self:colorLayout(self.boxLayout)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:colorLayout(layout)
|
||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
||||
if colors == nil or self.boxLayout == nil then
|
||||
if colors == nil or layout == nil then
|
||||
return
|
||||
end
|
||||
local unpackFn = table.unpack or unpack
|
||||
local isEvenRow = false
|
||||
for _, element in ipairs(self.boxLayout.elements) do
|
||||
for _, element in ipairs(layout.elements) do
|
||||
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
||||
end
|
||||
@@ -390,10 +450,33 @@ function SmartPickupFrame:rebuild()
|
||||
self.statusRow = self:addTextRow("")
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||
self:refreshLive()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
if adEnv ~= nil then
|
||||
self:fillFieldSiloRows(adEnv)
|
||||
end
|
||||
self.monthRebuildTicks = SmartPickupFrame.FIELDSILO_REBUILD_TICKS
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
self:refreshButtons()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
||||
if adEnv ~= nil then
|
||||
self:fillMonthRows(adEnv)
|
||||
end
|
||||
self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
self:refreshButtons()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
if adEnv ~= nil then
|
||||
self:fillOverviewRows(adEnv)
|
||||
@@ -407,46 +490,17 @@ function SmartPickupFrame:rebuild()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
self:fillRunOption(adEnv)
|
||||
local hasRun = run ~= nil and adEnv ~= nil
|
||||
for _, element in ipairs({self.waitPoolOption, self.loopsOption, self.returnBelowOption, self.sellAtOption, self.storeAboveOption}) do
|
||||
if element ~= nil then
|
||||
element:setDisabled(not hasRun)
|
||||
end
|
||||
end
|
||||
if hasRun then
|
||||
self:fillWaitPoolOption(adEnv, run)
|
||||
self:rebuildFlowDetail(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_vehicles"))
|
||||
self:fillVehicleRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_status"))
|
||||
if adEnv ~= nil then
|
||||
self:fillFlowTabRows(adEnv)
|
||||
end
|
||||
self.statusRow = self:addTextRow("")
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||
self:refreshLive()
|
||||
self:restoreFocusIfLost()
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillRunOption(adEnv)
|
||||
local texts, index = {}, 1
|
||||
self.runIds = {}
|
||||
for position, run in ipairs(self:getRunsForTab()) do
|
||||
local status = adEnv ~= nil and ADRunsController.getRunStatus(adEnv, run) or {running = 0, total = #run.vehicleIds}
|
||||
table.insert(texts, string.format("%s (%d/%d)", run.name, status.running, status.total))
|
||||
table.insert(self.runIds, run.id)
|
||||
if run.id == self.selectedRunId then
|
||||
index = position
|
||||
end
|
||||
end
|
||||
if #texts == 0 then
|
||||
texts = {self.i18n:getText("spu_noRuns")}
|
||||
end
|
||||
self.runOption:setTexts(texts)
|
||||
self.runOption:setState(index, false)
|
||||
self.runOption:setDisabled(#self.runIds == 0)
|
||||
end
|
||||
|
||||
-- Overblik: "Cow 1 food: TMR 18.0k/27.8k · Silage 3.0k/…" (mest trængende først)
|
||||
function SmartPickupFrame:describeTargetLevels(target)
|
||||
if #target.levels == 0 then
|
||||
@@ -455,114 +509,6 @@ function SmartPickupFrame:describeTargetLevels(target)
|
||||
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillWaitPoolOption(adEnv, run)
|
||||
local texts, index = {self.i18n:getText("spu_waitPool_all")}, 1
|
||||
self.poolGroups = {""}
|
||||
for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do
|
||||
table.insert(texts, string.format("%s (%d)", pool.group, pool.count))
|
||||
table.insert(self.poolGroups, pool.group)
|
||||
if pool.group == run.waitPoolGroup then
|
||||
index = #texts
|
||||
end
|
||||
end
|
||||
self.waitPoolOption:setTexts(texts)
|
||||
self.waitPoolOption:setState(index, false)
|
||||
end
|
||||
|
||||
-- Traktorer: som standard flowets egne og de ledige der kan bruges; "Vis alle" tilføjer traktorer i andre
|
||||
-- flows og låste (vognen passer ikke) — de er tit mange og gør listen uoverskuelig.
|
||||
function SmartPickupFrame:fillVehicleRows(adEnv, run)
|
||||
local inRun = {}
|
||||
for _, id in ipairs(run.vehicleIds) do
|
||||
inRun[id] = true
|
||||
end
|
||||
local entries = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local otherRun = ADRunsController.getRunForVehicle(vehicle)
|
||||
local state = "free"
|
||||
if inRun[id] then
|
||||
state = "in"
|
||||
elseif otherRun ~= nil and otherRun.id ~= run.id then
|
||||
state = "other"
|
||||
elseif SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] then
|
||||
state = "blocked"
|
||||
end
|
||||
table.insert(entries, {id = id, label = ADRunsController.getVehicleLabel(adEnv, vehicle), state = state, vehicle = vehicle, otherRun = otherRun})
|
||||
end
|
||||
local rows = ADFlowView.vehicleRows(entries, self.showAllVehicles == true)
|
||||
local hidden = ADFlowView.hiddenCount(entries, rows)
|
||||
if hidden > 0 or self.showAllVehicles then
|
||||
self.showAllVehiclesOption = self:addToggleRow("showAllVehicles", string.format(self.i18n:getText("spu_showAllVehicles"), hidden), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllVehicles == true)
|
||||
end
|
||||
for _, entry in ipairs(rows) do
|
||||
local vehicle = entry.vehicle
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "vehicle", vehicle = vehicle, row = nil})
|
||||
local option, title = row:getDescendantByName("option"), row:getDescendantByName("title")
|
||||
self.rowData[option].row = row
|
||||
option:setState(entry.state == "in" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
if entry.state == "other" then
|
||||
-- én traktor = ét flow; rækken viser hvor den er
|
||||
option:setDisabled(true)
|
||||
self.rowData[option].locked = true
|
||||
title:setText(string.format("%s — %s", entry.label, string.format(self.i18n:getText("spu_inRun"), tostring(entry.otherRun.name))))
|
||||
else
|
||||
title:setText(self:describeVehicle(adEnv, run, vehicle, entry.state == "in"))
|
||||
if entry.state == "blocked" then
|
||||
option:setDisabled(true)
|
||||
self.rowData[option].locked = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — ✔" / "… — ✖ ingen vogn"; reservationsnote fra Start bevares.
|
||||
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — ✔" / "… — ✖ ingen vogn".
|
||||
-- Vises for ALLE traktorer, så man kan se om vognen passer, før man siger Ja.
|
||||
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
|
||||
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||
local parts = {label}
|
||||
if validation.isAutoload and validation.capacity == nil then
|
||||
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
|
||||
elseif validation.capacity ~= nil then
|
||||
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
|
||||
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
|
||||
if carriable ~= nil then
|
||||
table.insert(parts, carriable)
|
||||
end
|
||||
end
|
||||
if validation.ok then
|
||||
table.insert(parts, "OK")
|
||||
else
|
||||
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
|
||||
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
|
||||
reason = string.format(reason, tostring(validation.missingBuilding))
|
||||
end
|
||||
table.insert(parts, "X " .. reason)
|
||||
end
|
||||
return table.concat(parts, " — ")
|
||||
end
|
||||
|
||||
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
|
||||
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
|
||||
local canCarry = {}
|
||||
for _, fillType in ipairs(carriable or {}) do
|
||||
canCarry[fillType] = true
|
||||
end
|
||||
local titles = {}
|
||||
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
|
||||
if canCarry[fillType] then
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||||
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
|
||||
end
|
||||
end
|
||||
if #titles == 0 then
|
||||
return nil
|
||||
end
|
||||
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
|
||||
end
|
||||
|
||||
-- Felterne følger modellen: når et klik ikke er nået ind i run'et, springer feltet tilbage,
|
||||
-- og loggen ("run '…': waitPoolGroup=…") viser om klikket blev registreret.
|
||||
function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
|
||||
@@ -657,10 +603,19 @@ end
|
||||
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
|
||||
function SmartPickupFrame:refreshButtons()
|
||||
local wanted = self.flowButtons
|
||||
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
if self.editPanelOpen then
|
||||
wanted = self.guide ~= nil and self:panelGuideButtons() or self:getPanelButtons()
|
||||
elseif self:hasGuideOn(self.currentTab) then
|
||||
wanted = self:getGuideButtons()
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||
wanted = self.overviewButtons
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||
wanted = self.fieldButtons
|
||||
wanted = self:getFieldButtons()
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
wanted = self.monthButtons
|
||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||
wanted = self.storageButtons
|
||||
self.renameBuildingButtonInfo.disabled = self.storageDetailPlaceable == nil
|
||||
end
|
||||
if self.menuButtonInfo ~= wanted then
|
||||
self.menuButtonInfo = wanted
|
||||
@@ -681,10 +636,17 @@ function SmartPickupFrame:refreshLive()
|
||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
||||
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
|
||||
if isOverview then
|
||||
self:refreshOverview(adEnv)
|
||||
end
|
||||
if self.editPanelOpen then self:refreshEditPanel(adEnv) end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||
self:refreshFieldRows(adEnv)
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
@@ -692,6 +654,13 @@ function SmartPickupFrame:refreshLive()
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_FLOWS then
|
||||
self:refreshFlowTab(adEnv)
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
|
||||
self:refreshButtons()
|
||||
return
|
||||
end
|
||||
self:refreshStorageRows(adEnv)
|
||||
local isRunning = false
|
||||
if isOverview then
|
||||
@@ -701,8 +670,19 @@ function SmartPickupFrame:refreshLive()
|
||||
running, total = running + status.running, total + status.total
|
||||
if status.running > 0 then activeRuns = activeRuns + 1 end
|
||||
end
|
||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs))
|
||||
local statusText = string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs)
|
||||
local queued = #(ADRunsController.startQueue or {})
|
||||
if queued > 0 then
|
||||
local totalQueued = ADRunsController.startQueueTotal or queued
|
||||
statusText = string.format(self.i18n:getText("spu_status_starting"), totalQueued - queued, totalQueued)
|
||||
elseif ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then
|
||||
statusText = self.i18n:getText("spu_status_paused")
|
||||
end
|
||||
self:setStatusText(self.statusOverride or statusText)
|
||||
self.stopAllButtonInfo.disabled = running == 0
|
||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||
-- lagerfanerne: ingen flow-status (in-game 29/9: 'Stoppet (1 traktorer)' fra det sidst valgte flow)
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
elseif run ~= nil then
|
||||
self:syncOptionsFromModel(adEnv, run)
|
||||
if run.kind == "outbound" then
|
||||
@@ -711,7 +691,7 @@ function SmartPickupFrame:refreshLive()
|
||||
self:refreshNeedsRows(adEnv, run)
|
||||
end
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
isRunning = status.isRunning == true
|
||||
isRunning = status.canStop == true
|
||||
local text = status.running > 0 and string.format(self.i18n:getText("spu_status_running"), status.running, status.total)
|
||||
or string.format(self.i18n:getText("spu_status_stopped"), status.total)
|
||||
self:setStatusText(self.statusOverride or text)
|
||||
@@ -747,27 +727,14 @@ function SmartPickupFrame:onClickRun(state)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickIngredient(state, element)
|
||||
-- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
|
||||
-- guiderne og markflåden deler prefab (og dermed onClick) med vare-rækkerne
|
||||
local data = self.rowData[element]
|
||||
if data ~= nil and data.kind == "target" then
|
||||
return self:onClickTarget(state, element)
|
||||
elseif data ~= nil and data.kind == "source" then
|
||||
return self:onClickSource(state, element)
|
||||
elseif data ~= nil and data.kind == "ingredientMode" then
|
||||
return self:onClickIngredientMode(state)
|
||||
elseif data ~= nil and data.kind == "collectRound" then
|
||||
return self:onClickCollectRound(state)
|
||||
elseif data ~= nil and data.kind == "direction" then
|
||||
return self:onClickDirection(state)
|
||||
elseif data ~= nil and (data.kind == "showAllVehicles" or data.kind == "showAllBuildings") then
|
||||
local isOn = state == SmartPickupFrame.STATE_ON
|
||||
if data.kind == "showAllVehicles" then self.showAllVehicles = isOn else self.showAllBuildings = isOn end
|
||||
self:requestRebuild()
|
||||
return
|
||||
if data ~= nil and data.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, data)
|
||||
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
|
||||
return self:onClickFieldSiloRow(state, data)
|
||||
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
||||
return self:onClickFieldRow(state, data)
|
||||
elseif data ~= nil and (data.kind == "sourceBuilding" or data.kind == "targetBuilding" or data.kind == "storeBuilding") then
|
||||
return self:onClickBuilding(state, element)
|
||||
end
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
@@ -793,12 +760,18 @@ end
|
||||
|
||||
function SmartPickupFrame:onClickVehicle(state, element)
|
||||
local clicked = self.rowData[element]
|
||||
-- markfanens maskinrækker (markflåden og markens faste maskiner) bruger samme række-type som flowets
|
||||
-- traktorliste; de hører til markfanen (fieldRig blev før behandlet som flow-traktor og aldrig gemt)
|
||||
if clicked ~= nil and clicked.kind == "fleetVehicle" then
|
||||
return self:onClickFieldRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, clicked)
|
||||
end
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil or data.locked then return end
|
||||
-- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres
|
||||
if run == nil or data == nil or data.locked or data.vehicle == nil then return end
|
||||
if state == SmartPickupFrame.STATE_ON then
|
||||
local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle)
|
||||
if previousRunId ~= nil then
|
||||
@@ -819,14 +792,7 @@ function SmartPickupFrame:onClickStartStop()
|
||||
self:safe(function(frame)
|
||||
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||
if adEnv == nil or run == nil then return end
|
||||
if ADRunsController.getRunStatus(adEnv, run).isRunning then
|
||||
ADRunsController.stopRun(adEnv, run.id)
|
||||
frame.statusOverride = nil
|
||||
else
|
||||
local started, skipped, reasons = frame:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_started"), started, skipped), reasons)
|
||||
end
|
||||
ADRunsController.save()
|
||||
frame:toggleRun(adEnv, run.id)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
@@ -866,11 +832,28 @@ function SmartPickupFrame:onClickStartAll()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local summary = ADRunsController.startAllRuns(adEnv)
|
||||
local _, skipped, reasons = frame:summarizeStartResults(summary.results)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d flows startet (%d traktorer), %d kørte allerede, %d sprunget over",
|
||||
summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped)
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_startedAll"), summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped), reasons)
|
||||
-- pausen (Stop alt og parkér) ophæves; traktorerne startes én ad gangen med mellemrum
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then ADFieldJobs.setPaused(false) end
|
||||
local summary = ADRunsController.queueStartAll(adEnv)
|
||||
local _, skipped, reasons = frame:summarizeStartResults(summary.skipped)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d traktorer i kø (én hvert %d. sekund), %d flows kørte allerede, %d sprunget over",
|
||||
summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped)
|
||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_queuedAll"), summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped), reasons)
|
||||
ADRunsController.save()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickStopAndPark()
|
||||
self:safe(function(frame)
|
||||
local adEnv = frame.getAdEnv()
|
||||
if adEnv == nil then return end
|
||||
local summary = ADRunsController.stopAllAndPark(adEnv)
|
||||
local text = string.format(frame.i18n:getText("spu_status_stopParked"), summary.stopped, summary.parked)
|
||||
local notes = {}
|
||||
if #summary.noPark > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_noPark"), table.concat(summary.noPark, ", "))) end
|
||||
if #summary.manual > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_fetchManual"), table.concat(summary.manual, ", "))) end
|
||||
frame:showOverride(text, notes)
|
||||
ADRunsController.save()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
@@ -897,25 +880,28 @@ function SmartPickupFrame:openFlow(runId)
|
||||
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
end
|
||||
|
||||
-- Nyt run: navnet vælges FØR run'et oprettes (annulleres dialogen, oprettes intet).
|
||||
-- Forslaget tæller efter antal runs, ikke efter løbenummeret, så "Run 2" kan genbruges.
|
||||
-- Nyt flow: guiden "Opsæt flow" (gui/SmartPickupFlowGuide.lua) — intet oprettes før [Gem].
|
||||
function SmartPickupFrame:onClickNew()
|
||||
self:onClickFlowNew()
|
||||
end
|
||||
|
||||
-- Omdøb den valgte bygning i en bygningsfane (gemmes i ADSmartPickup_names.xml; tomt = butiksnavnet igen)
|
||||
function SmartPickupFrame:onClickRenameBuilding()
|
||||
self:safe(function(frame)
|
||||
local suggestion = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
||||
TextInputDialog.show(SmartPickupFrame.onNewRunName, frame, suggestion, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
||||
local placeable = frame.storageDetailPlaceable
|
||||
if placeable == nil then return end
|
||||
frame.renamingBuildingId = ADBuildings.getId(placeable)
|
||||
local current = ADNames.forPlaceable(placeable) or (placeable.getName ~= nil and placeable:getName()) or ""
|
||||
TextInputDialog.show(SmartPickupFrame.onRenameBuildingResult, frame, tostring(current), frame.i18n:getText("spu_renameBuildingTitle"), nil, 40, g_i18n:getText("button_ok"))
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onNewRunName(value, clickOk)
|
||||
function SmartPickupFrame:onRenameBuildingResult(value, clickOk)
|
||||
self:safe(function(frame)
|
||||
if not clickOk or value == nil or not value:match("%S") then
|
||||
return
|
||||
end
|
||||
local run = ADRunsController.createRun(value, "supply")
|
||||
frame.selectedRunId = run.id
|
||||
frame.statusOverride = nil
|
||||
frame.currentTab = SmartPickupFrame.TAB_FLOWS
|
||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
if not clickOk or frame.renamingBuildingId == nil then return end
|
||||
ADNames.set(frame.renamingBuildingId, value)
|
||||
frame.renamingBuildingId = nil
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_storeAbove"/>
|
||||
</Bitmap>
|
||||
</ScrollingLayout>
|
||||
<!-- redigeringsvindue (v1.58): skjult; placering/størrelse sættes i gui/SmartPickupEditPanel.lua -->
|
||||
<GuiElement profile="baseReference" id="editOverlay" visible="false">
|
||||
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
|
||||
<Bitmap profile="spuOverlayPanel" id="editPanel"/>
|
||||
<Text profile="spuPanelTitle" id="editTitle"/>
|
||||
<Text profile="spuCell" id="editStatus" size="700px 30px" textMaxWidth="690px"/>
|
||||
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout"/>
|
||||
</GuiElement>
|
||||
|
||||
<!-- prefabs: klones pr. vare / traktor / tekstlinje og fjernes fra layoutet ved initialize -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="ingredientPrefab">
|
||||
@@ -81,6 +89,15 @@
|
||||
<Text profile="spuCell" name="c5" position="620px 0px" size="50px 40px" textMaxWidth="42px"/>
|
||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
</Bitmap>
|
||||
<!-- 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>
|
||||
<!-- Marker: én række pr. mark (mark · afgrøde · plan · auto/man. · status · maskiner). Rækken er ~815 px bred. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
@@ -90,6 +107,26 @@
|
||||
<Text profile="spuCell" name="c5" position="440px 0px" size="230px 40px" textMaxWidth="222px"/>
|
||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
</Bitmap>
|
||||
<!-- Måned: mark · arbejde · afgrøde · færdig · næste · vindue (vinduet bredt nok til "OBS: sidste såmåned"). ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="monthPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="100px 40px" textMaxWidth="92px"/>
|
||||
<Text profile="spuCell" name="c2" position="120px 0px" size="130px 40px" textMaxWidth="122px"/>
|
||||
<Text profile="spuCell" name="c3" position="250px 0px" size="110px 40px" textMaxWidth="102px"/>
|
||||
<Text profile="spuCell" name="c4" position="360px 0px" size="80px 40px" textMaxWidth="72px"/>
|
||||
<Text profile="spuCell" name="c5" position="440px 0px" size="150px 40px" textMaxWidth="142px"/>
|
||||
<Text profile="spuCell" name="c6" position="590px 0px" size="240px 40px" textMaxWidth="232px"/>
|
||||
</Bitmap>
|
||||
<!-- Fields-listen (v1.59): ● · mark · afgrøde · plan · kørsel · status · maskiner · [Start]/[Stop]. ~835 px. -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldListPrefab">
|
||||
<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>
|
||||
<!-- Lagre: statistik-tabel (vare · i bygningen · værdi nu · ved toppris · gården · rækker til · forbrug/prod.) -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="statPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||
@@ -106,6 +143,41 @@
|
||||
<Text profile="spuCell" name="c2" position="290px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||
<Text profile="spuCell" name="c3" position="550px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||
</Bitmap>
|
||||
<!-- Lagre, vareoversigt (v1.52). Rækken er ~815 px bred. -->
|
||||
<!-- nøgletal: 4 felter (tal + label); højden sættes i Lua -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="kpiPrefab">
|
||||
<Text profile="spuKpiValue" name="v1" position="20px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l1" position="20px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v2" position="220px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l2" position="220px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v3" position="420px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiLabel" name="l3" position="420px 0px" size="195px 40px" textMaxWidth="190px"/>
|
||||
<Text profile="spuKpiValue" name="v4" position="620px 0px" size="190px 40px" textMaxWidth="185px"/>
|
||||
<Text profile="spuKpiLabel" name="l4" position="620px 0px" size="190px 40px" textMaxWidth="185px"/>
|
||||
</Bitmap>
|
||||
<!-- kræver handling: farvet markør + tekst -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="alertPrefab">
|
||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
||||
<Text profile="spuCell" name="c1" position="40px 0px" size="770px 40px" textMaxWidth="765px"/>
|
||||
</Bitmap>
|
||||
<!-- vare: +/– navn · bjælke · mængde · værdi · netto/døgn og rækkevidde (klikbar) -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="itemPrefab">
|
||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="170px 40px" textMaxWidth="165px"/>
|
||||
<Bitmap profile="spuBarBg" name="barBg" position="190px 0px" size="140px 16px"/>
|
||||
<Bitmap profile="spuBar" name="bar" position="190px 0px" size="140px 16px"/>
|
||||
<Text profile="spuCell" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
|
||||
<Text profile="spuCell" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
|
||||
<Text profile="spuCell" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
|
||||
</Bitmap>
|
||||
<!-- bygning under en udfoldet vare: indrykket navn · tynd bjælke · mængde · værdi · procent -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="subPrefab">
|
||||
<Text profile="spuCellDim" name="c1" position="45px 0px" size="145px 40px" textMaxWidth="140px"/>
|
||||
<Bitmap profile="spuBarThinBg" name="barBg" position="190px 0px" size="140px 10px"/>
|
||||
<Bitmap profile="spuBarThin" name="bar" position="190px 0px" size="140px 10px"/>
|
||||
<Text profile="spuCellDim" name="c2" position="340px 0px" size="160px 40px" textMaxWidth="155px"/>
|
||||
<Text profile="spuCellDim" name="c3" position="500px 0px" size="105px 40px" textMaxWidth="100px"/>
|
||||
<Text profile="spuCellDim" name="c4" position="605px 0px" size="225px 40px" textMaxWidth="220px"/>
|
||||
</Bitmap>
|
||||
<!-- udkørsel: én række pr. vare med politik Fra/Lager/Salg/Salg, ellers lager -->
|
||||
<Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab">
|
||||
<MultiTextOption profile="fs25_settingsMultiTextOption" onClick="onClickProduct" name="option">
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
-- Smart Pickup — fælles guide-skærm ("Opsæt mark" / "Opsæt flow")
|
||||
-- Ét trin ad gangen: overskrift, "Trin 3 af 7 — …", kort forklaring, valgene som rækker og bundknapperne
|
||||
-- Næste/Gem, Tilbage, Annullér (+ Gem uden at starte på oversigts-trinnet). Al logik ligger i ADGuide og
|
||||
-- trin-definitionerne (ADFieldGuide/ADFlowGuide); her tegnes kun rækkerne og klik routes.
|
||||
-- Hver guide registrerer sig i SmartPickupFrame.GUIDE_HOOKS[kind] = {
|
||||
-- model, tab, heading(frame, guide), context(frame, adEnv, guide) -> context|nil (nil = luk guiden),
|
||||
-- choose(frame, id, value) -> handled, save(frame, guide, start), startKey(guide), showSaveOnly(guide),
|
||||
-- canAdvance(guide), afterRows(frame, adEnv, guide) }
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||
|
||||
function SmartPickupFrame:initGuideButtons(i18n)
|
||||
self.guideNextButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_guide_next"), callback = function() self:onClickGuideNext(true) end}
|
||||
self.guideSaveOnlyButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_saveOnly"), callback = function() self:onClickGuideNext(false) end}
|
||||
self.guidePrevButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_back"), callback = function() self:onClickGuideBack() end}
|
||||
self.guideCancelButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_guide_cancel"), callback = function() self:onClickGuideCancel() end}
|
||||
self.guideButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
|
||||
self.guideSummaryButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guideSaveOnlyButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:guideHooks()
|
||||
return self.guideKind ~= nil and SmartPickupFrame.GUIDE_HOOKS[self.guideKind] or nil
|
||||
end
|
||||
|
||||
-- Er der en åben guide på fanen?
|
||||
function SmartPickupFrame:hasGuideOn(tab)
|
||||
local hooks = self:guideHooks()
|
||||
return self.guide ~= nil and hooks ~= nil and hooks.tab == tab
|
||||
end
|
||||
|
||||
function SmartPickupFrame:openGuide(kind, guide)
|
||||
self.guideKind, self.guide = kind, guide
|
||||
self.statusOverride = nil
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Kladden kasseres (Annullér, fanebyt, lukket menu).
|
||||
function SmartPickupFrame:closeGuide()
|
||||
self.guideKind, self.guide = nil, nil
|
||||
end
|
||||
|
||||
function SmartPickupFrame:guideText(label)
|
||||
return ADGuide.labelText(label, function(key) return self.i18n:getText(key) end)
|
||||
end
|
||||
|
||||
-- Klikbar tekstrække (ombrydes; alle linjer er klikbare). Handlingen køres fra update() (pendingClick).
|
||||
function SmartPickupFrame:addLinkRow(text, action)
|
||||
local first = nil
|
||||
for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||
local row = self:addRow(self.textPrefab, {kind = "text"})
|
||||
local title = row:getDescendantByName("title")
|
||||
title:setText(line)
|
||||
self:makeRowClickable(row, action)
|
||||
first = first or title
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
local function optionIndex(options, value)
|
||||
for index, option in ipairs(options) do
|
||||
if option.value == value then return index end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addGuideRow(row)
|
||||
local data = {kind = "guideRow", row = row}
|
||||
if row.type == "choice" then
|
||||
local element = self:addRow(self.productPrefab, data)
|
||||
local option = element:getDescendantByName("option")
|
||||
local texts = {}
|
||||
for _, entry in ipairs(row.options) do table.insert(texts, self:guideText(entry.label)) end
|
||||
option:setTexts(texts)
|
||||
option:setState(optionIndex(row.options, row.value), false)
|
||||
element:getDescendantByName("title"):setText(self:guideText(row.title))
|
||||
elseif row.type == "toggle" or row.type == "check" then
|
||||
local element = self:addRow(self.ingredientPrefab, data)
|
||||
local option = element:getDescendantByName("option")
|
||||
if row.type == "toggle" then option:setTexts({self:guideText(row.off), self:guideText(row.on)}) end
|
||||
option:setState(row.value and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
element:getDescendantByName("title"):setText((row.type == "check" and " " or "") .. self:guideText(row.title))
|
||||
elseif row.type == "link" then
|
||||
local text = row.label ~= nil and self:guideText(row.label)
|
||||
or (ADGuide.lineText(row.line, function(key) return self.i18n:getText(key) end) .. self.i18n:getText("spu_guide_editSuffix"))
|
||||
self:addLinkRow(" " .. text, function(frame) frame:onGuideLink(row) end)
|
||||
else
|
||||
self:addTextRow(self:guideText(row.label))
|
||||
end
|
||||
end
|
||||
|
||||
-- Tegner den åbne guide. false = guiden er lukket (fx marken findes ikke længere) — fanen viser oversigten.
|
||||
function SmartPickupFrame:fillGuideRows(adEnv)
|
||||
local hooks = self:guideHooks()
|
||||
if hooks == nil or self.guide == nil then return false end
|
||||
local context = hooks.context(self, adEnv, self.guide)
|
||||
if context == nil then
|
||||
self:closeGuide()
|
||||
self.statusOverride = self.i18n:getText("spu_guide_closed")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
return false
|
||||
end
|
||||
self.guide = ADGuide.withContext(self.guide, context)
|
||||
local guide, model = self.guide, hooks.model
|
||||
self:addTextRow(hooks.heading(self, guide))
|
||||
local stepTitle = self.i18n:getText(model.stepKey(guide.step, guide))
|
||||
local index, total = ADGuide.position(guide)
|
||||
local header
|
||||
if guide.single then
|
||||
header = string.format(self.i18n:getText("spu_guide_editStep"), stepTitle)
|
||||
elseif index ~= nil and total > 1 then
|
||||
header = string.format(self.i18n:getText("spu_guide_stepOf"), index, total, stepTitle)
|
||||
else
|
||||
header = string.format(self.i18n:getText("spu_guide_stepOne"), index or 1, stepTitle)
|
||||
end
|
||||
self:addTextRow(header)
|
||||
self:addTextRow(" " .. self.i18n:getText(model.hintKey(guide.step, guide)))
|
||||
for _, row in ipairs(model.rows(guide)) do
|
||||
self:addGuideRow(row)
|
||||
end
|
||||
if hooks.afterRows ~= nil then hooks.afterRows(self, adEnv, guide) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- ---- klik ----
|
||||
|
||||
function SmartPickupFrame:applyGuideChoice(id, value)
|
||||
local hooks = self:guideHooks()
|
||||
if hooks == nil or self.guide == nil then return end
|
||||
if hooks.choose ~= nil and hooks.choose(self, id, value) then return end
|
||||
self.guide = hooks.model.choose(self.guide, id, value)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local row = data ~= nil and data.row or nil
|
||||
if row == nil or frame.guide == nil then return end
|
||||
local value
|
||||
if row.type == "choice" then
|
||||
local option = row.options[state]
|
||||
if option == nil then return end
|
||||
value = option.value
|
||||
-- valg pr. vare (fx politik): modellen skal vide hvilken
|
||||
if row.item ~= nil then value = {item = row.item, value = option.value} end
|
||||
elseif row.type == "toggle" then
|
||||
value = state == SmartPickupFrame.STATE_ON
|
||||
elseif row.type == "check" then
|
||||
value = {item = row.item, on = state == SmartPickupFrame.STATE_ON}
|
||||
else
|
||||
return
|
||||
end
|
||||
frame:applyGuideChoice(row.id, value)
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Klikbare linjer: [Ret] på oversigts-trinnet åbner ét trin; andre links er valg (fx mark i trin 1).
|
||||
function SmartPickupFrame:onGuideLink(row)
|
||||
if self.guide == nil then return end
|
||||
if row.id == "jump" then
|
||||
self.guide = ADGuide.jump(self.guide, row.value, "summary")
|
||||
else
|
||||
self:applyGuideChoice(row.id, row.value)
|
||||
end
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
local function finish(frame, hooks, guide, start)
|
||||
hooks.save(frame, guide, start)
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
end
|
||||
|
||||
-- Næste / Gem / Kør automatisk (start = true) og Gem uden at starte (start = false).
|
||||
function SmartPickupFrame:onClickGuideNext(start)
|
||||
self:safe(function(frame)
|
||||
local hooks, guide = frame:guideHooks(), frame.guide
|
||||
if hooks == nil or guide == nil then return end
|
||||
if hooks.canAdvance ~= nil and not hooks.canAdvance(guide) then
|
||||
frame:showOverride(frame.i18n:getText(hooks.cannotAdvanceKey or "spu_guide_cannotAdvance"))
|
||||
return
|
||||
end
|
||||
if guide.single and guide.returnTo == "close" then
|
||||
if start == false then return end
|
||||
return finish(frame, hooks, guide, nil)
|
||||
end
|
||||
if guide.step == "summary" and not guide.single then
|
||||
return finish(frame, hooks, guide, start)
|
||||
end
|
||||
if start == false then return end
|
||||
local nextGuide, event = ADGuide.next(guide)
|
||||
if event == "done" then return finish(frame, hooks, guide, true) end
|
||||
frame.guide = nextGuide
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideBack()
|
||||
self:safe(function(frame)
|
||||
if frame.guide == nil then return end
|
||||
local previous, event = ADGuide.back(frame.guide)
|
||||
if event == "cancel" then frame:closeGuide() else frame.guide = previous end
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickGuideCancel()
|
||||
self:safe(function(frame)
|
||||
frame:closeGuide()
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Knapperne følger trinnet: Næste / OK (tilbage til oversigten) / Gem ([Ret] fra fanen) / Kør automatisk.
|
||||
function SmartPickupFrame:getGuideButtons()
|
||||
local hooks, guide = self:guideHooks(), self.guide
|
||||
local i18n = self.i18n
|
||||
local isSummary = guide.step == "summary" and not guide.single
|
||||
local key = "spu_guide_next"
|
||||
if guide.single then
|
||||
key = guide.returnTo == "close" and "spu_guide_save" or "spu_guide_ok"
|
||||
elseif isSummary then
|
||||
key = hooks.startKey ~= nil and hooks.startKey(guide) or "spu_guide_save"
|
||||
end
|
||||
self.guideNextButtonInfo.text = i18n:getText(key)
|
||||
self.guideNextButtonInfo.disabled = hooks.canAdvance ~= nil and not hooks.canAdvance(guide)
|
||||
if isSummary and (hooks.showSaveOnly == nil or hooks.showSaveOnly(guide)) then
|
||||
return self.guideSummaryButtons
|
||||
end
|
||||
return self.guideButtons
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Smart Pickup — fanen Måned
|
||||
-- Hvad skal nås før månedsskiftet? Høst og såning har korte vinduer; fanen viser markerne med arbejde i
|
||||
-- gang, fremdrift, næste trin og om vinduet lukker. Kun visning — reglerne ligger i ADMonthView.
|
||||
|
||||
-- Periodens navn: spillets egen tekst (fx "Sen sommer") hvis den findes, ellers "Periode 6".
|
||||
-- Periode 1 = tidligt forår = marts (nordlig halvkugle) -> "(aug)".
|
||||
function SmartPickupFrame:monthPeriodText(period)
|
||||
local month = (period + 1) % 12 + 1
|
||||
local names = {}
|
||||
for name in string.gmatch(self.i18n:getText("spu_month_months"), "[^,]+") do table.insert(names, name) end
|
||||
local suffix = names[month] ~= nil and (" (" .. names[month] .. ")") or ""
|
||||
local ok, text = pcall(function() return g_i18n:formatPeriod(period, false) end)
|
||||
if ok and type(text) == "string" and text ~= "" then return text .. suffix end
|
||||
return string.format(self.i18n:getText("spu_month_period"), period) .. suffix
|
||||
end
|
||||
|
||||
-- {period, day, days, hour, minute}; nil hvis miljøet ikke kan læses.
|
||||
function SmartPickupFrame.monthClock()
|
||||
local ok, clock = pcall(function()
|
||||
local environment = g_currentMission.environment
|
||||
local dayTime = environment.dayTime or 0
|
||||
return {period = environment.currentPeriod, day = environment.currentDayInPeriod, days = environment.daysPerPeriod,
|
||||
hour = math.floor(dayTime / 3600000), minute = math.floor(dayTime / 60000) % 60}
|
||||
end)
|
||||
if ok and type(clock) == "table" and type(clock.period) == "number" then return clock end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Courseplays fremdrift for markens sæt (gennemsnit), nil hvis ingen melder noget.
|
||||
function SmartPickupFrame.monthCpProgress(fieldId)
|
||||
local sum, count = 0, 0
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||
local vehicle = assignment.vehicle
|
||||
if not assignment.helper and vehicle ~= nil and vehicle.getCpFieldWorkProgress ~= nil then
|
||||
local ok, progress = pcall(vehicle.getCpFieldWorkProgress, vehicle)
|
||||
if ok and type(progress) == "number" then
|
||||
sum, count = sum + progress, count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return count > 0 and sum / count or nil
|
||||
end
|
||||
|
||||
function SmartPickupFrame.monthContext(period)
|
||||
return {
|
||||
period = period,
|
||||
plantableIn = function(name, forPeriod)
|
||||
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
||||
return fruit ~= nil and ADCpBridge.isPlantableIn(fruit, forPeriod)
|
||||
end,
|
||||
withers = function(info, nextPeriod)
|
||||
local fruit = info.fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
|
||||
local ok, result = pcall(ADMonthView.withersIn, fruit, nextPeriod, info.growthState)
|
||||
return ok and result == true
|
||||
end,
|
||||
cpProgress = SmartPickupFrame.monthCpProgress,
|
||||
}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:monthStageText(stage)
|
||||
if stage == nil then return "–" end
|
||||
return self.i18n:getText("spu_field_stage_" .. tostring(stage))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:monthCells(row)
|
||||
local crop = row.sowFruit ~= nil and self:fruitTitle(row.sowFruit) or (row.crop or "")
|
||||
return {"F" .. tostring(row.id), self:monthStageText(row.stage), crop, ADMonthView.percentText(row.progress),
|
||||
self:monthStageText(row.nextStage), self.i18n:getText("spu_month_window_" .. row.window)}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillMonthRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
local clock = SmartPickupFrame.monthClock()
|
||||
if clock == nil then
|
||||
self:addTextRow(i18n:getText("spu_month_noClock"))
|
||||
return
|
||||
end
|
||||
self:addTextRow(string.format(i18n:getText("spu_month_header"), self:monthPeriodText(clock.period),
|
||||
clock.day or 1, clock.days or 1, clock.hour, clock.minute))
|
||||
local infos = ADFields.list()
|
||||
if #infos == 0 then
|
||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||
return
|
||||
end
|
||||
local view = ADMonthView.build(infos, ADFieldJobs.state.fields, SmartPickupFrame.monthContext(clock.period))
|
||||
if view.critical > 0 then
|
||||
self:addTextRow(string.format(i18n:getText("spu_month_verdictUrgent"), view.critical))
|
||||
else
|
||||
self:addTextRow(i18n:getText("spu_month_verdictCalm"))
|
||||
end
|
||||
if #view.rows == 0 then
|
||||
self:addTextRow(i18n:getText("spu_month_noWork"))
|
||||
else
|
||||
self:addCellRow(self.monthPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_month_col_work"), i18n:getText("spu_col_crop"),
|
||||
i18n:getText("spu_month_col_progress"), i18n:getText("spu_month_col_next"), i18n:getText("spu_month_col_window")})
|
||||
for _, row in ipairs(view.rows) do
|
||||
self:addCellRow(self.monthPrefab, self:monthCells(row))
|
||||
end
|
||||
end
|
||||
if #view.upcoming > 0 then
|
||||
self:addTextRow(i18n:getText("spu_month_upcoming"))
|
||||
for _, entry in ipairs(view.upcoming) do
|
||||
local key = entry.closes and "spu_month_upcomingLast" or "spu_month_upcomingOpen"
|
||||
self:addTextRow(string.format(i18n:getText(key), entry.id, self:fruitTitle(entry.fruit)))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -57,66 +57,9 @@ function SmartPickupFrame.getPolicyIndex(policies, policy)
|
||||
return 1
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillSourceRows(adEnv, run)
|
||||
local chosen = {}
|
||||
for _, id in ipairs(run.sourceWayPointIds or {}) do
|
||||
chosen[id] = true
|
||||
end
|
||||
local entries = ADRunsController.getSourceMarkers(adEnv, g_currentMission:getFarmId())
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "source", wayPointId = entry.marker.id})
|
||||
local kindText = self.i18n:getText("spu_source_kind_" .. tostring(entry.kind))
|
||||
row:getDescendantByName("title"):setText(string.format(" %s — %s (%s)", tostring(entry.marker.name), tostring(entry.name), kindText))
|
||||
row:getDescendantByName("option"):setState(chosen[entry.marker.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
if #entries == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_noSources"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillProductRows(adEnv, run)
|
||||
self.productRows = {}
|
||||
local products = ADRunsController.getRunProducts(adEnv, run)
|
||||
for _, product in ipairs(products) do
|
||||
local policies = SmartPickupFrame.getPoliciesFor(product)
|
||||
local policyTexts = {}
|
||||
for _, policy in ipairs(policies) do
|
||||
table.insert(policyTexts, self.i18n:getText("spu_policy_" .. policy))
|
||||
end
|
||||
local row = self:addRow(self.productPrefab, {kind = "product", name = product.name, policies = policies})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts(policyTexts)
|
||||
option:setState(SmartPickupFrame.getPolicyIndex(policies, product.policy), false)
|
||||
local title = row:getDescendantByName("title")
|
||||
title:setText(self:describeProduct(adEnv, product))
|
||||
table.insert(self.productRows, {name = product.name, title = title, option = option})
|
||||
end
|
||||
if #products == 0 then
|
||||
self:addTextRow(#(run.sourceWayPointIds or {}) == 0 and self.i18n:getText("spu_noSources") or self.i18n:getText("spu_noProducts"))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillOutboundStatusRows(adEnv, run)
|
||||
self.outboundStatusRows = {}
|
||||
local status = ADRunsController.getOutboundStatus(adEnv, run)
|
||||
for _, reason in ipairs(status.reasons or {}) do
|
||||
table.insert(self.outboundStatusRows, self:addTextRow(string.format(self.i18n:getText("spu_outbound_waiting"), reason)))
|
||||
end
|
||||
if status.warning ~= nil then
|
||||
table.insert(self.outboundStatusRows, self:addTextRow(string.format(self.i18n:getText("spu_outbound_warning"), status.warning)))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:rebuildOutboundTab(adEnv, run)
|
||||
self.sellAtOption:setState(SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT), false)
|
||||
self.storeAboveOption:setState(SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT), false)
|
||||
self.collectRoundRowOption = self:addToggleRow("collectRound", self.i18n:getText("spu_collectRound"), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, run.collectRound == true)
|
||||
self:addTextRow(self.i18n:getText("spu_section_sources"))
|
||||
self:fillSourceRows(adEnv, run)
|
||||
self:addTextRow(self.i18n:getText("spu_section_products"))
|
||||
self:fillProductRows(adEnv, run)
|
||||
self:fillOutboundStatusRows(adEnv, run)
|
||||
end
|
||||
|
||||
-- Hvert sekund: produktrækkernes mængde/pris og statuslinjerne (antal linjer ændres kun ved rebuild).
|
||||
function SmartPickupFrame:refreshOutboundRows(adEnv, run)
|
||||
@@ -167,21 +110,21 @@ end
|
||||
|
||||
-- ---- callbacks ----
|
||||
|
||||
function SmartPickupFrame:onClickSource(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, run.id, data.wayPointId)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': sources=%s", tostring(run.name), table.concat(ADRuns.getRun(ADRunsController.store, run.id).sourceWayPointIds, " "))
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickProduct(state, element)
|
||||
local clicked = self.rowData[element]
|
||||
if clicked ~= nil and clicked.kind == "guideRow" then
|
||||
return self:onClickGuideRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "storageView" then
|
||||
return self:safe(function(frame) frame:onClickStorageView(state) end)
|
||||
end
|
||||
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
||||
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
||||
end
|
||||
if clicked ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[clicked.kind] then
|
||||
return self:onClickFieldSiloRow(state, clicked)
|
||||
end
|
||||
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
||||
return self:onClickFieldRow(state, clicked)
|
||||
end
|
||||
@@ -210,10 +153,3 @@ function SmartPickupFrame:onClickStoreAbove(state)
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickCollectRound(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {collectRound = state == SmartPickupFrame.STATE_ON})
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -128,6 +128,7 @@ end
|
||||
|
||||
function SmartPickupFrame:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||
self.storageDetailRows = {}
|
||||
self.storageDetailPlaceable = nil
|
||||
if #inventory == 0 then
|
||||
return
|
||||
end
|
||||
@@ -177,6 +178,12 @@ function SmartPickupFrame:fillStorageRows(adEnv)
|
||||
self.storageRows = {}
|
||||
local fullInventory = self:getInventory(adEnv)
|
||||
local inventory = ADStorageStats.filterKind(fullInventory, SmartPickupFrame.getTabKind(self.currentTab))
|
||||
self:addStorageViewRow()
|
||||
if self:getStorageView() == SmartPickupFrame.STORAGE_VIEW_ITEMS then
|
||||
self.storageDetailPlaceable = nil
|
||||
self:fillItemOverview(adEnv, inventory, fullInventory)
|
||||
return
|
||||
end
|
||||
self:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||
self:addTextRow(self.i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))))
|
||||
for _, entry in ipairs(inventory) do
|
||||
@@ -193,6 +200,11 @@ end
|
||||
|
||||
function SmartPickupFrame:refreshStorageRows(adEnv)
|
||||
local kind = SmartPickupFrame.getTabKind(self.currentTab)
|
||||
if kind ~= nil and self.itemView ~= nil then
|
||||
local fullInventory = self:getInventory(adEnv)
|
||||
self:refreshItemOverview(adEnv, ADStorageStats.filterKind(fullInventory, kind), fullInventory)
|
||||
return
|
||||
end
|
||||
if kind == nil or #(self.storageRows or {}) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
-- Smart Pickup — fanerne Lagre, vareoversigten (v1.52)
|
||||
-- Én række pr. vare med samlet beholdning, fyldningsbjælke, værdi og gårdens netto; klik folder varen ud
|
||||
-- til bygningerne den ligger i. Øverst nøgletal og "kræver handling". Tallene regnes i ADStorageStats;
|
||||
-- her tegnes kun. Indlæses efter SmartPickupStorageTab.lua (funktionerne lægges på SmartPickupFrame).
|
||||
|
||||
SmartPickupFrame.STORAGE_VIEW_ITEMS, SmartPickupFrame.STORAGE_VIEW_BUILDINGS = 1, 2
|
||||
SmartPickupFrame.MAX_ATTENTION = 4
|
||||
-- nøgletalsrækken er højere end de andre rækker (tal over label)
|
||||
SmartPickupFrame.KPI_ROW_SCALE = 1.6
|
||||
-- ændret struktur (ny vare, flere alarmer) genopbygges højst hvert N. sekund, så fokus ikke hopper
|
||||
SmartPickupFrame.ITEM_REBUILD_TICKS = 10
|
||||
|
||||
SmartPickupFrame.LEVEL_COLORS = {
|
||||
red = {0.90, 0.25, 0.20, 1},
|
||||
yellow = {0.95, 0.72, 0.15, 1},
|
||||
green = {0.30, 0.75, 0.35, 1},
|
||||
}
|
||||
SmartPickupFrame.NEUTRAL_BAR_COLOR = {0.35, 0.60, 0.85, 1}
|
||||
SmartPickupFrame.TEXT_COLOR = {1, 1, 1, 1}
|
||||
|
||||
local unpackFn = table.unpack or unpack
|
||||
|
||||
-- ---- små tegne-hjælpere ----
|
||||
|
||||
local function setTextColor(element, color)
|
||||
if element ~= nil and element.setTextColor ~= nil then element:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
|
||||
-- Fyld bjælken til brøken af baggrundens bredde; tom bjælke skjules (bredde 0 tegnes som en prik).
|
||||
function SmartPickupFrame.setBar(bar, background, fraction, color)
|
||||
if bar == nil or background == nil or background.size == nil then return end
|
||||
if fraction <= 0 then
|
||||
bar:setVisible(false)
|
||||
return
|
||||
end
|
||||
bar:setVisible(true)
|
||||
bar:setSize(background.size[1] * fraction, background.size[2])
|
||||
if bar.setImageColor ~= nil then bar:setImageColor(nil, unpackFn(color)) end
|
||||
end
|
||||
|
||||
-- Bjælkens farve: rød/gul ved problem, ellers grøn for varer i omløb og blå for rent lager.
|
||||
function SmartPickupFrame.barColor(level, isActive)
|
||||
if level ~= nil then return SmartPickupFrame.LEVEL_COLORS[level] end
|
||||
return isActive and SmartPickupFrame.LEVEL_COLORS.green or SmartPickupFrame.NEUTRAL_BAR_COLOR
|
||||
end
|
||||
|
||||
function SmartPickupFrame:formatDays(days)
|
||||
return string.format(self.i18n:getText("spu_stat_days"), days)
|
||||
end
|
||||
|
||||
-- "−14.4k/døgn · 3.2 døgn", "+3.6k/døgn · fuld om 2.0 døgn", "+1.2k/døgn · stiger" eller "-"
|
||||
function SmartPickupFrame:itemRateText(stat)
|
||||
local fc = SmartPickupFrame.formatCompact
|
||||
if (stat.net or 0) == 0 then
|
||||
return (stat.consumption or 0) > 0 and self.i18n:getText("spu_item_balanced") or "-"
|
||||
end
|
||||
local rate = string.format(self.i18n:getText("spu_item_perDay"), (stat.net < 0 and "-" or "+") .. fc(math.abs(stat.net)))
|
||||
if stat.days ~= nil then
|
||||
return rate .. " · " .. self:formatDays(stat.days)
|
||||
elseif stat.daysToFull ~= nil then
|
||||
return rate .. " · " .. string.format(self.i18n:getText("spu_item_fullIn"), self:formatDays(stat.daysToFull))
|
||||
end
|
||||
return rate .. " · " .. self.i18n:getText("spu_stat_rising")
|
||||
end
|
||||
|
||||
-- "1.2M/1.5M · 80 %" eller "24 paller · 1 %"
|
||||
function SmartPickupFrame:itemAmountText(entry)
|
||||
local fc = SmartPickupFrame.formatCompact
|
||||
local amount = entry.count ~= nil and string.format(self.i18n:getText("spu_storage_pallets"), entry.count)
|
||||
or string.format("%s/%s", fc(entry.level or 0), fc(entry.capacity or 0))
|
||||
if entry.percent ~= nil then amount = amount .. string.format(" · %d %%", entry.percent) end
|
||||
return amount
|
||||
end
|
||||
|
||||
-- ---- data ----
|
||||
|
||||
-- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården.
|
||||
function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory)
|
||||
self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local totals = ADStorageStats.farmTotals(fullInventory)
|
||||
local capacities = ADStorageStats.farmCapacities(fullInventory)
|
||||
local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {}
|
||||
local stats = {}
|
||||
local titleOf = function(fillType) return self:fillTypeTitle(fillType) end
|
||||
for _, group in ipairs(ADStorageStats.groupByFillType(inventory, titleOf)) do
|
||||
local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(group.fillType) or nil
|
||||
local farm = {level = totals[group.fillType], capacity = capacities[group.fillType]}
|
||||
table.insert(stats, ADStorageStats.groupStats(group, farm, rates[group.fillType], quote))
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Nøgle der ændrer sig når rækkerne skal være anderledes (varer, udfoldede bygninger, alarmer).
|
||||
function SmartPickupFrame:itemStructureKey(stats, attention)
|
||||
local parts = {}
|
||||
for _, stat in ipairs(stats) do
|
||||
table.insert(parts, tostring(stat.fillType))
|
||||
if self:isItemExpanded(stat.fillType) then
|
||||
-- sorteret: at to bygninger bytter plads i mængde må ikke udløse en genopbygning
|
||||
local placeables = {}
|
||||
for _, building in ipairs(stat.buildings) do table.insert(placeables, tostring(building.placeable)) end
|
||||
table.sort(placeables)
|
||||
table.insert(parts, table.concat(placeables, ";"))
|
||||
end
|
||||
end
|
||||
table.insert(parts, "a" .. #attention)
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:isItemExpanded(fillType)
|
||||
local expanded = self.expandedItems ~= nil and self.expandedItems[self.currentTab] or nil
|
||||
return expanded ~= nil and expanded[fillType] == true
|
||||
end
|
||||
|
||||
function SmartPickupFrame:toggleItem(fillType)
|
||||
self.expandedItems = self.expandedItems or {}
|
||||
self.expandedItems[self.currentTab] = self.expandedItems[self.currentTab] or {}
|
||||
local expanded = self.expandedItems[self.currentTab]
|
||||
expanded[fillType] = not expanded[fillType] or nil
|
||||
-- listen hopper ellers til toppen ved genopbygningen
|
||||
local layout = self.boxLayout
|
||||
local scrollY = layout ~= nil and layout.firstVisibleY or nil
|
||||
self:rebuild()
|
||||
if scrollY ~= nil and layout.scrollTo ~= nil then pcall(layout.scrollTo, layout, scrollY, true) end
|
||||
end
|
||||
|
||||
-- ---- visningsvælger (Varer | Bygninger) ----
|
||||
|
||||
function SmartPickupFrame:getStorageView()
|
||||
local views = self.storageViewByTab or {}
|
||||
return views[self.currentTab] or SmartPickupFrame.STORAGE_VIEW_ITEMS
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addStorageViewRow()
|
||||
local row = self:addRow(self.productPrefab, {kind = "storageView"})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts({self.i18n:getText("spu_view_items"), self.i18n:getText("spu_view_buildings")})
|
||||
option:setState(self:getStorageView(), false)
|
||||
row:getDescendantByName("title"):setText(self.i18n:getText("spu_view_title"))
|
||||
local tooltip = option:getDescendantByName("ignore")
|
||||
if tooltip ~= nil then tooltip:setText(self.i18n:getText("spu_view_tooltip")) end
|
||||
option.toolTipText = self.i18n:getText("spu_view_tooltip")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickStorageView(state)
|
||||
self.storageViewByTab = self.storageViewByTab or {}
|
||||
self.storageViewByTab[self.currentTab] = state
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- ---- opbygning ----
|
||||
|
||||
function SmartPickupFrame:kpiTexts(stats)
|
||||
local kpi = ADStorageStats.kpis(stats)
|
||||
local i18n = self.i18n
|
||||
return {
|
||||
{SmartPickupFrame.formatMoney(kpi.valueNow), i18n:getText("spu_kpi_value")},
|
||||
{"+" .. SmartPickupFrame.formatMoney(kpi.gain), i18n:getText("spu_kpi_gain")},
|
||||
{tostring(kpi.emptyCount), i18n:getText("spu_kpi_empty")},
|
||||
{tostring(kpi.fullCount), i18n:getText("spu_kpi_full")},
|
||||
}, kpi
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setKpiRow(row, stats)
|
||||
local texts, kpi = self:kpiTexts(stats)
|
||||
for index, pair in ipairs(texts) do
|
||||
local value = row:getDescendantByName("v" .. index)
|
||||
if value ~= nil then value:setText(pair[1]) end
|
||||
local label = row:getDescendantByName("l" .. index)
|
||||
if label ~= nil then label:setText(pair[2]) end
|
||||
end
|
||||
local colors = SmartPickupFrame.LEVEL_COLORS
|
||||
setTextColor(row:getDescendantByName("v2"), kpi.gain > 0 and colors.green or SmartPickupFrame.TEXT_COLOR)
|
||||
setTextColor(row:getDescendantByName("v3"), kpi.emptyCount > 0 and colors.red or SmartPickupFrame.TEXT_COLOR)
|
||||
setTextColor(row:getDescendantByName("v4"), kpi.fullCount > 0 and colors.yellow or SmartPickupFrame.TEXT_COLOR)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addKpiRow(stats)
|
||||
local row = self:addRow(self.kpiPrefab, {kind = "text"})
|
||||
if row.size ~= nil then row:setSize(row.size[1], row.size[2] * SmartPickupFrame.KPI_ROW_SCALE) end
|
||||
self:setKpiRow(row, stats)
|
||||
return row
|
||||
end
|
||||
|
||||
function SmartPickupFrame:alertText(entry)
|
||||
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
||||
if entry.kind == "empty" then
|
||||
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
|
||||
elseif entry.kind == "full" then
|
||||
if stat.daysToFull ~= nil then
|
||||
return string.format(i18n:getText("spu_alert_fullIn"), stat.title, stat.percent or 0, self:formatDays(stat.daysToFull))
|
||||
end
|
||||
return string.format(i18n:getText("spu_alert_full"), stat.title, stat.percent or 0)
|
||||
end
|
||||
return string.format(i18n:getText("spu_alert_sell"), stat.title, math.floor((stat.priceRatio or 0) * 100 + 0.5), SmartPickupFrame.formatMoney(stat.valueNow))
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setAlertRow(row, entry)
|
||||
row.entry = entry
|
||||
local hint = ""
|
||||
if SmartPickupFrame.attentionAdvice ~= nil then
|
||||
local ok, advice, info = pcall(self.attentionAdvice, self, entry)
|
||||
if ok then hint = self:attentionHint(advice, info) end
|
||||
end
|
||||
row.c1:setText(self:alertText(entry) .. hint)
|
||||
if row.dot ~= nil and row.dot.setImageColor ~= nil then
|
||||
row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green))
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:itemCells(stat)
|
||||
local marker = self:isItemExpanded(stat.fillType) and "– " or "+ "
|
||||
local value = stat.valueNow ~= nil and SmartPickupFrame.formatMoney(stat.valueNow) or self.i18n:getText("spu_stat_noSell")
|
||||
return {marker .. stat.title, self:itemAmountText(stat), value, self:itemRateText(stat)}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:setItemRow(row, stat)
|
||||
SmartPickupFrame.setCells(row.cells, self:itemCells(stat))
|
||||
local isActive = (stat.consumption or 0) > 0 or (stat.production or 0) > 0
|
||||
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(stat.percent), SmartPickupFrame.barColor(stat.severity, isActive))
|
||||
local rateColor = SmartPickupFrame.TEXT_COLOR
|
||||
if stat.emptyLevel ~= nil then
|
||||
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.emptyLevel]
|
||||
elseif stat.fullLevel ~= nil then
|
||||
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.fullLevel]
|
||||
end
|
||||
setTextColor(row.cells[4], rateColor)
|
||||
end
|
||||
|
||||
-- Bygningens andel af varen: værdi efter samme pris som varen, bjælke efter bygningens egen fyldning.
|
||||
function SmartPickupFrame:setSubRow(row, stat, building)
|
||||
local kind = SmartPickupFrame.getTabKind(self.currentTab)
|
||||
local isStableOutput = kind == "husbandry" and (stat.production or 0) > 0 and (stat.consumption or 0) == 0
|
||||
local value = "-"
|
||||
if stat.valueNow ~= nil and stat.level > 0 then
|
||||
value = SmartPickupFrame.formatMoney(stat.valueNow * building.level / stat.level)
|
||||
end
|
||||
local share = stat.level > 0 and math.floor(100 * building.level / stat.level + 0.5) or 0
|
||||
SmartPickupFrame.setCells(row.cells, {tostring(building.name), self:itemAmountText(building), value,
|
||||
string.format(self.i18n:getText("spu_item_share"), share)})
|
||||
local level = ADStorageStats.buildingLevel(building, kind, isStableOutput)
|
||||
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(building.percent), SmartPickupFrame.barColor(level, false))
|
||||
end
|
||||
|
||||
-- Klon af item-/sub-prefab med referencer til cellerne og bjælken.
|
||||
function SmartPickupFrame:addBarRow(prefab)
|
||||
local row = self:addRow(prefab, {kind = "text"})
|
||||
local cells = {}
|
||||
for index = 1, 4 do cells[index] = row:getDescendantByName("c" .. index) end
|
||||
return {element = row, cells = cells, bar = row:getDescendantByName("bar"), barBg = row:getDescendantByName("barBg")}
|
||||
end
|
||||
|
||||
function SmartPickupFrame:addItemHeader()
|
||||
local i18n = self.i18n
|
||||
local header = self:addBarRow(self.itemPrefab)
|
||||
SmartPickupFrame.setCells(header.cells, {i18n:getText("spu_col_item"), i18n:getText("spu_icol_amount"),
|
||||
i18n:getText("spu_icol_value"), i18n:getText("spu_icol_rate")})
|
||||
if header.bar ~= nil then header.bar:setVisible(false) end
|
||||
if header.barBg ~= nil then header.barBg:setVisible(false) end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
|
||||
local i18n = self.i18n
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
|
||||
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
|
||||
self:addTextRow(i18n:getText("spu_attention"))
|
||||
if #attention == 0 then
|
||||
self:addTextRow(i18n:getText("spu_attention_none"))
|
||||
end
|
||||
for _, entry in ipairs(attention) do
|
||||
local element = self:addRow(self.alertPrefab, {kind = "text"})
|
||||
local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")}
|
||||
self:setAlertRow(row, entry)
|
||||
-- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua)
|
||||
self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end)
|
||||
table.insert(self.itemView.alertRows, row)
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint"))
|
||||
if #stats == 0 then
|
||||
self:addTextRow(i18n:getText("spu_storage_empty"))
|
||||
return
|
||||
end
|
||||
self:addItemHeader()
|
||||
for _, stat in ipairs(stats) do
|
||||
local row = self:addBarRow(self.itemPrefab)
|
||||
local fillType = stat.fillType
|
||||
self:makeRowClickable(row.element, function(frame) frame:toggleItem(fillType) end)
|
||||
self:setItemRow(row, stat)
|
||||
row.subs = {}
|
||||
if self:isItemExpanded(fillType) then
|
||||
for _, building in ipairs(stat.buildings) do
|
||||
local sub = self:addBarRow(self.subPrefab)
|
||||
sub.placeable = building.placeable
|
||||
self:setSubRow(sub, stat, building)
|
||||
table.insert(row.subs, sub)
|
||||
end
|
||||
end
|
||||
table.insert(self.itemView.itemRows, row)
|
||||
end
|
||||
end
|
||||
|
||||
-- Hvert sekund: tal og bjælker opdateres på stedet; ny struktur -> genopbygning (højst hvert 10. s).
|
||||
function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
|
||||
local view = self.itemView
|
||||
if view == nil then return end
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
if self:itemStructureKey(stats, attention) ~= view.key then
|
||||
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
|
||||
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
|
||||
return
|
||||
end
|
||||
self:setKpiRow(view.kpiRow, stats)
|
||||
for index, entry in ipairs(attention) do self:setAlertRow(view.alertRows[index], entry) end
|
||||
for index, stat in ipairs(stats) do
|
||||
local row = view.itemRows[index]
|
||||
self:setItemRow(row, stat)
|
||||
-- underrækkerne beholder rækkefølgen fra opbygningen; tallene findes via bygningen
|
||||
local byPlaceable = {}
|
||||
for _, building in ipairs(stat.buildings) do byPlaceable[building.placeable] = building end
|
||||
for _, sub in ipairs(row.subs) do
|
||||
local building = byPlaceable[sub.placeable]
|
||||
if building ~= nil then self:setSubRow(sub, stat, building) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,42 +2,8 @@
|
||||
-- Mål, varer (Auto/Vælg), behovsrække, ture og "kør hjem"-tærskel for et forsynings-run.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
function SmartPickupFrame:rebuildSupplyTab(adEnv, run)
|
||||
self.loopsOption:setState(run.loops + 1, false)
|
||||
self.returnBelowOption:setState(self:getReturnBelowIndex(run), false)
|
||||
self:addTextRow(self.i18n:getText("spu_section_targets"))
|
||||
self.ingredientModeRowOption = self:addToggleRow("ingredientMode", self.i18n:getText("spu_ingredientMode"), {self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")}, run.ingredientMode == "manual")
|
||||
self:addTextRow(self.i18n:getText("spu_targets"))
|
||||
self:fillTargetRows(adEnv, run)
|
||||
self:fillIngredientRows(adEnv, run)
|
||||
self:fillNeedsRows(adEnv, run)
|
||||
end
|
||||
|
||||
-- Mål = afkrydsning (flere stalde/fabrikker pr. run). Ved hver tur vælges den der mangler mest.
|
||||
function SmartPickupFrame:fillTargetRows(adEnv, run)
|
||||
local chosen = {}
|
||||
for _, id in ipairs(ADRunsController.getRunTargets(run)) do
|
||||
chosen[id] = true
|
||||
end
|
||||
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, g_currentMission:getFarmId())) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "target", wayPointId = marker.id})
|
||||
row:getDescendantByName("title"):setText(" " .. tostring(marker.name))
|
||||
row:getDescendantByName("option"):setState(chosen[marker.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Behovsrække pr. mål ("Cow 1 Food: Forage 0/33.750 l · …"), opdateres hvert sekund.
|
||||
function SmartPickupFrame:fillNeedsRows(adEnv, run)
|
||||
self.needsRows = {}
|
||||
local targets = ADRunsController.getTargetsLevels(adEnv, run)
|
||||
if #targets == 0 then
|
||||
self:addTextRow(self.i18n:getText("spu_needs_noTarget"))
|
||||
return
|
||||
end
|
||||
for _, target in ipairs(targets) do
|
||||
table.insert(self.needsRows, {wayPointId = target.wayPointId, title = self:addTextRow(self:describeTargetNeeds(target))})
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:describeTargetNeeds(target)
|
||||
if #target.levels == 0 then
|
||||
@@ -58,39 +24,8 @@ function SmartPickupFrame:refreshNeedsRows(adEnv, run)
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillIngredientRows(adEnv, run)
|
||||
if run.ingredientMode ~= "manual" then
|
||||
return
|
||||
end
|
||||
local chosen = {}
|
||||
for _, name in ipairs(run.ingredients) do
|
||||
chosen[name] = true
|
||||
end
|
||||
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, run)) do
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "ingredient", name = fillType.name})
|
||||
row:getDescendantByName("title"):setText(tostring(fillType.title))
|
||||
row:getDescendantByName("option"):setState(chosen[fillType.name] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickTarget(state, element)
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil then return end
|
||||
ADRunsController.store = ADRuns.toggleTarget(ADRunsController.store, run.id, data.wayPointId)
|
||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': targets=%s", tostring(run.name), table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, run.id)), " "))
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickIngredientMode(state)
|
||||
self:safe(function(frame)
|
||||
local run = frame:getRun()
|
||||
if run == nil then return end
|
||||
ADRunsController.updateRun(run.id, {ingredientMode = state == SmartPickupFrame.STATE_ON and "manual" or "auto"})
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:onClickLoops(state)
|
||||
self:safe(function(frame)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<Profile name="spuInfoText" extends="fs25_settingsMultiTextOptionTitle">
|
||||
<textBold value="false"/>
|
||||
<size value="1400px 40px"/>
|
||||
<textMaxWidth value="800px"/>
|
||||
</Profile>
|
||||
<!-- Tabel-celler i fanen Lagre: position og bredde sættes pr. celle i SmartPickupFrame.xml. -->
|
||||
<Profile name="spuCell" extends="fs25_settingsMultiTextOptionTitle">
|
||||
@@ -19,4 +20,53 @@
|
||||
<Profile name="spuCellBold" extends="spuCell">
|
||||
<textBold value="true"/>
|
||||
</Profile>
|
||||
<!-- Lagre (v1.52): bjælker = hvid pixel i spillets UI-atlas farvet med imageColor
|
||||
(samme teknik som Precision Farming). Bredden på fyldet sættes i Lua. -->
|
||||
<Profile name="spuBar" extends="baseReference" with="anchorMiddleLeft">
|
||||
<imageUVs value="10px 1010px 4px 4px"/>
|
||||
<positionOrigin value="middleLeft"/>
|
||||
<imageColor value="0.30 0.75 0.35 1"/>
|
||||
<size value="200px 16px"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarBg" extends="spuBar">
|
||||
<imageColor value="0 0 0 0.45"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarThin" extends="spuBar">
|
||||
<size value="200px 10px"/>
|
||||
</Profile>
|
||||
<Profile name="spuBarThinBg" extends="spuBarThin">
|
||||
<imageColor value="0 0 0 0.45"/>
|
||||
</Profile>
|
||||
<!-- markør foran en "kræver handling"-linje -->
|
||||
<Profile name="spuDot" extends="spuBar">
|
||||
<size value="8px 26px"/>
|
||||
</Profile>
|
||||
<!-- nøgletal: stort tal over lille label -->
|
||||
<Profile name="spuKpiValue" extends="spuCellBold">
|
||||
<textSize value="24px"/>
|
||||
<textOffset value="0px 8px"/>
|
||||
</Profile>
|
||||
<Profile name="spuKpiLabel" extends="spuCell">
|
||||
<textSize value="14px"/>
|
||||
<textOffset value="0px -18px"/>
|
||||
<textColor value="0.75 0.75 0.75 1"/>
|
||||
</Profile>
|
||||
<Profile name="spuCellDim" extends="spuCell">
|
||||
<textColor value="0.75 0.75 0.75 1"/>
|
||||
</Profile>
|
||||
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser/placering sættes i Lua ud fra boxLayout -->
|
||||
<Profile name="spuOverlayBackdrop" extends="baseReference" with="anchorTopLeft">
|
||||
<imageUVs value="10px 1010px 4px 4px"/>
|
||||
<positionOrigin value="bottomLeft"/>
|
||||
<imageColor value="0 0 0 0.65"/>
|
||||
<size value="100px 100px"/>
|
||||
</Profile>
|
||||
<!-- farver er lineære: 0.016 ≈ menuens egen mørke baggrund (0.09 blev lysegrå, in-game 29/9) -->
|
||||
<Profile name="spuOverlayPanel" extends="spuOverlayBackdrop">
|
||||
<imageColor value="0.016 0.016 0.016 1"/>
|
||||
</Profile>
|
||||
<Profile name="spuPanelTitle" extends="spuCellBold">
|
||||
<textSize value="20px"/>
|
||||
<size value="700px 30px"/>
|
||||
</Profile>
|
||||
</GUIProfiles>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<text name="spu_title" text="Smart Pickup"/>
|
||||
<text name="spu_run" text="Run"/>
|
||||
<text name="spu_run_tooltip" text="Vælg det run du vil se eller ændre."/>
|
||||
<text name="spu_noRuns" text="Ingen runs — tryk 'Nyt run'"/>
|
||||
<text name="spu_noRuns" text="Ingen flows — tryk 'Nyt flow'"/>
|
||||
<text name="spu_target" text="Mål"/>
|
||||
<text name="spu_target_tooltip" text="AutoDrive-markør ved stalden eller fabrikken der skal fyldes."/>
|
||||
<text name="spu_noTarget" text="(vælg mål)"/>
|
||||
@@ -27,7 +27,10 @@
|
||||
<text name="spu_status_started" text="Startet %d, sprunget over %d"/>
|
||||
<text name="spu_status_noAd" text="AutoDrive-API ikke fundet"/>
|
||||
<text name="spu_status_error" text="Fejl: %s"/>
|
||||
<text name="spu_reason_noTrailer" text="ingen vogn"/>
|
||||
<text name="spu_guide_flow_wagon" text=" ↳ Vogn"/>
|
||||
<text name="spu_guide_flow_noFreeWagon" text=" ↳ ingen ledig vogn (frakoblet, med Connect-markør, kan bære varen)"/>
|
||||
<text name="spu_guide_flow_soloText" text="%s — ingen vogn bagpå: henter en ledig vogn"/>
|
||||
<text name="spu_reason_noTrailer" text="ingen vogn (kobl en på, eller giv vognens parkering flowets navn)"/>
|
||||
<text name="spu_reason_cannotCarry" text="vognen kan ikke bære run'ets varer"/>
|
||||
<text name="spu_reason_farFromNetwork" text="for langt fra vejnettet"/>
|
||||
<text name="spu_reason_noTarget" text="intet mål"/>
|
||||
@@ -40,7 +43,28 @@
|
||||
<text name="spu_status_stoppedAll" text="Stoppet %d traktorer"/>
|
||||
<text name="spu_overview_hint" text="Klik på et flow for at redigere det"/>
|
||||
<text name="spu_tab_fields" text="Marker"/>
|
||||
<text name="spu_tab_month" text="Måned"/>
|
||||
<text name="spu_month_months" text="jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec"/>
|
||||
<text name="spu_month_period" text="Periode %d"/>
|
||||
<text name="spu_month_header" text="%s · dag %d af %d · kl. %02d:%02d"/>
|
||||
<text name="spu_month_noClock" text="Kan ikke læse spillets kalender"/>
|
||||
<text name="spu_month_verdictCalm" text="Intet haster — du kan roligt sove til næste måned"/>
|
||||
<text name="spu_month_verdictUrgent" text="OBS: %d marker skal nå det før månedsskiftet"/>
|
||||
<text name="spu_month_noWork" text="Ingen marker har arbejde i gang"/>
|
||||
<text name="spu_month_col_work" text="Arbejde"/>
|
||||
<text name="spu_month_col_progress" text="Færdig"/>
|
||||
<text name="spu_month_col_next" text="Næste"/>
|
||||
<text name="spu_month_col_window" text="Vindue"/>
|
||||
<text name="spu_month_window_ok" text="ok"/>
|
||||
<text name="spu_month_window_harvestCloses" text="OBS: visner snart"/>
|
||||
<text name="spu_month_window_lastSowMonth" text="OBS: sidste såmåned"/>
|
||||
<text name="spu_month_upcoming" text="Kan sås nu, ikke gået i gang:"/>
|
||||
<text name="spu_month_upcomingOpen" text="F%d: %s — kan også sås næste måned"/>
|
||||
<text name="spu_month_upcomingLast" text="F%d: %s — OBS: sidste såmåned"/>
|
||||
<text name="spu_field_button_start" text="Start nu"/>
|
||||
<text name="spu_field_manualTransport" text=" %s kører selv på vejen (Nej = for bred, du kører den til marken)"/>
|
||||
<text name="spu_field_button_takeover" text="Start nu"/>
|
||||
<text name="spu_field_takeoverRequested" text="F%d: overtager høsteren nu, hvis skærebordet passer"/>
|
||||
<text name="spu_field_button_stop" text="Stop mark"/>
|
||||
<text name="spu_field_noMarker" text="! mangler markør F%d"/>
|
||||
<text name="spu_field_stage_idle" text="Venter"/>
|
||||
@@ -51,8 +75,45 @@
|
||||
<text name="spu_field_stage_collecting" text="Samles op"/>
|
||||
<text name="spu_field_stage_done" text="Færdig"/>
|
||||
<text name="spu_field_reason_noRig" text="ingen %s med flueben i markflåden"/>
|
||||
<text name="spu_field_reasonShort_noRig" text="ingen maskine"/>
|
||||
<text name="spu_field_reasonShort_allBusy" text="venter på maskine"/>
|
||||
<text name="spu_field_reasonShort_tooWide" text="for bred"/>
|
||||
<text name="spu_field_reasonShort_widthUnknown" text="bredde ukendt"/>
|
||||
<text name="spu_field_reasonShort_tooNarrow" text="for smal"/>
|
||||
<text name="spu_field_reasonShort_cannotDo" text="ingen kan"/>
|
||||
<text name="spu_field_reasonShort_noSowFruit" text="vælg afgrøde"/>
|
||||
<text name="spu_field_reasonShort_notEnough" text="for få ledige"/>
|
||||
<text name="spu_field_reasonShort_notPlantable" text="ikke såtid nu"/>
|
||||
<text name="spu_field_reasonShort_regrowing" text="venter på genvækst"/>
|
||||
<text name="spu_field_reasonShort_rain" text="venter på tørvejr"/>
|
||||
<text name="spu_field_reasonShort_text" text="se detaljer"/>
|
||||
<text name="spu_field_reason_allBusy" text="venter på ledig %s"/>
|
||||
<text name="spu_pool_section_tractors" text="Maskinpulje — traktorer (henter redskaber når ingen færdig kombination er ledig)"/>
|
||||
<text name="spu_pool_section_implements" text="Maskinpulje — redskaber (parkering "X" + markør "Connect X")"/>
|
||||
<text name="spu_pool_noTractors" text="ingen traktorer uden redskab"/>
|
||||
<text name="spu_pool_noImplements" text="ingen redskaber med en markrolle"/>
|
||||
<text name="spu_pool_tractor_free" text="ledig%.0s"/>
|
||||
<text name="spu_pool_tractor_noPark" text="mangler egen AD-parkering%.0s"/>
|
||||
<text name="spu_pool_tractor_player" text="spilleren kører den%.0s"/>
|
||||
<text name="spu_pool_tractor_returning" text="sætter redskab af og kører hjem%.0s"/>
|
||||
<text name="spu_pool_tractor_job" text="opgave F%s"/>
|
||||
<text name="spu_pool_tractor_busy" text="AutoDrive/Courseplay kører%.0s"/>
|
||||
<text name="spu_pool_tractor_notInFleet" text="ikke i markflåden (flueben)%.0s"/>
|
||||
<text name="spu_pool_tractor_offNetwork" text="står uden for AutoDrive-vejnettet%.0s"/>
|
||||
<text name="spu_pool_tractor_hitched" text="har et redskab på (kobl det af for at bruge den i puljen)%.0s"/>
|
||||
<text name="spu_pool_implement_flow" text="hører til flowet '%s'"/>
|
||||
<text name="spu_pool_implement_parked" text="parkeret ved %s"/>
|
||||
<text name="spu_pool_implement_noPark" text="ikke i puljen — mangler AD-parkering%.0s"/>
|
||||
<text name="spu_pool_implement_noConnect" text="ikke i puljen — mangler markøren "Connect %s""/>
|
||||
<text name="spu_pool_implement_attached" text="på %s"/>
|
||||
<text name="spu_pool_implement_quarantine" text="kunne ikke kobles på — prøves igen om %s min"/>
|
||||
<text name="spu_pool_implement_reserved" text="reserveret — en traktor er på vej%.0s"/>
|
||||
<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"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s for bred: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_widthUnknown" text="%s: arbejdsbredde %.1f m kan ikke være rigtig — sæt bredden i Courseplay (sendes ikke ud)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
|
||||
<text name="spu_field_role_mower" text="slåmaskine"/>
|
||||
<text name="spu_field_role_tedder" text="vender"/>
|
||||
@@ -65,25 +126,33 @@
|
||||
<text name="spu_field_mode_manual" text="Manuel"/>
|
||||
<text name="spu_field_notMowable" text="ikke græs/kløver"/>
|
||||
<text name="spu_field_scanning" text="Marker indlæses ... (én mark pr. sekund)"/>
|
||||
<text name="spu_field_hint" text="Klik på en mark for at vælge den. Sæt flueben ved maskinerne i markflåden nederst."/>
|
||||
<text name="spu_field_hint" text="Klik på en mark for at se og rette dens opsætning."/>
|
||||
<text name="spu_field_fleetEmpty" text="! Markflåden står på "kun med flueben", men ingen maskiner har flueben — intet kan sendes ud. Åbn Markflåde og vælg "Brug alle ledige maskiner"."/>
|
||||
<text name="spu_field_showAll" text="Vis alle marker (%d skjult)"/>
|
||||
<text name="spu_col_field" text="Mark"/>
|
||||
<text name="spu_col_crop" text="Afgrøde"/>
|
||||
<text name="spu_col_plan" text="Plan"/>
|
||||
<text name="spu_col_mode" text="Kørsel"/>
|
||||
<text name="spu_field_section" text="— Mark F%d: %s —"/>
|
||||
<text name="spu_field_statusFull" text="Status: %s"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Kørsel"/>
|
||||
<text name="spu_field_rake" text="Riv skårene sammen før opsamling (gør smalle læssevogne brugbare)"/>
|
||||
<text name="spu_field_marker" text=" Markør %s, %.0f m fra marken"/>
|
||||
<text name="spu_field_mowWidth" text=" Slået med %.1f m — opsamling skal være mindst lige så bred"/>
|
||||
<text name="spu_field_section_store" text="— Tving lager (ellers nærmeste silo med plads) —"/>
|
||||
<text name="spu_field_section_dedicated" text="— Faste maskiner til denne mark (bruges kun her; ingen valgt = de frie) —"/>
|
||||
<text name="spu_field_dedicatedElsewhere" text="fast på F%s"/>
|
||||
<text name="spu_field_hiddenDedicated" text="%d maskiner er faste på andre marker (%s) og vises ikke her"/>
|
||||
<text name="spu_field_fieldSilo" text="marksilo"/>
|
||||
<text name="spu_field_storeHidden" text="+ %d siloer længere væk vises ikke"/>
|
||||
<text name="spu_field_noStore" text="ingen silo tager markens vare"/>
|
||||
<text name="spu_field_storeAuto" text="intet valgt: nærmeste silo med plads bruges"/>
|
||||
<text name="spu_field_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
|
||||
<text name="spu_field_section_fleet" text="— Markflåde (flueben = må sendes ud) —"/>
|
||||
<text name="spu_field_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
||||
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
||||
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d startes, når en maskine er ledig"/>
|
||||
@@ -94,6 +163,7 @@
|
||||
<text name="spu_field_fermenter" text="gæringssilo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="intet valgt: nærmeste gæringssilo med plads, ellers nærmeste silo"/>
|
||||
<text name="spu_field_bare" text="bar jord"/>
|
||||
<text name="spu_field_stubble" text="stub (%s)"/>
|
||||
<text name="spu_field_careNeed" text="trænger til %s"/>
|
||||
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
||||
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
||||
@@ -116,7 +186,7 @@
|
||||
<text name="spu_field_rig_seeds" text="frø ok"/>
|
||||
<text name="spu_field_rigs" text="Maskiner pr. trin (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="kultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="gødningsspreder"/>
|
||||
<text name="spu_field_role_fertilizer" text="gødskning"/>
|
||||
<text name="spu_field_role_harvester" text="høster"/>
|
||||
<text name="spu_field_role_limer" text="kalkspreder"/>
|
||||
<text name="spu_field_role_mulcher" text="stubhakker"/>
|
||||
@@ -152,14 +222,53 @@
|
||||
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
||||
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
||||
<text name="spu_field_notice_done" text="er færdig"/>
|
||||
<text name="spu_field_reason_regrowing" text="venter på at græsset vokser helt ud igen"/>
|
||||
<text name="spu_field_reason_rain" text="venter på tørvejr"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="Nyt run"/>
|
||||
<text name="spu_field_target" text="Afgrøde på marken (moden passer resten)"/>
|
||||
<text name="spu_field_target_none" text="Ingen — planlæg selv"/>
|
||||
<text name="spu_field_target_bare" text="Klargøres og tilsås med %s, så snart det er såtid"/>
|
||||
<text name="spu_field_target_keep" text="%s står allerede: slås og samles op, hver gang den er klar"/>
|
||||
<text name="spu_field_target_switch" text="%s får lov at vokse færdig og høstes/slås; derefter klargøres og tilsås med %s"/>
|
||||
<text name="spu_field_waitSowing" text="venter på såtid for %s"/>
|
||||
<text name="spu_field_fleetAll" text="Brug alle ledige maskiner (Nej = kun med flueben)"/>
|
||||
<text name="spu_field_role_baler" text="ballepresser"/>
|
||||
<text name="spu_field_role_baleCollector" text="ballesamler"/>
|
||||
<text name="spu_field_stage_strawCollecting" text="Halm samles"/>
|
||||
<text name="spu_field_stage_baling" text="Presses"/>
|
||||
<text name="spu_field_stage_baleCollecting" text="Baller samles"/>
|
||||
<text name="spu_field_straw" text="Halm efter høst"/>
|
||||
<text name="spu_field_straw_chop" text="Hakkes (spredes på marken)"/>
|
||||
<text name="spu_field_straw_collect" text="Samles op til silo"/>
|
||||
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
|
||||
<text name="spu_button_start" text="Start flow"/>
|
||||
<text name="spu_button_stop" text="Stop flow"/>
|
||||
<text name="spu_button_new" text="Nyt flow"/>
|
||||
<text name="spu_button_renameBuilding" text="Omdøb bygning"/>
|
||||
<text name="spu_renameBuildingTitle" text="Nyt navn på bygningen (tomt = butiksnavnet)"/>
|
||||
<text name="spu_button_stopPark" text="Stop alt og parkér"/>
|
||||
<text name="spu_status_queuedAll" text="Start alle: %d traktorer startes én ad gangen (hvert %d. sekund), %d flows kørte allerede, %d sprunget over"/>
|
||||
<text name="spu_status_stopParked" text="Stop alt: %d stoppet, %d på vej til parkering — pause til Start alle"/>
|
||||
<text name="spu_status_noPark" text="uden parkeringspunkt: %s"/>
|
||||
<text name="spu_status_fetchManual" text="hent selv (for brede): %s"/>
|
||||
<text name="spu_status_starting" text="Starter traktorer: %d af %d …"/>
|
||||
<text name="spu_status_paused" text="Pause — tryk Start alle for at fortsætte"/>
|
||||
<text name="spu_flow_pickOther" text="Vælg et andet flow (tavlen med alle flows står i Oversigt)"/>
|
||||
<text name="spu_button_rename" text="Omdøb"/>
|
||||
<text name="spu_button_delete" text="Slet run"/>
|
||||
<text name="spu_newRunName" text="Run %d"/>
|
||||
<text name="spu_renameTitle" text="Navn på run"/>
|
||||
<text name="spu_button_delete" text="Slet flow"/>
|
||||
<text name="spu_panel_close" text="Luk"/>
|
||||
<text name="spu_panel_gone" text="Posten findes ikke længere — vinduet er lukket"/>
|
||||
<text name="spu_flowList_hint" text="Klik på et flow for at rette det. [Start]/[Stop] yderst til højre styrer det direkte. Farver: grøn kører · gul venter · rød problem · grå stoppet. "Efter behov" = hver tur henter det målet har mindst af."/>
|
||||
<text name="spu_flowList_start" text="[Start]"/>
|
||||
<text name="spu_flowList_stop" text="[Stop]"/>
|
||||
<text name="spu_flowList_status" text="Flows i gang: %d af %d"/>
|
||||
<text name="spu_panel_flowTitle" text="Rediger flow: %s"/>
|
||||
<text name="spu_panel_fieldTitle" text="Mark F%d"/>
|
||||
<text name="spu_field_pausedSuffix" text="(pause)"/>
|
||||
<text name="spu_field_pausedBanner" text="! Markarbejdet står på pause siden "Stop alt" (gemmes med savegamet) — tryk Start alle i Oversigt, eller [Start] på en enkelt mark."/>
|
||||
<text name="spu_prune_removed" text="Flow '%s': %d traktor(er) findes ikke længere og er fjernet (%d tilbage)"/>
|
||||
<text name="spu_prune_empty" text="Flow '%s' har ingen traktorer længere og virker ikke — tildel en i Flows"/>
|
||||
<text name="spu_newRunName" text="Flow %d"/>
|
||||
<text name="spu_renameTitle" text="Navn på flow"/>
|
||||
<text name="spu_deleteConfirm" text="Slet run '%s'? Dets kørende traktorer stoppes. Kan ikke fortrydes."/>
|
||||
<text name="spu_capacity" text="%s l"/>
|
||||
<text name="spu_capacity_pallets" text="autoload (paller)"/>
|
||||
@@ -203,6 +312,8 @@
|
||||
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
|
||||
<text name="spu_storeAbove" text="Kør til lager over"/>
|
||||
<text name="spu_sellAbove" text="Sælg altid når kilden er over"/>
|
||||
<text name="spu_sellAbove_off" text="Fra"/>
|
||||
<text name="spu_storeAbove_tooltip" text="Varer med Lager-politik køres til silo når kildens lager er over denne fyldning (eller der er et fuldt læs)."/>
|
||||
<text name="spu_collectRound" text="Saml op på én tur"/>
|
||||
<text name="spu_collectRound_tooltip" text="Tipvogn: samme vare fra flere kilder til vognen er fuld. Autoload-vogn: flere pallevarer med samme mål på én tur."/>
|
||||
@@ -212,6 +323,7 @@
|
||||
<text name="spu_reason_noProducts" text="ingen vare valgt (sæt Lager/Salg på en vare ovenfor)"/>
|
||||
<text name="spu_reason_noWaitPoint" text="intet Wait-punkt i puljen (markør i en mappe med 'Wait' i navnet)"/>
|
||||
<text name="spu_reason_cargoNoTarget" text="lasten på vognen har intet mål (pris for lav / ingen silo) — tøm vognen"/>
|
||||
<text name="spu_reason_autoloadOn" text="AutoLoad er slået til på vognen — AutoDrive ser den kun som palle-/ballevogn (slå AutoLoad fra på vognen)"/>
|
||||
<text name="spu_reason_needsAutoload" text="pallevarer kræver Universal Autoload på vognen"/>
|
||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||
<text name="spu_price_pct" text="%d %% af top (%s)"/>
|
||||
@@ -237,14 +349,16 @@
|
||||
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
||||
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
||||
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
||||
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
||||
<text name="spu_flow_round" text="runde"/>
|
||||
<text name="spu_flow_ruleIn" text="behov"/>
|
||||
<text name="spu_flow_ruleInLoops" text="behov, %d ture"/>
|
||||
<text name="spu_flow_ruleIn" text="efter behov — laveste først"/>
|
||||
<text name="spu_flow_ruleInLoops" text="efter behov, %d ture"/>
|
||||
<text name="spu_markerAt" text="%s %d m"/>
|
||||
<text name="spu_noMarker" text="INGEN MARKØR inden for 40 m — sæt en AD-markør ved triggeren"/>
|
||||
<text name="spu_noBuildings" text="Ingen bygninger fundet"/>
|
||||
<text name="spu_buildingGone" text="bygningen findes ikke længere"/>
|
||||
<text name="spu_buildingGone" text="Fjernet bygning (findes ikke mere) — sæt NEJ"/>
|
||||
<text name="spu_reason_noMarker" text="'%s' mangler en AD-markør ved triggeren"/>
|
||||
<text name="spu_storage_select" text="Vis bygning"/>
|
||||
<text name="spu_stat_now" text="nu %s"/>
|
||||
@@ -284,5 +398,181 @@
|
||||
<text name="spu_col_lasts" text="Rækker til"/>
|
||||
<text name="spu_col_rate" text="Forbrug/prod. døgn"/>
|
||||
<text name="spu_storage_select_tooltip" text="Vælg bygningen tabellen viser."/>
|
||||
<text name="spu_guide_next" text="Næste"/>
|
||||
<text name="spu_guide_save" text="Gem"/>
|
||||
<text name="spu_guide_ok" text="OK"/>
|
||||
<text name="spu_guide_back" text="Tilbage"/>
|
||||
<text name="spu_guide_cancel" text="Annullér"/>
|
||||
<text name="spu_guide_saveOnly" text="Gem uden at starte"/>
|
||||
<text name="spu_guide_runAuto" text="Kør automatisk"/>
|
||||
<text name="spu_guide_stepOf" text="Trin %d af %d — %s"/>
|
||||
<text name="spu_guide_stepOne" text="Trin %d — %s"/>
|
||||
<text name="spu_guide_editStep" text="Ret: %s"/>
|
||||
<text name="spu_guide_editSuffix" text=" [Ret]"/>
|
||||
<text name="spu_guide_sum_hint" text="Tryk på en linje for at rette netop det."/>
|
||||
<text name="spu_guide_auto" text="Automatisk (anbefalet)"/>
|
||||
<text name="spu_guide_manual" text="Vælg selv"/>
|
||||
<text name="spu_guide_closed" text="Guiden er lukket — marken/flowet findes ikke længere"/>
|
||||
<text name="spu_guide_cannotAdvance" text="Vælg først"/>
|
||||
<text name="spu_guide_pickField" text="Vælg en mark først"/>
|
||||
<text name="spu_guide_saved" text="F%d gemt"/>
|
||||
<text name="spu_guide_noMarker" text="! Sæt en AD-markør ved F%d — arbejdet venter på markøren"/>
|
||||
<text name="spu_guide_field_open" text="Opsæt mark"/>
|
||||
<text name="spu_guide_fleet_open" text="Markflåde"/>
|
||||
<text name="spu_guide_fleet_close" text="Til marker"/>
|
||||
<text name="spu_guide_field_title" text="— Opsæt mark F%d —"/>
|
||||
<text name="spu_guide_field_titleNone" text="— Opsæt mark —"/>
|
||||
<text name="spu_guide_field_step_field" text="Mark"/>
|
||||
<text name="spu_guide_field_step_crop" text="Afgrøde"/>
|
||||
<text name="spu_guide_field_step_cropChoice" text="Afgrødens valg"/>
|
||||
<text name="spu_guide_field_step_prep" text="Klargøring"/>
|
||||
<text name="spu_guide_field_step_care" text="Pleje"/>
|
||||
<text name="spu_guide_field_step_machines" text="Maskiner"/>
|
||||
<text name="spu_guide_field_step_store" text="Lager"/>
|
||||
<text name="spu_guide_field_step_summary" text="Oversigt"/>
|
||||
<text name="spu_guide_field_hint_field" text="Vælg den mark du vil sætte op."/>
|
||||
<text name="spu_guide_field_hint_crop" text="Hvad skal gro på marken? 'Behold' kører videre med det der står nu."/>
|
||||
<text name="spu_guide_field_hint_cropChoice" text="Hvad skal der ske med afgrøden ved slåning eller høst?"/>
|
||||
<text name="spu_guide_field_hint_prep" text="Efter høst / på bar mark. Automatisk pløjer eller kultiverer og kalker efter behov."/>
|
||||
<text name="spu_guide_field_hint_care" text="Mens afgrøden gror. Der gødes/sprøjtes kun når marken trænger."/>
|
||||
<text name="spu_guide_field_hint_machines" text="Hvor mange ad gangen, og skal marken have sine egne faste maskiner?"/>
|
||||
<text name="spu_guide_field_hint_store" text="Hvor skal det høstede/slåede hen?"/>
|
||||
<text name="spu_guide_field_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||
<text name="spu_guide_crop" text="Afgrøde"/>
|
||||
<text name="spu_guide_crop_keep" text="Behold det der gror"/>
|
||||
<text name="spu_guide_crop_sowAgain" text="Så samme afgrøde igen efter høst"/>
|
||||
<text name="spu_guide_prep" text="Klargøring"/>
|
||||
<text name="spu_guide_prep_autoText" text=" Pløjer når jorden trænger, ellers kultiverer; kalker ved lav pH."/>
|
||||
<text name="spu_guide_care" text="Pleje"/>
|
||||
<text name="spu_guide_care_autoText" text=" Gøder op til max når afgrøden vokser, sprøjter/harver ukrudt væk og kalker ved lav pH."/>
|
||||
<text name="spu_guide_rigs" text="Maskiner ad gangen (multi-tool)"/>
|
||||
<text name="spu_guide_dedicated" text="Faste maskiner til marken?"/>
|
||||
<text name="spu_guide_useFieldSilo" text="Brug marksiloen?"/>
|
||||
<text name="spu_guide_store" text="Lager"/>
|
||||
<text name="spu_guide_store_autoText" text=" Nærmeste silo med plads (ensilage: gæringssilo)."/>
|
||||
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||
<text name="spu_guide_sum_sowAgain" text="så igen"/>
|
||||
<text name="spu_guide_sum_rake" text="river"/>
|
||||
<text name="spu_guide_sum_noRake" text="uden river"/>
|
||||
<text name="spu_guide_sum_straw_chop" text="halm hakkes"/>
|
||||
<text name="spu_guide_sum_straw_collect" text="halm samles"/>
|
||||
<text name="spu_guide_sum_straw_bale" text="halm presses"/>
|
||||
<text name="spu_guide_sum_auto" text="Automatisk"/>
|
||||
<text name="spu_guide_sum_lime" text="kalk"/>
|
||||
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||
<text name="spu_guide_sum_roll" text="tromle"/>
|
||||
<text name="spu_guide_sum_fert" text="gødning"/>
|
||||
<text name="spu_guide_sum_weed" text="ukrudt"/>
|
||||
<text name="spu_guide_sum_noCare" text="ingen pleje"/>
|
||||
<text name="spu_guide_sum_dedicated" text="%d faste"/>
|
||||
<text name="spu_guide_sum_freeRigs" text="ledige maskiner"/>
|
||||
<text name="spu_guide_sum_rigs" text="%d ad gangen"/>
|
||||
<text name="spu_guide_sum_unloaders" text="%d aflæsningsvogne"/>
|
||||
<text name="spu_guide_sum_fieldSilo" text="Marksilo"/>
|
||||
<text name="spu_guide_sum_stores" text="%d valgte siloer"/>
|
||||
<text name="spu_guide_flow_open" text="Opsæt flow"/>
|
||||
<text name="spu_guide_flow_title" text="— Opsæt flow: %s —"/>
|
||||
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||
<text name="spu_guide_flow_saved" text="Flow '%s' gemt"/>
|
||||
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||
<text name="spu_guide_flow_rename" text="Skift navn …"/>
|
||||
<text name="spu_guide_flow_step_name" text="Navn"/>
|
||||
<text name="spu_guide_flow_step_direction" text="Retning"/>
|
||||
<text name="spu_guide_flow_step_from" text="Fra"/>
|
||||
<text name="spu_guide_flow_step_to" text="Til"/>
|
||||
<text name="spu_guide_flow_step_goods" text="Varer"/>
|
||||
<text name="spu_guide_flow_step_store" text="Lager"/>
|
||||
<text name="spu_guide_flow_step_rules" text="Regler"/>
|
||||
<text name="spu_guide_flow_step_wait" text="Vent ved"/>
|
||||
<text name="spu_guide_flow_step_tractors" text="Traktorer"/>
|
||||
<text name="spu_guide_flow_step_summary" text="Oversigt"/>
|
||||
<text name="spu_guide_flow_hint_name" text="Flowets navn (traktorerne får navn efter det)."/>
|
||||
<text name="spu_guide_flow_hint_direction" text="Ind til bygning: fyld stalde/fabrikker. Ud fra bygning: tøm, lagr og sælg."/>
|
||||
<text name="spu_guide_flow_hint_buildings_in" text="Hvilke stalde/fabrikker skal fyldes? Markøren findes automatisk."/>
|
||||
<text name="spu_guide_flow_hint_buildings_out" text="Hvor skal der hentes fra? Markøren findes automatisk."/>
|
||||
<text name="spu_guide_flow_hint_goods_in" text="Automatisk henter hver tur den vare målet mangler mest."/>
|
||||
<text name="spu_guide_flow_hint_goods_out" text="For hver vare: Lager, Salg, eller Salg ellers lager."/>
|
||||
<text name="spu_guide_flow_hint_store" text="Hvor skal lagervarerne hen?"/>
|
||||
<text name="spu_guide_flow_hint_rules_in" text="Antal ture, og hvornår traktoren kører hjem fra Wait."/>
|
||||
<text name="spu_guide_flow_hint_rules_out" text="Hvornår der sælges, og hvornår der køres til lager."/>
|
||||
<text name="spu_guide_flow_hint_wait" text="Hvor venter traktoren med et rest-læs?"/>
|
||||
<text name="spu_guide_flow_hint_tractors" text="Hvilke traktorer kører flowet? Traktorer i andre flows vises ikke."/>
|
||||
<text name="spu_guide_flow_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||
<text name="spu_guide_flow_goods" text="Varer"/>
|
||||
<text name="spu_guide_flow_goodsAutoChoice" text="Automatisk (det bygningen mangler)"/>
|
||||
<text name="spu_guide_flow_goodsAutoText" text=" Hver tur hentes den vare målet mangler mest."/>
|
||||
<text name="spu_guide_flow_noGoodsTo" text=" Vælg først hvor der skal køres til (trinnet Til)"/>
|
||||
<text name="spu_guide_flow_noProducts" text=" Vælg først hvor der hentes fra (trinnet Fra)"/>
|
||||
<text name="spu_guide_flow_storeAutoChoice" text="Automatisk (nærmeste med plads)"/>
|
||||
<text name="spu_guide_flow_storeAutoText" text=" Lagerturene kører til nærmeste lager med plads."/>
|
||||
<text name="spu_guide_flow_noTractors" text=" Ingen ledige traktorer"/>
|
||||
<text name="spu_guide_flow_otherFlows" text=" %d traktorer kører i andre flows og vises ikke"/>
|
||||
<text name="spu_guide_flow_blockedShow" text=" %d traktorer kan ikke bruges her (vognen passer ikke) — klik for at se hvorfor"/>
|
||||
<text name="spu_guide_flow_blockedHide" text=" %d traktorer kan ikke bruges her — klik for at skjule"/>
|
||||
<text name="spu_guide_flow_blocked" text=" %d traktorer kan ikke bruges her (vognen passer ikke)"/>
|
||||
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
||||
<text name="spu_guide_flow_sumRound" text="samlerunde"/>
|
||||
<text name="spu_guide_flow_sumLoops" text="%d ture"/>
|
||||
<text name="spu_guide_flow_sumLoopsInf" text="uendelige ture"/>
|
||||
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
|
||||
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
|
||||
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
|
||||
<text name="spu_tab_fieldsilos" text="Marksiloer"/>
|
||||
<text name="spu_fs_enabled" text="Hent marksiloer hjem automatisk"/>
|
||||
<text name="spu_fs_maxVehicles" text="Maks. vogne ude"/>
|
||||
<text name="spu_fs_minLiters" text="Mindste mængde pr. vare"/>
|
||||
<text name="spu_fs_status" text="%d/%d vogne ude · %d siloer venter"/>
|
||||
<text name="spu_fs_noStores" text="Vælg mindst ét gårdlager nedenfor — ellers køres intet hjem"/>
|
||||
<text name="spu_fs_silosTitle" text="Marksiloer med varer"/>
|
||||
<text name="spu_fs_noVehicle" text="Ingen ledig tømmevogn: %d uden AD-parkeringspunkt, %d optaget, %d med spiller i, %d ikke i flåden (vogne i flows lånes ikke)"/>
|
||||
<text name="spu_fs_nothing" text="Ingen marksiloer har varer over mindste mængde"/>
|
||||
<text name="spu_fs_col_silo" text="Silo"/>
|
||||
<text name="spu_fs_col_liters" text="Liter"/>
|
||||
<text name="spu_fs_col_onWay" text="På vej"/>
|
||||
<text name="spu_fs_col_state" text="Status"/>
|
||||
<text name="spu_fs_state_ok" text="ok"/>
|
||||
<text name="spu_fs_state_noMarker" text="mangler markør"/>
|
||||
<text name="spu_fs_state_noFarmStore" text="ingen gård tager %s"/>
|
||||
<text name="spu_fs_state_paused" text="pause (kom ikke i gang)"/>
|
||||
<text name="spu_fs_storesTitle" text="Gårdlagre (hvor varerne køres hjem til)"/>
|
||||
<text name="spu_fs_toProduction" text="Kør overskud til produktion (foder til staldene først)"/>
|
||||
<text name="spu_fs_reserveDays" text="Foderreserve: staldenes forbrug i"/>
|
||||
<text name="spu_fs_days" text="%d døgn"/>
|
||||
<text name="spu_fs_toFactory" text="→ fabrik"/>
|
||||
<text name="spu_fs_harvestHeld" text="%d tømmevogn(e) holdes tilbage til høstere der arbejder"/>
|
||||
<text name="spu_view_title" text="Vis"/>
|
||||
<text name="spu_view_items" text="Varer"/>
|
||||
<text name="spu_view_buildings" text="Bygninger"/>
|
||||
<text name="spu_view_tooltip" text="Varer: samlet beholdning pr. vare (klik for at se hvor den ligger). Bygninger: tabel for én bygning."/>
|
||||
<text name="spu_kpi_value" text="Lagerværdi nu"/>
|
||||
<text name="spu_kpi_gain" text="Ekstra ved toppris"/>
|
||||
<text name="spu_kpi_empty" text="Løber tør snart"/>
|
||||
<text name="spu_kpi_full" text="Næsten fuldt"/>
|
||||
<text name="spu_attention" text="— Kræver handling —"/>
|
||||
<text name="spu_attention_none" text="Intet kræver handling lige nu"/>
|
||||
<text name="spu_attention_hintFlow" text=" → klik: opret flow"/>
|
||||
<text name="spu_attention_hintOpen" text=" → klik: åbn flow '%s'"/>
|
||||
<text name="spu_attention_hintProduce" text=" → producér/køb"/>
|
||||
<text name="spu_attention_produce" text="Et flow hjælper ikke — gården har kun %2$s %1$s i alt. Producér eller køb mere."/>
|
||||
<text name="spu_attention_flowName" text="%s til %s"/>
|
||||
<text name="spu_attention_hintSellFlow" text=" → klik: opret salgsflow"/>
|
||||
<text name="spu_attention_sellFlowName" text="Sælg %s"/>
|
||||
<text name="spu_alert_empty" text="%s løber tør om %s — gården har %s, bruger %s/døgn"/>
|
||||
<text name="spu_alert_fullIn" text="%s er %d %% fuld — fuld om %s"/>
|
||||
<text name="spu_alert_full" text="%s er %d %% fuld"/>
|
||||
<text name="spu_alert_sell" text="Sælg %s nu: prisen er %d %% af årets top — %s"/>
|
||||
<text name="spu_items_hint" text="(klik på en vare)"/>
|
||||
<text name="spu_icol_amount" text="Beholdning"/>
|
||||
<text name="spu_icol_value" text="Værdi nu"/>
|
||||
<text name="spu_icol_rate" text="Gården netto · rækker"/>
|
||||
<text name="spu_item_perDay" text="%s/døgn"/>
|
||||
<text name="spu_item_fullIn" text="fuld om %s"/>
|
||||
<text name="spu_item_balanced" text="i balance"/>
|
||||
<text name="spu_item_share" text="%d %% af varen"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<text name="spu_title" text="Smart Pickup"/>
|
||||
<text name="spu_run" text="Run"/>
|
||||
<text name="spu_run_tooltip" text="Select the run to view or edit."/>
|
||||
<text name="spu_noRuns" text="No runs — press 'New run'"/>
|
||||
<text name="spu_noRuns" text="No flows — press 'New flow'"/>
|
||||
<text name="spu_target" text="Target"/>
|
||||
<text name="spu_target_tooltip" text="AutoDrive marker at the barn or factory to keep filled."/>
|
||||
<text name="spu_noTarget" text="(choose target)"/>
|
||||
@@ -27,7 +27,10 @@
|
||||
<text name="spu_status_started" text="Started %d, skipped %d"/>
|
||||
<text name="spu_status_noAd" text="AutoDrive API not found"/>
|
||||
<text name="spu_status_error" text="Error: %s"/>
|
||||
<text name="spu_reason_noTrailer" text="no trailer"/>
|
||||
<text name="spu_guide_flow_wagon" text=" ↳ Trailer"/>
|
||||
<text name="spu_guide_flow_noFreeWagon" text=" ↳ no free trailer (detached, with a Connect marker, able to carry the goods)"/>
|
||||
<text name="spu_guide_flow_soloText" text="%s — no trailer attached: fetches a free trailer"/>
|
||||
<text name="spu_reason_noTrailer" text="no trailer (attach one, or name the trailer's parking after the flow)"/>
|
||||
<text name="spu_reason_cannotCarry" text="trailer cannot carry the run's goods"/>
|
||||
<text name="spu_reason_farFromNetwork" text="too far from the road network"/>
|
||||
<text name="spu_reason_noTarget" text="no target"/>
|
||||
@@ -40,7 +43,28 @@
|
||||
<text name="spu_status_stoppedAll" text="Stopped %d tractors"/>
|
||||
<text name="spu_overview_hint" text="Click a flow to edit it"/>
|
||||
<text name="spu_tab_fields" text="Fields"/>
|
||||
<text name="spu_tab_month" text="Month"/>
|
||||
<text name="spu_month_months" text="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"/>
|
||||
<text name="spu_month_period" text="Period %d"/>
|
||||
<text name="spu_month_header" text="%s · day %d of %d · %02d:%02d"/>
|
||||
<text name="spu_month_noClock" text="Cannot read the game calendar"/>
|
||||
<text name="spu_month_verdictCalm" text="Nothing urgent — safe to sleep to next month"/>
|
||||
<text name="spu_month_verdictUrgent" text="NOTE: %d fields must finish before the month ends"/>
|
||||
<text name="spu_month_noWork" text="No fields have work in progress"/>
|
||||
<text name="spu_month_col_work" text="Work"/>
|
||||
<text name="spu_month_col_progress" text="Done"/>
|
||||
<text name="spu_month_col_next" text="Next"/>
|
||||
<text name="spu_month_col_window" text="Window"/>
|
||||
<text name="spu_month_window_ok" text="ok"/>
|
||||
<text name="spu_month_window_harvestCloses" text="NOTE: withers soon"/>
|
||||
<text name="spu_month_window_lastSowMonth" text="NOTE: last sow month"/>
|
||||
<text name="spu_month_upcoming" text="Can be sown now, not started:"/>
|
||||
<text name="spu_month_upcomingOpen" text="F%d: %s — can also be sown next month"/>
|
||||
<text name="spu_month_upcomingLast" text="F%d: %s — NOTE: last sowing month"/>
|
||||
<text name="spu_field_button_start" text="Start now"/>
|
||||
<text name="spu_field_manualTransport" text=" %s drives itself on the road (No = too wide, you drive it to the field)"/>
|
||||
<text name="spu_field_button_takeover" text="Start now"/>
|
||||
<text name="spu_field_takeoverRequested" text="F%d: taking over the harvester now if the header fits"/>
|
||||
<text name="spu_field_button_stop" text="Stop field"/>
|
||||
<text name="spu_field_noMarker" text="! missing marker F%d"/>
|
||||
<text name="spu_field_stage_idle" text="Waiting"/>
|
||||
@@ -51,8 +75,45 @@
|
||||
<text name="spu_field_stage_collecting" text="Collecting"/>
|
||||
<text name="spu_field_stage_done" text="Done"/>
|
||||
<text name="spu_field_reason_noRig" text="no %s ticked in the field fleet"/>
|
||||
<text name="spu_field_reasonShort_noRig" text="no machine"/>
|
||||
<text name="spu_field_reasonShort_allBusy" text="waiting for machine"/>
|
||||
<text name="spu_field_reasonShort_tooWide" text="too wide"/>
|
||||
<text name="spu_field_reasonShort_widthUnknown" text="width unknown"/>
|
||||
<text name="spu_field_reasonShort_tooNarrow" text="too narrow"/>
|
||||
<text name="spu_field_reasonShort_cannotDo" text="none can do it"/>
|
||||
<text name="spu_field_reasonShort_noSowFruit" text="choose crop"/>
|
||||
<text name="spu_field_reasonShort_notEnough" text="too few free"/>
|
||||
<text name="spu_field_reasonShort_notPlantable" text="not sowing time"/>
|
||||
<text name="spu_field_reasonShort_regrowing" text="waiting for regrowth"/>
|
||||
<text name="spu_field_reasonShort_rain" text="waiting for dry weather"/>
|
||||
<text name="spu_field_reasonShort_text" text="see details"/>
|
||||
<text name="spu_field_reason_allBusy" text="waiting for a free %s"/>
|
||||
<text name="spu_pool_section_tractors" text="Machine pool — tractors (fetch implements when no ready combination is free)"/>
|
||||
<text name="spu_pool_section_implements" text="Machine pool — implements (park "X" + marker "Connect X")"/>
|
||||
<text name="spu_pool_noTractors" text="no tractors without implement"/>
|
||||
<text name="spu_pool_noImplements" text="no implements with a field role"/>
|
||||
<text name="spu_pool_tractor_free" text="free%.0s"/>
|
||||
<text name="spu_pool_tractor_noPark" text="needs its own AD park destination%.0s"/>
|
||||
<text name="spu_pool_tractor_player" text="player is driving%.0s"/>
|
||||
<text name="spu_pool_tractor_returning" text="dropping implement and driving home%.0s"/>
|
||||
<text name="spu_pool_tractor_job" text="job F%s"/>
|
||||
<text name="spu_pool_tractor_busy" text="AutoDrive/Courseplay active%.0s"/>
|
||||
<text name="spu_pool_tractor_notInFleet" text="not in field fleet (tick)%.0s"/>
|
||||
<text name="spu_pool_tractor_offNetwork" text="off the AutoDrive network%.0s"/>
|
||||
<text name="spu_pool_tractor_hitched" text="has an implement attached (detach it to use the pool)%.0s"/>
|
||||
<text name="spu_pool_implement_flow" text="belongs to flow '%s'"/>
|
||||
<text name="spu_pool_implement_parked" text="parked at %s"/>
|
||||
<text name="spu_pool_implement_noPark" text="not in pool — needs an AD park destination%.0s"/>
|
||||
<text name="spu_pool_implement_noConnect" text="not in pool — needs marker "Connect %s""/>
|
||||
<text name="spu_pool_implement_attached" text="on %s"/>
|
||||
<text name="spu_pool_implement_quarantine" text="could not attach — retry in %s min"/>
|
||||
<text name="spu_pool_implement_reserved" text="reserved — a tractor is on its way%.0s"/>
|
||||
<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"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s too wide: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_widthUnknown" text="%s: work width %.1f m cannot be right — set the width in Courseplay (not sent out)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m — turn on Rake or use a wider one"/>
|
||||
<text name="spu_field_role_mower" text="mower"/>
|
||||
<text name="spu_field_role_tedder" text="tedder"/>
|
||||
@@ -65,25 +126,33 @@
|
||||
<text name="spu_field_mode_manual" text="Manual"/>
|
||||
<text name="spu_field_notMowable" text="not grass/clover"/>
|
||||
<text name="spu_field_scanning" text="Loading fields ... (one field per second)"/>
|
||||
<text name="spu_field_hint" text="Click a field to select it. Tick the machines in the field fleet at the bottom."/>
|
||||
<text name="spu_field_hint" text="Click a field to see and change its setup."/>
|
||||
<text name="spu_field_fleetEmpty" text="! The field fleet is set to "only ticked", but no machine is ticked — nothing can be sent out. Open Field fleet and choose "Use all free machines"."/>
|
||||
<text name="spu_field_showAll" text="Show all fields (%d hidden)"/>
|
||||
<text name="spu_col_field" text="Field"/>
|
||||
<text name="spu_col_crop" text="Crop"/>
|
||||
<text name="spu_col_plan" text="Plan"/>
|
||||
<text name="spu_col_mode" text="Mode"/>
|
||||
<text name="spu_field_section" text="— Field F%d: %s —"/>
|
||||
<text name="spu_field_statusFull" text="Status: %s"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Mode"/>
|
||||
<text name="spu_field_rake" text="Rake the swaths before collecting (lets narrow loading wagons collect)"/>
|
||||
<text name="spu_field_marker" text=" Marker %s, %.0f m from the field"/>
|
||||
<text name="spu_field_mowWidth" text=" Mowed at %.1f m — collecting must be at least as wide"/>
|
||||
<text name="spu_field_section_store" text="— Force storage (else nearest silo with room) —"/>
|
||||
<text name="spu_field_section_dedicated" text="— Dedicated machines for this field (used only here; none = the free ones) —"/>
|
||||
<text name="spu_field_dedicatedElsewhere" text="dedicated to F%s"/>
|
||||
<text name="spu_field_hiddenDedicated" text="%d machines are dedicated to other fields (%s) and not shown here"/>
|
||||
<text name="spu_field_fieldSilo" text="field silo"/>
|
||||
<text name="spu_field_storeHidden" text="+ %d silos further away not shown"/>
|
||||
<text name="spu_field_noStore" text="no silo takes this field's goods"/>
|
||||
<text name="spu_field_storeAuto" text="none chosen: nearest silo with room is used"/>
|
||||
<text name="spu_field_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
|
||||
<text name="spu_field_section_fleet" text="— Field fleet (ticked = may be sent out) —"/>
|
||||
<text name="spu_field_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
||||
<text name="spu_field_rig_working" text="working on F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
||||
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d starts when a machine is free"/>
|
||||
@@ -94,6 +163,7 @@
|
||||
<text name="spu_field_fermenter" text="fermenting silo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="none chosen: nearest fermenting silo with room, else nearest silo"/>
|
||||
<text name="spu_field_bare" text="bare soil"/>
|
||||
<text name="spu_field_stubble" text="stubble (%s)"/>
|
||||
<text name="spu_field_careNeed" text="needs %s"/>
|
||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
||||
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
||||
@@ -116,7 +186,7 @@
|
||||
<text name="spu_field_rig_seeds" text="seeds ok"/>
|
||||
<text name="spu_field_rigs" text="Machines per step (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="cultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="fertilizer spreader"/>
|
||||
<text name="spu_field_role_fertilizer" text="fertilizing"/>
|
||||
<text name="spu_field_role_harvester" text="harvester"/>
|
||||
<text name="spu_field_role_limer" text="lime spreader"/>
|
||||
<text name="spu_field_role_mulcher" text="mulcher"/>
|
||||
@@ -152,14 +222,53 @@
|
||||
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
||||
<text name="spu_field_weed" text="Control weeds"/>
|
||||
<text name="spu_field_notice_done" text="is done"/>
|
||||
<text name="spu_field_reason_regrowing" text="waiting for the regrowth to reach full height"/>
|
||||
<text name="spu_field_reason_rain" text="waiting for dry weather"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="New run"/>
|
||||
<text name="spu_field_target" text="Crop on the field (the mod handles the rest)"/>
|
||||
<text name="spu_field_target_none" text="None — plan it yourself"/>
|
||||
<text name="spu_field_target_bare" text="Prepared and sown with %s as soon as it is sowing time"/>
|
||||
<text name="spu_field_target_keep" text="%s is already there: mowed and collected every time it is ready"/>
|
||||
<text name="spu_field_target_switch" text="%s is left to finish growing and is harvested/mowed; then the field is prepared and sown with %s"/>
|
||||
<text name="spu_field_waitSowing" text="waiting for sowing time for %s"/>
|
||||
<text name="spu_field_fleetAll" text="Use all free machines (No = only ticked ones)"/>
|
||||
<text name="spu_field_role_baler" text="baler"/>
|
||||
<text name="spu_field_role_baleCollector" text="bale collector"/>
|
||||
<text name="spu_field_stage_strawCollecting" text="Collecting straw"/>
|
||||
<text name="spu_field_stage_baling" text="Baling"/>
|
||||
<text name="spu_field_stage_baleCollecting" text="Collecting bales"/>
|
||||
<text name="spu_field_straw" text="Straw after harvest"/>
|
||||
<text name="spu_field_straw_chop" text="Chopped (spread on the field)"/>
|
||||
<text name="spu_field_straw_collect" text="Collected to a silo"/>
|
||||
<text name="spu_field_straw_bale" text="Baled and collected"/>
|
||||
<text name="spu_button_start" text="Start flow"/>
|
||||
<text name="spu_button_stop" text="Stop flow"/>
|
||||
<text name="spu_button_new" text="New flow"/>
|
||||
<text name="spu_button_renameBuilding" text="Rename building"/>
|
||||
<text name="spu_renameBuildingTitle" text="New name for the building (empty = shop name)"/>
|
||||
<text name="spu_button_stopPark" text="Stop all and park"/>
|
||||
<text name="spu_status_queuedAll" text="Start all: %d tractors start one at a time (every %d seconds), %d flows already running, %d skipped"/>
|
||||
<text name="spu_status_stopParked" text="Stop all: %d stopped, %d driving to parking — paused until Start all"/>
|
||||
<text name="spu_status_noPark" text="no park destination: %s"/>
|
||||
<text name="spu_status_fetchManual" text="fetch yourself (too wide): %s"/>
|
||||
<text name="spu_status_starting" text="Starting tractors: %d of %d …"/>
|
||||
<text name="spu_status_paused" text="Paused — press Start all to continue"/>
|
||||
<text name="spu_flow_pickOther" text="Pick another flow (the board with all flows is in Overview)"/>
|
||||
<text name="spu_button_rename" text="Rename"/>
|
||||
<text name="spu_button_delete" text="Delete run"/>
|
||||
<text name="spu_newRunName" text="Run %d"/>
|
||||
<text name="spu_renameTitle" text="Run name"/>
|
||||
<text name="spu_button_delete" text="Delete flow"/>
|
||||
<text name="spu_panel_close" text="Close"/>
|
||||
<text name="spu_panel_gone" text="That item no longer exists — window closed"/>
|
||||
<text name="spu_flowList_hint" text="Click a flow to edit it. [Start]/[Stop] on the far right controls it directly. Colours: green runs · yellow waits · red problem · grey stopped. "By need" = each trip fetches what the target is lowest on."/>
|
||||
<text name="spu_flowList_start" text="[Start]"/>
|
||||
<text name="spu_flowList_stop" text="[Stop]"/>
|
||||
<text name="spu_flowList_status" text="Flows running: %d of %d"/>
|
||||
<text name="spu_panel_flowTitle" text="Edit flow: %s"/>
|
||||
<text name="spu_panel_fieldTitle" text="Field F%d"/>
|
||||
<text name="spu_field_pausedSuffix" text="(paused)"/>
|
||||
<text name="spu_field_pausedBanner" text="! Field work is paused since "Stop all" (saved with the savegame) — press Start all in Overview, or [Start] on a single field."/>
|
||||
<text name="spu_prune_removed" text="Flow '%s': %d tractor(s) no longer exist and were removed (%d left)"/>
|
||||
<text name="spu_prune_empty" text="Flow '%s' has no tractors left and won't run — assign one in Flows"/>
|
||||
<text name="spu_newRunName" text="Flow %d"/>
|
||||
<text name="spu_renameTitle" text="Flow name"/>
|
||||
<text name="spu_deleteConfirm" text="Delete run '%s'? Its running tractors will be stopped. This cannot be undone."/>
|
||||
<text name="spu_capacity" text="%s l"/>
|
||||
<text name="spu_capacity_pallets" text="autoload (pallets)"/>
|
||||
@@ -203,6 +312,8 @@
|
||||
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
|
||||
<text name="spu_storeAbove" text="Drive to storage above"/>
|
||||
<text name="spu_sellAbove" text="Always sell when source is above"/>
|
||||
<text name="spu_sellAbove_off" text="Off"/>
|
||||
<text name="spu_storeAbove_tooltip" text="Goods with the Store policy are driven to a silo when the source storage is above this fill level (or a full load is available)."/>
|
||||
<text name="spu_collectRound" text="Collect in one trip"/>
|
||||
<text name="spu_collectRound_tooltip" text="Tipper: same good from several sources until full. Autoload trailer: several pallet goods with the same destination in one trip."/>
|
||||
@@ -212,6 +323,7 @@
|
||||
<text name="spu_reason_noProducts" text="no good selected (set Store/Sell on a good above)"/>
|
||||
<text name="spu_reason_noWaitPoint" text="no Wait point in the pool (marker in a folder with 'Wait' in its name)"/>
|
||||
<text name="spu_reason_cargoNoTarget" text="the load on the trailer has no destination (price too low / no silo) — empty the trailer"/>
|
||||
<text name="spu_reason_autoloadOn" text="AutoLoad is on for this trailer — AutoDrive treats it as a pallet/bale trailer only (turn AutoLoad off on the trailer)"/>
|
||||
<text name="spu_reason_needsAutoload" text="pallet goods need Universal Autoload on the trailer"/>
|
||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||
<text name="spu_price_pct" text="%d %% of top (%s)"/>
|
||||
@@ -237,14 +349,16 @@
|
||||
<text name="spu_flow_noSource" text="(no source)"/>
|
||||
<text name="spu_flow_noTarget" text="(no target)"/>
|
||||
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
||||
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
||||
<text name="spu_flow_round" text="round"/>
|
||||
<text name="spu_flow_ruleIn" text="need"/>
|
||||
<text name="spu_flow_ruleInLoops" text="need, %d loops"/>
|
||||
<text name="spu_flow_ruleIn" text="by need — lowest first"/>
|
||||
<text name="spu_flow_ruleInLoops" text="by need, %d trips"/>
|
||||
<text name="spu_markerAt" text="%s %d m"/>
|
||||
<text name="spu_noMarker" text="NO MARKER within 40 m — place an AD marker at the trigger"/>
|
||||
<text name="spu_noBuildings" text="No buildings found"/>
|
||||
<text name="spu_buildingGone" text="building no longer exists"/>
|
||||
<text name="spu_buildingGone" text="Removed building (no longer exists) — set NO"/>
|
||||
<text name="spu_reason_noMarker" text="'%s' has no AD marker at its trigger"/>
|
||||
<text name="spu_storage_select" text="Show building"/>
|
||||
<text name="spu_stat_now" text="now %s"/>
|
||||
@@ -284,5 +398,181 @@
|
||||
<text name="spu_col_lasts" text="Lasts"/>
|
||||
<text name="spu_col_rate" text="Use/prod. per day"/>
|
||||
<text name="spu_storage_select_tooltip" text="Choose the building shown in the table."/>
|
||||
<text name="spu_guide_next" text="Next"/>
|
||||
<text name="spu_guide_save" text="Save"/>
|
||||
<text name="spu_guide_ok" text="OK"/>
|
||||
<text name="spu_guide_back" text="Back"/>
|
||||
<text name="spu_guide_cancel" text="Cancel"/>
|
||||
<text name="spu_guide_saveOnly" text="Save without starting"/>
|
||||
<text name="spu_guide_runAuto" text="Run automatically"/>
|
||||
<text name="spu_guide_stepOf" text="Step %d of %d — %s"/>
|
||||
<text name="spu_guide_stepOne" text="Step %d — %s"/>
|
||||
<text name="spu_guide_editStep" text="Edit: %s"/>
|
||||
<text name="spu_guide_editSuffix" text=" [Edit]"/>
|
||||
<text name="spu_guide_sum_hint" text="Click a line to change just that."/>
|
||||
<text name="spu_guide_auto" text="Automatic (recommended)"/>
|
||||
<text name="spu_guide_manual" text="Choose myself"/>
|
||||
<text name="spu_guide_closed" text="Guide closed — the field/flow no longer exists"/>
|
||||
<text name="spu_guide_cannotAdvance" text="Choose first"/>
|
||||
<text name="spu_guide_pickField" text="Pick a field first"/>
|
||||
<text name="spu_guide_saved" text="F%d saved"/>
|
||||
<text name="spu_guide_noMarker" text="! Place an AD marker at F%d — work waits for the marker"/>
|
||||
<text name="spu_guide_field_open" text="Set up field"/>
|
||||
<text name="spu_guide_fleet_open" text="Field fleet"/>
|
||||
<text name="spu_guide_fleet_close" text="Back to fields"/>
|
||||
<text name="spu_guide_field_title" text="— Set up field F%d —"/>
|
||||
<text name="spu_guide_field_titleNone" text="— Set up field —"/>
|
||||
<text name="spu_guide_field_step_field" text="Field"/>
|
||||
<text name="spu_guide_field_step_crop" text="Crop"/>
|
||||
<text name="spu_guide_field_step_cropChoice" text="Crop options"/>
|
||||
<text name="spu_guide_field_step_prep" text="Preparation"/>
|
||||
<text name="spu_guide_field_step_care" text="Crop care"/>
|
||||
<text name="spu_guide_field_step_machines" text="Machines"/>
|
||||
<text name="spu_guide_field_step_store" text="Storage"/>
|
||||
<text name="spu_guide_field_step_summary" text="Summary"/>
|
||||
<text name="spu_guide_field_hint_field" text="Pick the field you want to set up."/>
|
||||
<text name="spu_guide_field_hint_crop" text="What should grow here? 'Keep' carries on with the current crop."/>
|
||||
<text name="spu_guide_field_hint_cropChoice" text="What happens to the crop when mowing or harvesting?"/>
|
||||
<text name="spu_guide_field_hint_prep" text="After harvest / on bare land. Automatic plows or cultivates and limes as needed."/>
|
||||
<text name="spu_guide_field_hint_care" text="While the crop grows. Fertilizing/spraying only happens when needed."/>
|
||||
<text name="spu_guide_field_hint_machines" text="How many at a time, and should the field have its own dedicated machines?"/>
|
||||
<text name="spu_guide_field_hint_store" text="Where should the harvest go?"/>
|
||||
<text name="spu_guide_field_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||
<text name="spu_guide_crop" text="Crop"/>
|
||||
<text name="spu_guide_crop_keep" text="Keep what grows"/>
|
||||
<text name="spu_guide_crop_sowAgain" text="Sow the same crop again after harvest"/>
|
||||
<text name="spu_guide_prep" text="Preparation"/>
|
||||
<text name="spu_guide_prep_autoText" text=" Plows when the soil needs it, otherwise cultivates; limes at low pH."/>
|
||||
<text name="spu_guide_care" text="Crop care"/>
|
||||
<text name="spu_guide_care_autoText" text=" Fertilizes up to max while the crop grows, removes weeds and limes at low pH."/>
|
||||
<text name="spu_guide_rigs" text="Machines at a time (multi-tool)"/>
|
||||
<text name="spu_guide_dedicated" text="Dedicated machines for this field?"/>
|
||||
<text name="spu_guide_useFieldSilo" text="Use the field silo?"/>
|
||||
<text name="spu_guide_store" text="Storage"/>
|
||||
<text name="spu_guide_store_autoText" text=" Nearest silo with room (silage: fermenting silo)."/>
|
||||
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||
<text name="spu_guide_sum_sowAgain" text="sow again"/>
|
||||
<text name="spu_guide_sum_rake" text="rake"/>
|
||||
<text name="spu_guide_sum_noRake" text="no rake"/>
|
||||
<text name="spu_guide_sum_straw_chop" text="straw chopped"/>
|
||||
<text name="spu_guide_sum_straw_collect" text="straw collected"/>
|
||||
<text name="spu_guide_sum_straw_bale" text="straw baled"/>
|
||||
<text name="spu_guide_sum_auto" text="Automatic"/>
|
||||
<text name="spu_guide_sum_lime" text="lime"/>
|
||||
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||
<text name="spu_guide_sum_roll" text="roll"/>
|
||||
<text name="spu_guide_sum_fert" text="fertilizer"/>
|
||||
<text name="spu_guide_sum_weed" text="weeds"/>
|
||||
<text name="spu_guide_sum_noCare" text="no care"/>
|
||||
<text name="spu_guide_sum_dedicated" text="%d dedicated"/>
|
||||
<text name="spu_guide_sum_freeRigs" text="free machines"/>
|
||||
<text name="spu_guide_sum_rigs" text="%d at a time"/>
|
||||
<text name="spu_guide_sum_unloaders" text="%d unloaders"/>
|
||||
<text name="spu_guide_sum_fieldSilo" text="Field silo"/>
|
||||
<text name="spu_guide_sum_stores" text="%d chosen silos"/>
|
||||
<text name="spu_guide_flow_open" text="Set up flow"/>
|
||||
<text name="spu_guide_flow_title" text="— Set up flow: %s —"/>
|
||||
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||
<text name="spu_guide_flow_saved" text="Flow '%s' saved"/>
|
||||
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||
<text name="spu_guide_flow_rename" text="Change name …"/>
|
||||
<text name="spu_guide_flow_step_name" text="Name"/>
|
||||
<text name="spu_guide_flow_step_direction" text="Direction"/>
|
||||
<text name="spu_guide_flow_step_from" text="From"/>
|
||||
<text name="spu_guide_flow_step_to" text="To"/>
|
||||
<text name="spu_guide_flow_step_goods" text="Goods"/>
|
||||
<text name="spu_guide_flow_step_store" text="Storage"/>
|
||||
<text name="spu_guide_flow_step_rules" text="Rules"/>
|
||||
<text name="spu_guide_flow_step_wait" text="Wait at"/>
|
||||
<text name="spu_guide_flow_step_tractors" text="Tractors"/>
|
||||
<text name="spu_guide_flow_step_summary" text="Summary"/>
|
||||
<text name="spu_guide_flow_hint_name" text="The flow's name (tractors are named after it)."/>
|
||||
<text name="spu_guide_flow_hint_direction" text="Into building: keep barns/factories filled. Out of building: empty, store and sell."/>
|
||||
<text name="spu_guide_flow_hint_buildings_in" text="Which barns/factories should be filled? The marker is found automatically."/>
|
||||
<text name="spu_guide_flow_hint_buildings_out" text="Where should goods be picked up? The marker is found automatically."/>
|
||||
<text name="spu_guide_flow_hint_goods_in" text="Automatic fetches whatever the target lacks most on each trip."/>
|
||||
<text name="spu_guide_flow_hint_goods_out" text="For each good: Store, Sell, or Sell else store."/>
|
||||
<text name="spu_guide_flow_hint_store" text="Where should stored goods go?"/>
|
||||
<text name="spu_guide_flow_hint_rules_in" text="Number of trips, and when the tractor leaves Wait."/>
|
||||
<text name="spu_guide_flow_hint_rules_out" text="When to sell, and when to drive to storage."/>
|
||||
<text name="spu_guide_flow_hint_wait" text="Where does the tractor wait with a leftover load?"/>
|
||||
<text name="spu_guide_flow_hint_tractors" text="Which tractors run this flow? Tractors in other flows are hidden."/>
|
||||
<text name="spu_guide_flow_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||
<text name="spu_guide_flow_goods" text="Goods"/>
|
||||
<text name="spu_guide_flow_goodsAutoChoice" text="Automatic (what the building needs)"/>
|
||||
<text name="spu_guide_flow_goodsAutoText" text=" Each trip fetches the good the target lacks most."/>
|
||||
<text name="spu_guide_flow_noGoodsTo" text=" First choose where to deliver (step To)"/>
|
||||
<text name="spu_guide_flow_noProducts" text=" First choose where to pick up (step From)"/>
|
||||
<text name="spu_guide_flow_storeAutoChoice" text="Automatic (nearest with room)"/>
|
||||
<text name="spu_guide_flow_storeAutoText" text=" Storage trips go to the nearest store with room."/>
|
||||
<text name="spu_guide_flow_noTractors" text=" No free tractors"/>
|
||||
<text name="spu_guide_flow_otherFlows" text=" %d tractors run in other flows and are hidden"/>
|
||||
<text name="spu_guide_flow_blockedShow" text=" %d tractors cannot be used here (trailer does not fit) — click to see why"/>
|
||||
<text name="spu_guide_flow_blockedHide" text=" %d tractors cannot be used here — click to hide"/>
|
||||
<text name="spu_guide_flow_blocked" text=" %d tractors cannot be used here (trailer does not fit)"/>
|
||||
<text name="spu_guide_flow_none" text="none chosen"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
||||
<text name="spu_guide_flow_sumRound" text="collection round"/>
|
||||
<text name="spu_guide_flow_sumLoops" text="%d trips"/>
|
||||
<text name="spu_guide_flow_sumLoopsInf" text="endless trips"/>
|
||||
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
|
||||
<text name="spu_guide_flow_sumNoTractors" text="none"/>
|
||||
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
|
||||
<text name="spu_tab_fieldsilos" text="Field silos"/>
|
||||
<text name="spu_fs_enabled" text="Bring field silos home automatically"/>
|
||||
<text name="spu_fs_maxVehicles" text="Max. vehicles out"/>
|
||||
<text name="spu_fs_minLiters" text="Minimum amount per product"/>
|
||||
<text name="spu_fs_status" text="%d/%d vehicles out · %d silos waiting"/>
|
||||
<text name="spu_fs_noStores" text="Select at least one farm storage below — otherwise nothing is brought home"/>
|
||||
<text name="spu_fs_silosTitle" text="Field silos with products"/>
|
||||
<text name="spu_fs_noVehicle" text="No free unloading trailer: %d without AD park destination, %d busy, %d with a player in it, %d not in the fleet (flow vehicles are never borrowed)"/>
|
||||
<text name="spu_fs_nothing" text="No field silo has products above the minimum amount"/>
|
||||
<text name="spu_fs_col_silo" text="Silo"/>
|
||||
<text name="spu_fs_col_liters" text="Liters"/>
|
||||
<text name="spu_fs_col_onWay" text="En route"/>
|
||||
<text name="spu_fs_col_state" text="Status"/>
|
||||
<text name="spu_fs_state_ok" text="ok"/>
|
||||
<text name="spu_fs_state_noMarker" text="missing marker"/>
|
||||
<text name="spu_fs_state_noFarmStore" text="no farm storage takes %s"/>
|
||||
<text name="spu_fs_state_paused" text="paused (did not get going)"/>
|
||||
<text name="spu_fs_storesTitle" text="Farm storages (where products are brought home to)"/>
|
||||
<text name="spu_fs_toProduction" text="Send surplus to production (barn feed first)"/>
|
||||
<text name="spu_fs_reserveDays" text="Feed reserve: barn consumption for"/>
|
||||
<text name="spu_fs_days" text="%d days"/>
|
||||
<text name="spu_fs_toFactory" text="→ factory"/>
|
||||
<text name="spu_fs_harvestHeld" text="%d unloading trailer(s) held back for working harvesters"/>
|
||||
<text name="spu_view_title" text="Show"/>
|
||||
<text name="spu_view_items" text="Goods"/>
|
||||
<text name="spu_view_buildings" text="Buildings"/>
|
||||
<text name="spu_view_tooltip" text="Goods: total stock per good (click to see where it is stored). Buildings: table for one building."/>
|
||||
<text name="spu_kpi_value" text="Stock value now"/>
|
||||
<text name="spu_kpi_gain" text="Extra at top price"/>
|
||||
<text name="spu_kpi_empty" text="Running out soon"/>
|
||||
<text name="spu_kpi_full" text="Almost full"/>
|
||||
<text name="spu_attention" text="— Needs attention —"/>
|
||||
<text name="spu_attention_none" text="Nothing needs attention right now"/>
|
||||
<text name="spu_attention_hintFlow" text=" → click: create flow"/>
|
||||
<text name="spu_attention_hintOpen" text=" → click: open flow '%s'"/>
|
||||
<text name="spu_attention_hintProduce" text=" → produce/buy"/>
|
||||
<text name="spu_attention_produce" text="A flow won't help — the farm only has %2$s %1$s in total. Produce or buy more."/>
|
||||
<text name="spu_attention_flowName" text="%s to %s"/>
|
||||
<text name="spu_attention_hintSellFlow" text=" → click: create sell flow"/>
|
||||
<text name="spu_attention_sellFlowName" text="Sell %s"/>
|
||||
<text name="spu_alert_empty" text="%s runs out in %s — farm has %s, uses %s/day"/>
|
||||
<text name="spu_alert_fullIn" text="%s is %d %% full — full in %s"/>
|
||||
<text name="spu_alert_full" text="%s is %d %% full"/>
|
||||
<text name="spu_alert_sell" text="Sell %s now: price is %d %% of the yearly top — %s"/>
|
||||
<text name="spu_items_hint" text="(click a good)"/>
|
||||
<text name="spu_icol_amount" text="Stock"/>
|
||||
<text name="spu_icol_value" text="Value now"/>
|
||||
<text name="spu_icol_rate" text="Farm net · lasts"/>
|
||||
<text name="spu_item_perDay" text="%s/day"/>
|
||||
<text name="spu_item_fullIn" text="full in %s"/>
|
||||
<text name="spu_item_balanced" text="balanced"/>
|
||||
<text name="spu_item_share" text="%d %% of the good"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.26.0.0</version>
|
||||
<version>1.60.21.0</version>
|
||||
<title>
|
||||
<en>AD Smart Pickup</en>
|
||||
</title>
|
||||
@@ -12,6 +12,10 @@
|
||||
<multiplayer supported="true"/>
|
||||
<l10n filenamePrefix="l10n/l10n"/>
|
||||
<extraSourceFiles>
|
||||
<sourceFile filename="adCache.lua"/>
|
||||
<sourceFile filename="adNaming.lua"/>
|
||||
<sourceFile filename="adNames.lua"/>
|
||||
<sourceFile filename="adStuckDiag.lua"/>
|
||||
<sourceFile filename="adSupplyPlanner.lua"/>
|
||||
<sourceFile filename="adRuns.lua"/>
|
||||
<sourceFile filename="adRunsStorage.lua"/>
|
||||
@@ -22,18 +26,35 @@
|
||||
<sourceFile filename="adOutboundPlanner.lua"/>
|
||||
<sourceFile filename="adStorageStats.lua"/>
|
||||
<sourceFile filename="adFlowView.lua"/>
|
||||
<sourceFile filename="adGuide.lua"/>
|
||||
<sourceFile filename="adFlowGuide.lua"/>
|
||||
<sourceFile filename="adSources.lua"/>
|
||||
<sourceFile filename="adBuildings.lua"/>
|
||||
<sourceFile filename="adOutbound.lua"/>
|
||||
<sourceFile filename="adTipSide.lua"/>
|
||||
<sourceFile filename="adUnloadWait.lua"/>
|
||||
<sourceFile filename="adFieldModel.lua"/>
|
||||
<sourceFile filename="adFieldFleet.lua"/>
|
||||
<sourceFile filename="adFieldView.lua"/>
|
||||
<sourceFile filename="adFieldGuide.lua"/>
|
||||
<sourceFile filename="adFields.lua"/>
|
||||
<sourceFile filename="adCpBridge.lua"/>
|
||||
<sourceFile filename="adCourseCache.lua"/>
|
||||
<sourceFile filename="adFieldStorage.lua"/>
|
||||
<sourceFile filename="adManualTransport.lua"/>
|
||||
<sourceFile filename="adFieldWork.lua"/>
|
||||
<sourceFile filename="adFieldSiloPlanner.lua"/>
|
||||
<sourceFile filename="adFieldSiloConfig.lua"/>
|
||||
<sourceFile filename="adFieldSilos.lua"/>
|
||||
<sourceFile filename="adCpResume.lua"/>
|
||||
<sourceFile filename="adHitch.lua"/>
|
||||
<sourceFile filename="adHitchCreep.lua"/>
|
||||
<sourceFile filename="adPool.lua"/>
|
||||
<sourceFile filename="adFlowWagons.lua"/>
|
||||
<sourceFile filename="adFleetNames.lua"/>
|
||||
<sourceFile filename="adFieldJobs.lua"/>
|
||||
<sourceFile filename="adMonthView.lua"/>
|
||||
<sourceFile filename="adProfiler.lua"/>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
</modDesc>
|
||||
|
||||
@@ -1,265 +1,96 @@
|
||||
# FS25_ADSmartPickup
|
||||
# AD Smart Pickup (FS25_ADSmartPickup)
|
||||
|
||||
**Brugermanual for spillere:** [dansk](docs/BRUGERMANUAL.md) · [English](docs/USER_MANUAL.md). Resten af denne fil er udviklernoter pr. version.
|
||||
Tillæg til **AutoDrive** i Farming Simulator 25. Du fortæller, hvad der skal køres hvorhen, og moden
|
||||
styrer dine AutoDrive-traktorer tur for tur. Markarbejdet kører med Courseplay på marken.
|
||||
Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
|
||||
|
||||
AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Deliver:
|
||||
*English summary below.*
|
||||
|
||||
## 1. Vare-bevidst mappe-rotation (én vare valgt)
|
||||
## Hvad moden gør
|
||||
|
||||
Når pålæsningsmål roteres i en AD-mappe, køres der direkte til den silo i mappen, der har den
|
||||
valgte vare — mindst fyldte først (mælk/gylle: fyldeste først) — i stedet for at prøve markørerne én efter én.
|
||||
- **Forsyning:** holder stalde og fabrikker fyldt. Hver tur henter den vare, målet mangler mest, fra
|
||||
den bedste silo på kortet.
|
||||
- **Udkørsel:** tømmer fabrikker, stalde, siloer og palleskure. Varerne køres til lager eller sælges, når
|
||||
prisen er tæt på årets top, uden at sælge det, gården selv skal bruge.
|
||||
- **Flows:** en navngivet opskrift med bygninger, varer, regler og traktorer. Flows sættes op i en guide og
|
||||
startes eller stoppes samlet fra en farvet liste.
|
||||
- **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og
|
||||
sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje.
|
||||
- **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen,
|
||||
og Courseplay arbejder på marken.
|
||||
- **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre.
|
||||
- **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter
|
||||
det flow, der løser problemet.
|
||||
- **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes
|
||||
med savegamet.
|
||||
|
||||
## 2. Behovsstyret forsyning (flere varer valgt)
|
||||
## Download og installation
|
||||
|
||||
Én traktor holder alle input i en produktion fyldt — fx hø, græs, ensilage og sukkerroer til en
|
||||
TMR-blander — i stedet for én traktor pr. vare.
|
||||
1. Hent `FS25_ADSmartPickup.zip` under [Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases).
|
||||
2. Læg zip-filen (ikke udpakket) i `Documents/My Games/FarmingSimulator2025/mods/`.
|
||||
3. Aktivér moden sammen med AutoDrive, når du vælger savegame. Menuen **Smart Pickup** ligger i
|
||||
ESC-menuen lige over Indstillinger.
|
||||
|
||||
1. Aflæsningsmål = produktionens markør.
|
||||
2. Multi-vælg ingredienserne i AutoDrives vare-dropdown.
|
||||
3. Start Pickup&Deliver. Loop-tælleren virker som normalt (N læs → parkér, 0 = uendeligt).
|
||||
## Krav
|
||||
|
||||
Pr. tur læses målets lager; den ingrediens der mangler mest (relativt til kapacitet) hentes fra en
|
||||
silo **hvor som helst på kortet** (eget lager før nabo-lån, mindst fyldte først). Ingredienser uden
|
||||
plads i målet (< 1000 l fri) eller uden kilde springes over. Mens AD kører viser HUD'en kun turens
|
||||
vare (AutoDrive ville ellers selv rotere vare ved triggeren); når AD stopper, gendannes hele listen.
|
||||
Slå fra ved at vælge en anden enkelt vare.
|
||||
- **FS25_AutoDrive** (påkrævet) og et AutoDrive-vejnet mellem bygningerne.
|
||||
- **Courseplay** anbefales. Det er nødvendigt for markarbejdet.
|
||||
- **Universal Autoload** anbefales på vogne, der skal køre paller.
|
||||
|
||||
"Mest brug for" defineres i `ADSupplyPlanner.scoreNeed` (`adSupplyPlanner.lua`).
|
||||
## Dokumentation
|
||||
|
||||
### Fabriksstyret (v1.6): målet er et produktionspunkt
|
||||
- Brugermanual: [dansk](docs/BRUGERMANUAL.md) · [English](docs/USER_MANUAL.md)
|
||||
- Ændringer pr. version: [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
Er aflæsningsmålet en fabrik (produktionspunkt), **erstattes** ingredienslisten af inputs fra
|
||||
fabrikkens ønskede produktioner — slår du fx TMR til i en AIO-fabrik, begynder traktoren at hente
|
||||
ensilage/hø/halm/roer/mineralfoder af sig selv; slår du den fra, holder den op. Dit multi-valg er kun
|
||||
tændknappen (og det gendannes ved stop). Listen genudledes hver tur, begrænset til det vognen kan bære.
|
||||
## Feedback
|
||||
|
||||
- **Ønsket produktion** = tændt, ELLER slukket af kortet med status "mangler input" (Dakotas
|
||||
`ExtendedProductionPoint` slår produktioner fra automatisk, når de løber tør — dem skal vi netop
|
||||
fodre). Manuelt slukket = ikke ønsket.
|
||||
- **Prioritet = tid til tom:** `lager / (Σ amount × cyclesPerMinute)` over ønskede produktioner —
|
||||
den vare der løber tør først hentes først, uanset relativ fyldning. Loggen viser forbrug og minutter.
|
||||
- Fabrik uden ønskede produktioner, eller inputs vognen ikke kan bære → dit multi-valg som hidtil.
|
||||
Fejl og idéer: giv udvikleren besked (issues her kræver en konto på git.powerdata.dk). Vedlæg gerne `log.txt` fra
|
||||
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
|
||||
|
||||
### Kilder: en fabriks inputs stjæles aldrig (v1.6.1)
|
||||
## Udvikling
|
||||
|
||||
En fabriks læssestation udleverer også dens **inputs** (halm i en TMR-fabrik). Sådan en vare hentes
|
||||
aldrig derfra — kun fabrikkens outputs og rigtige siloer er kilder. Gælder både mappe-logik og
|
||||
behovsstyring. Varer der både er input og output (kæde) må hentes.
|
||||
- Mod-koden ligger i `FS25_ADSmartPickup/`.
|
||||
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||
|
||||
### Stalde som mål (v1.6.1)
|
||||
---
|
||||
|
||||
Kostald m.fl. svarer selv for niveau/kapacitet (`getHusbandryFillLevel/Capacity`), så halm+TMR til en
|
||||
stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder.
|
||||
## English summary
|
||||
|
||||
### Wait-punkter: rest-læs ved fuld stald (v1.7)
|
||||
**AD Smart Pickup** is an add-on for **AutoDrive** in Farming Simulator 25. You tell it what goes where,
|
||||
and it drives your AutoDrive tractors trip by trip. Field work runs with Courseplay on the field. Everything
|
||||
is set up in the **Smart Pickup** menu in the ESC menu.
|
||||
|
||||
AutoDrive holder ellers en traktor med rest-læs stille ved aflæsningspunktet, til den er tømt — og
|
||||
spærrer for de næste i køen. Med Wait-punkter:
|
||||
**Features**
|
||||
|
||||
1. Læg markører i en **mappe med ordet `Wait` i navnet** (fx mappen `Cow Wait`) — så mange du har
|
||||
plads til; de er en fælles pulje for alle traktorer. Markørernes egne navne er ligegyldige, så
|
||||
dine mark-ventepladser ("F91 Wait" i mark-mapperne) rører den ikke.
|
||||
2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt
|
||||
(modden holder styr på reservationer) og venter dér.
|
||||
3. Når målet igen har plads til **hele** resten — eller er mindst **halvt tomt** (kæmpe vogne kan aldrig
|
||||
tømme hele resten i en lille stald) — kører den tilbage og læsser af. Derefter normal tur.
|
||||
4. Wait-punkterne SKAL være forbundet til vejnettet både fra målets udkørsel og tilbage til målet.
|
||||
Modden tjekker ruten begge veje før den omdirigerer; uden rute prøves næste punkt (AutoDrive
|
||||
ville ellers vise "cannot reach" og slukke sig selv).
|
||||
- **Supply:** keeps barns and factories filled. Each trip fetches the good the target lacks most, from
|
||||
the best silo on the map.
|
||||
- **Outbound:** empties factories, barns, silos and pallet sheds. Goods are stored or sold near the
|
||||
yearly top price, and the mod never sells what the farm needs itself.
|
||||
- **Flows:** a named recipe of buildings, goods, rules and tractors. You set flows up in a guide and
|
||||
start or stop them from a colour-coded list.
|
||||
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
|
||||
marker and park it again. Detached implements and free tractors form a shared pool.
|
||||
- **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive
|
||||
drives the roads and Courseplay works the field.
|
||||
- **Field silos:** goods in field silos are brought home to the farm's storages automatically.
|
||||
- **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list
|
||||
creates the flow that fixes the problem.
|
||||
- **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved
|
||||
with the savegame.
|
||||
|
||||
Kun den aktuelle aflæsningsopgaves destination ændres — mål, rotation og loop-tæller røres ikke.
|
||||
Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet.
|
||||
Ingen ledige Wait-punkter → AutoDrive venter ved målet som i dag (én advarsel i loggen).
|
||||
Seks markører må gerne hedde det samme (fx `Cow Wait`); loggen viser markørnummeret.
|
||||
**Install:** download `FS25_ADSmartPickup.zip` from
|
||||
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
|
||||
in `Documents/My Games/FarmingSimulator2025/mods/`. Enable the mod together with AutoDrive when you pick
|
||||
the savegame. The **Smart Pickup** menu is in the ESC menu, just above Settings.
|
||||
|
||||
**Læs der passer, foretrækkes (v1.7):** ved valg af næste tur går varer, hvor et fuldt læs kan komme af
|
||||
i målet, forud; passer intet fuldt læs, tages den mest trængende alligevel (og Wait-punktet bruges).
|
||||
**Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is
|
||||
needed for pallets.
|
||||
|
||||
**Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*.
|
||||
Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet,
|
||||
så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en
|
||||
fabrik ("kun aflæsning") — så kalder AutoDrive aldrig modden, og alt valgt læsses dér.
|
||||
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
|
||||
[Changelog](CHANGELOG.md)
|
||||
|
||||
## v1.8 — Runs i ESC-menuen
|
||||
**Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with
|
||||
`ADSmartPickup`.
|
||||
|
||||
Et **run** er en navngivet opskrift: mål + varer + Wait-pulje + ture, samt en liste traktorer der skal
|
||||
køre den. I stedet for at sætte AutoDrive op manuelt på hver traktor, defineres run'et ét sted og
|
||||
startes/stoppes samlet for alle traktorer i det.
|
||||
|
||||
**Oprettelse:** ESC → fanen "Smart Pickup" → "+ Nyt run". Runs vælges i Run-selectoren øverst;
|
||||
"Omdøb" og "Slet run" findes som knapper nederst sammen med Start/Stop.
|
||||
|
||||
**Felter:**
|
||||
|
||||
- **Mål** — dropdown med aflæsningsmål (markører med aflæsningsstation, fx en stald eller fabrik).
|
||||
- **Varer** — Auto (følger målets behov, som ved almindelig behovsstyret forsyning) eller Vælg
|
||||
(afkrydsning af bestemte varer).
|
||||
- **Wait-pulje** — hvilken `Wait`-mappe rest-læs skal vente i ("Alle (nærmeste ledige)" eller en
|
||||
navngivet pulje som fx "Cow Wait").
|
||||
- **Ture** — loop-tælleren (0–99, 0 = uendeligt), som normalt i AutoDrives Pickup&Deliver.
|
||||
- **Traktorer** — rækker med Ja/Nej (er traktoren med i run'et), kapacitet og et ✔/✖-ikon. En
|
||||
**✖-traktor springes over ved Start**, med en grund vist ved siden af (fx "ingen vogn", "vognen
|
||||
kan ikke bære run'ets varer", "for langt fra vejnettet" eller "intet mål"). Sættes en traktor til
|
||||
Nej (fjernet fra run'et), rører det IKKE dens AutoDrive-vareliste — run'ets varer forbliver valgt
|
||||
i traktorens AD-vareliste (synlig/redigerbar i AD's eget HUD) indtil de ændres dér manuelt.
|
||||
|
||||
**Start/Stop:** "Start run" sætter AutoDrive op på hver ✔-traktor: mode Pickup&Deliver, målmarkør,
|
||||
nærmeste pålæsningsmarkør som start, run'ets varer valgt, run'ets tur-tæller, mapper ("Brug mapper")
|
||||
slået til, og rotation sat til "kun pålæsning" (bemærk: "Brug mapper" er en global
|
||||
AutoDrive-indstilling og slås til for hele spillet, ikke kun run'ets traktorer). **Dette overskriver
|
||||
traktorens hidtidige manuelle AutoDrive-indstillinger.** "Stop run" stopper AutoDrive på hver traktor
|
||||
i run'et (medlemskabet i run'et bevares, så det kan startes igen). Traktorer der ikke er del af noget
|
||||
run, kører som før — bortset fra at "Brug mapper" kan være blevet slået til globalt af et run.
|
||||
|
||||
**Wait-puljer-afsnittet** nederst på siden viser hver Wait-mappe skrivebeskyttet: antal punkter og
|
||||
hvilken traktor der holder hvor.
|
||||
|
||||
**Gemning:** Runs gemmes i `savegameN/ADSmartPickup_runs.xml` (ved gem og ved Start/Stop). De
|
||||
**genstartes ikke automatisk** ved indlæsning af et savegame — kør "Start run" igen efter load, hvis
|
||||
traktorerne skal køre videre.
|
||||
|
||||
## v1.9 — læs-bytte fra Wait
|
||||
|
||||
Venter en traktor ved et Wait-punkt med en vare målet er fuldt af, mens målet **samtidig mangler
|
||||
en anden vare** på traktorens liste (run eller AD's multi-valg) — kan der ellers gå i hårdknude:
|
||||
alle traktorer venter med den ene vare, mens ingen henter den anden. Hvert 5. sekund tjekkes derfor:
|
||||
|
||||
1. Er der en anden vare i listen, målet mangler et **fuldt** læs af (og som kan hentes fra en
|
||||
silo)? Ellers venter traktoren som hidtil.
|
||||
2. Findes en **ren lager-silo** (ikke stald, ikke fabrik, og ikke målet selv) med en
|
||||
aflæsningsstation ved en AD-markør, der tager varen og har fri plads ≥ resten, samt vejnet
|
||||
både til siloen og videre fra siloen til målet? Siloer der **allerede har varen** foretrækkes,
|
||||
ellers den nærmeste.
|
||||
3. Findes en silo: Wait-punktet frigives, traktoren kører derhen og læsser resten af. Når vognen
|
||||
er tom, henter næste tur den vare målet mangler.
|
||||
4. Findes ingen egnet silo: traktoren bliver ved Wait-punktet (som før v1.9).
|
||||
|
||||
Loglinjer: `'<navn>' bytter læs: N l STRAW tilbage i <markør> (markør n), målet mangler FORAGE` ved
|
||||
bytte, og én gang pr. venteperiode `'<navn>' venter med N l STRAW, og målet mangler FORAGE — ingen
|
||||
silo med plads og vejnet til/fra tager resten, så den bliver ved Wait-punktet` når ingen silo findes.
|
||||
|
||||
Kræver at traktoren har **begge** varer i sin liste (run's varer eller AD's multi-valg) — kun én
|
||||
vare valgt betyder ingen bytte-mulighed, og traktoren venter som hidtil. Wait-logikken gælder desuden
|
||||
kun mens aflæsningsopgaven peger på AD's rigtige aflæsningsmål, så en silo-omvej (fx efter et bytte)
|
||||
aldrig fejlagtigt bliver opfattet som et fuldt mål.
|
||||
|
||||
## v1.13 — Flow-tavle (bygningsbaseret opsætning)
|
||||
|
||||
ESC-fanen har nu to under-faner: **Flows** og **Lagre**. Flows viser én række pr. flow (run):
|
||||
`> Mælk: Sukker · Kostald 1 + Kostald 2 → salg · pris ≥ 90 %, lager > 80 % · Virtus · kører (1/1) — SUGAR 78 % af top`
|
||||
og under tavlen en detail-blok for det valgte flow: **Retning** (ind til bygning / ud fra bygning),
|
||||
**Fra** (udkørsel) eller **Til** (forsyning) som afkrydsning pr. **bygning** — moden finder selv den
|
||||
AD-markør der står nærmest bygningens læsse-/aflæsningstrigger (≤ 40 m; siloer via det strenge
|
||||
opslag) og viser den: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `INGEN MARKØR inden for
|
||||
40 m`. Derefter Varer, Regler, Vent ved, Traktorer og Status som før. Forsyning har "Fra = alle
|
||||
siloer" (som hidtil). Wait-puljerne står nederst.
|
||||
|
||||
Gamle runs oversættes automatisk: markører der er en bygnings markør bliver til bygningen; rå
|
||||
markører uden bygning beholdes og vises som `markør '…'`. Start afviser med `'<bygning>' mangler en
|
||||
AD-markør ved triggeren`, når en valgt bygning ingen markør har. Runs gemmes som XML version 3.
|
||||
|
||||
## v1.12 — Udkørsel af produkter
|
||||
|
||||
Et run har nu en **retning**: *Forsyning* (som hidtil) eller *Udkørsel*. ESC-fanen har tre
|
||||
under-faner: **Oversigt** · **Forsyning** · **Udkørsel**.
|
||||
|
||||
Et udkørsels-run består af:
|
||||
|
||||
- **Kilder** (én eller flere AD-markører): en **fabrik** (produktionspunkt, dens outputs),
|
||||
en **silo** (`spec_silo`, dens indhold) eller et **objektlager** (palle-/ballelager som HVF-
|
||||
pakkens; paller tages ud automatisk). Kilden genkendes inden for 40 m af markøren.
|
||||
- **Varer** med politik pr. vare: **Fra** · **Lager** · **Salg** · **Salg, ellers lager**.
|
||||
Bulk-varer kræver tipvogn/tank; pallevarer kræver **Universal Autoload** på vognen.
|
||||
- **Regler:** *Sælg fra* (% af årets toppris, standard 90), *Kør til lager over* (% fyldning i
|
||||
kilden, standard 80), *Saml op på én tur* (opsamlingsrunde).
|
||||
- Wait-pulje og traktorer som i forsynings-runs.
|
||||
|
||||
**Turvurdering** (før hver pålæsning, og hvert 5. sekund mens traktoren holder):
|
||||
|
||||
1. *Salg*: en vare med Salg-politik, hvis bedst betalende **nåelige** salgssted giver mindst
|
||||
tærsklen af årets toppris — **kører uanset mængde**. Højeste pris-% først. Toppris = stationens
|
||||
grundpris nu × (årets højeste periodefaktor ÷ nuværende). Stor efterspørgsel giver > 100 %.
|
||||
2. *Lager*: en vare med Lager-politik, når der er et **fuldt læs** eller kildens lager er over
|
||||
tærsklen — til nærmeste silo med plads til hele læsset og vejnet begge veje (aldrig en kilde,
|
||||
fabrik eller stald). Mest fyldte kilde først.
|
||||
3. Ingen tur → traktoren holder ved et ledigt Wait-punkt (run'ets pulje) og prøver igen.
|
||||
Loggen viser hvorfor: `holder ved Wait 3: SUGAR 78 % af top (kræver 90), lager 41 %`.
|
||||
|
||||
**Opsamlingsrunde:** tipvogn = samme vare fra flere kilder efter tur til vognen er fuld (næste
|
||||
kilde kun ved ≥ 10 % fri plads); autoload-vogn = flere pallevarer med samme mål på én tur.
|
||||
|
||||
**Objektlager:** markøren skal stå så vognens autoload-zone dækker lagerets spawn-område (som når
|
||||
man læsser manuelt). Moden beder lageret spawne **én palle ad gangen** og venter til UAL har taget
|
||||
den, så der aldrig ligger paller tilbage. Sidder lageret fast (kendt spilfejl), afsluttes
|
||||
læsningen efter 30 s med det der er. Ved en autoload-kilde uden fremdrift i 15 s køres der med
|
||||
det der er ("uanset mængde").
|
||||
|
||||
**Advarsler på skærmen:** kan intet salgssted nås (vejnet), eller findes ingen silo med plads,
|
||||
vises en rød AutoDrive-notifikation (`'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR –
|
||||
tjek vejnettet`), samme tekst i AD's notifikations-historik, i log.txt (WARN) og i run'ets status.
|
||||
Traktoren holder ved Wait og kører selv når vejen er koblet på. Advarslen gentages først når
|
||||
traktor, station eller vare ændrer sig.
|
||||
|
||||
**Prisregel (v1.12.1, metode fra Distribution Redux):** pris-% = nuværende periodes faktor ÷ årets
|
||||
højeste faktor i spillets faste sæsonkurve (samme data som prisgrafen) × stationens egen afvigelse
|
||||
(effektiv pris ÷ stationens grundpris nu). Sværhedsgraden falder ud; flade kurver (top/bund < 1,02)
|
||||
regnes som 100 %; stor efterspørgsel giver > 100 %.
|
||||
|
||||
**Reserve (v1.12.1):** før salg fratrækkes det gårdens egne fabrikker og stalde skal bruge det næste
|
||||
døgn (forbrug × 24 t minus det de allerede har, højst deres fri plads; fabrikker = ønskede
|
||||
produktioners inputs, stalde = foder/halm/vand-rate). Der sælges kun når et helt læs kan tages uden
|
||||
at røre reserven. Loggen/UI'et siger fx `SUGARBEET: 196.0k reserveret til TMR-fabrik`. Lager-politik
|
||||
rører ikke reserven (varen bliver på gården).
|
||||
|
||||
**Paller til lager (v1.12.1):** Lager-mål for pallevarer = nærmeste objektlager (palleskur) der tager
|
||||
varen og har plads; AutoDrive læsser UAL-vognen af dér.
|
||||
|
||||
**Tætte siloer (AutoDrive-indstilling):** AutoDrive tipper ved den første aflæsningstrigger inden for sin
|
||||
globale "Max trigger distance" (standard 25 m). Står siloerne tættere end det, kan nabosiloen få læsset —
|
||||
sæt AD-indstillingen til **10 m**. Moden advarer i loggen første gang et lagermål har en anden
|
||||
aflæsningsmarkør inden for afstanden.
|
||||
|
||||
**Fanen Lagre (v1.12.1):** gårdens beholdning pr. bygning (fabrikker med ▸ ved outputs, stalde,
|
||||
palleskure som antal paller, siloer), opdateret hvert sekund.
|
||||
|
||||
**Opsætning:** Udkørsel-fanen → Nyt run → afkryds kilder → sæt politik pr. vare → Ja på traktorer
|
||||
→ Start. AD-tilstand pr. traktor: PickupAndDeliver, 1./2. markør = første kilde (pladsholder,
|
||||
sættes pr. tur), rotation "kun pålæsning". Runs gemmes i `ADSmartPickup_runs.xml` version 2;
|
||||
ældre filer læses som forsynings-runs.
|
||||
|
||||
## Fælles
|
||||
|
||||
- Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en.
|
||||
- Siloens eget lager rangeres før nabo-lån (udvidelses-siloer deler lager inden for `storageRadius`).
|
||||
- Intet fund, intet behov eller fejl i et spil-API → AutoDrives originale rotation. Valg logges som `ADSmartPickup:` i log.txt.
|
||||
- Kræver i AutoDrive: "Brug mapper" (global AutoDrive-indstilling, gælder hele spillet) +
|
||||
"Rotér mål: pålæsning" på køretøjet (pr. køretøj).
|
||||
- **Markør ↔ silo (v1.3):** markøren er dér hvor trækkerens næse stopper; vognen holder et vogntog
|
||||
bagved. Markørens silo er derfor nærmeste trigger **bag næsen** set i ankomstretningen (forrige
|
||||
waypoint), højst 2 m foran. I tætte rækker (15 m mellem siloerne) er nabosiloens trigger ellers
|
||||
nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før.
|
||||
- **Kildestrategi (v1.5):** lagervarer hentes fra den **mindst fyldte** silo først (rester ryddes,
|
||||
siloer frigives). Varer hvis tanke *løber fulde* — mælk, gylle, digestat — hentes fra den **fyldeste**
|
||||
kilde først, så produktionen ikke går i stå. Listen er `ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES`
|
||||
(`adSupplyPlanner.lua`); tilføj fx `MANURE = true` hvis møddinger også skal tømmes fyldest-først.
|
||||
Loggen viser den brugte strategi. En trigger foran næsen tæller aldrig som markørens egen (v1.5).
|
||||
- **Spor-regel (v1.4):** triggeren skal ligge i trækkerens spor (højst 6 m til siden). En tank med
|
||||
aflæsningsbane på den ene side og pålæsningsbane på den anden giver ellers to markører til samme
|
||||
tank, og "Dropof"-markøren kunne vinde. Kendes retningen, men ligger intet i sporet (kurve ind til
|
||||
markøren), accepteres kun en trigger inden for 12 m.
|
||||
- **Kun fremme ved markøren (v1.4):** rotationen skjules først når målet er nået — undervejs ruller
|
||||
vognen gennem nabosiloers triggere, og dér skal AutoDrive køre videre ved tom trigger.
|
||||
- **Sikkerhedsventil (v1.4):** 30 s ved markøren uden at vognen fyldes → grebet slippes (advarsel i
|
||||
log), og AutoDrive kører videre som med almindelig rotation.
|
||||
- **Står og læsser (v1.2):** med rotation + mapper afslutter AutoDrive normalt pålæsningen i det øjeblik
|
||||
markøren nås (læsning skal være startet på vej ind, ellers kører den tom videre). Når moden har valgt
|
||||
siloen, ser AutoDrive under `LoadAtDestinationTask.update` rotationen som slået fra for netop det
|
||||
køretøj, så den holder ved markøren og læsser som uden rotation. Løber kilden tør, slippes grebet og
|
||||
turen fortsætter med det der er i vognen.
|
||||
|
||||
Begrænsninger: AD læsser fuld vogn — er der mindre plads i målet end ét læs, venter traktoren ved
|
||||
aflæsning. Målet skal være en placeable med aflæsningstrigger (ikke en blandevogn).
|
||||
|
||||
Test: `for t in tests/test_*.lua; do luajit $t | tail -1; done`
|
||||
Byg: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||
Design/plan: `docs/superpowers/`
|
||||
**Development:** the mod code is in `FS25_ADSmartPickup/`. Build the zip with
|
||||
`cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||
|
||||
+753
-290
File diff suppressed because it is too large
Load Diff
@@ -1,148 +0,0 @@
|
||||
# Testplan — Markarbejde (fanen Marker), version 1.26.0.0
|
||||
|
||||
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
||||
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
||||
begynder altid med `ADSmartPickup: F<marknr>`.
|
||||
|
||||
Kryds af undervejs. Skriv resultatet (OK / FEJL + hvad der skete) ud for hver test, så retter vi i
|
||||
samme rækkefølge.
|
||||
|
||||
---
|
||||
|
||||
## 0. Forberedelse
|
||||
|
||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.26.0.0) FS25_ADSmartPickup`.
|
||||
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
||||
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
||||
- [ ] **0.3 F-markører.** Marker uden `F<nr>`-markør står med `!` foran marknummeret og "! mangler
|
||||
markør F…". Lav markører til de marker, der skal testes.
|
||||
- [ ] **0.4 Markflåden.** Nederst: alle maskiner med slåmaskine, vender, river, læssevogn, høster,
|
||||
aflæsningsvogn, kultivator, plov, såmaskine, tromle, strigle/sprøjte, gødnings-/kalkspreder,
|
||||
stubhakker — med rolle og bredde. Tjek:
|
||||
- [ ] Høstere viser "N afgrøder".
|
||||
- [ ] Såmaskiner viser "frø ok" eller "!! tom for frø".
|
||||
- [ ] Strigle viser "strigle (småt ukrudt)", ukrudtssprøjte viser "sprøjte".
|
||||
- [ ] En **tom** sprøjte/spreder vises ikke (den har ingen rolle før den har fyld på).
|
||||
- [ ] Maskiner i et Smart Pickup-flow kan godt ses, men sendes aldrig ud.
|
||||
- [ ] **0.5 Bredder.** BiG M 450 viser **40,8 m** (Courseplays egen bredde), læssevognene 40,8 m.
|
||||
|
||||
## 1. Etape 1 genbekræftet (græs → ensilage)
|
||||
|
||||
Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set.
|
||||
|
||||
- [ ] **1.1 Afslutning af opsamling.** Mark 22 (eller en anden lille græsmark) på Manuel + Ensilage,
|
||||
BiG M + én Boss Alpin med flueben → **Start nu**.
|
||||
- Forvent: slåmaskine → F22 → Courseplay slår → hjem. Læssevogn → F22 → Courseplay samler op med
|
||||
**slåmaskinens kursus** → AutoDrive tømmer ved fuld → tilbage → færdig → rest til lager → parkering.
|
||||
Marken står **Færdig**, og der kommer en grøn besked "F22 er færdig".
|
||||
- Log: `mower … sendt til F22`, `Courseplay startet (mower)`, `mowing -> collecting`,
|
||||
`læssevogn … aflæsser i <gæringssilo>`, `Courseplay startet (collector, mower-kursus kopieret)`,
|
||||
`collector … færdig`, `collecting -> done`.
|
||||
- [ ] **1.2 Gæringssilo.** Ensilage-læs går til nærmeste **gæringssilo** (FermentingSilo/GrassSilageSilo),
|
||||
ikke en Farma-silo. Log: `aflæsser i <gæringssilo>` eller `ingen gæringssilo kan tage græsset (…)`.
|
||||
- [ ] **1.3 Sidde i maskinen.** Sæt dig i slåmaskinen mens Courseplay kører, og stig ud igen.
|
||||
Forvent: den slippes IKKE (ingen `frigivet`-linje). Kør selv > 3 km/t uden CP → `frigivet — spilleren kører det`.
|
||||
- [ ] **1.4 Sovende køretøj.** Parkerede maskiner kører af sted med det samme ved udsendelse, uden at
|
||||
du skal sætte dig ind.
|
||||
|
||||
## 2. Hø og river
|
||||
|
||||
- [ ] **2.1 Hø uden river.** Plan = **Hø**, Riv = Nej. Slå → **Vendes** (venderen i sin egen bredde) →
|
||||
Samles op (læssevogn på slåkurset, ≥ 40,8 m) → hø-silo (kun almindelig silo, aldrig gæringssilo).
|
||||
Log: `mowing -> tedding`, `Courseplay startet (tedder)`, `tedding -> collecting`.
|
||||
- [ ] **2.2 Smal læssevogn uden river.** En læssevogn under 40,8 m → status "læssevogn for smal:
|
||||
… — sæt Riv til eller brug en bredere".
|
||||
- [ ] **2.3 Riv (midterrive).** Riv = Ja. Log: `river lægger skåret 0.x m fra sin kursuslinje`,
|
||||
derefter `Courseplay startet (collector, rake-kursus kopieret)`. Læssevognen rammer skårene.
|
||||
- [ ] **2.4 Sideriver.** Med en river der lægger skåret til siden: log viser fx `3.2 m`, og
|
||||
`skår ligger 3.2 m til … -> læssevognens forskydning …`. **Tjek at vognen kører OVER skåret.**
|
||||
Kører den ved siden af (forkert side), så skriv det ned — så vender jeg fortegnet.
|
||||
- [ ] **2.5 Riv til ensilage.** Plan = Ensilage + Riv = Ja: Slå → Rives → Samles op.
|
||||
|
||||
## 3. Høst (korn, raps, majs …)
|
||||
|
||||
- [ ] **3.1 Høst.** En moden kornmark, Manuel, høster med passende skærebord + én aflæsningsvogn
|
||||
(traktor + tipvogn/overlæssevogn) med flueben → **Start nu**.
|
||||
- Forvent: høster → F-markør → Courseplay høster. Aflæsningsvognen sendes ud når høsteren
|
||||
arbejder, følger den (Courseplays combine-unloader) og kører fulde læs til siloen.
|
||||
- Log: `harvester … sendt`, `Courseplay startet (harvester)`, `unloader … sendt`,
|
||||
`aflæsningsvogn … følger høsteren -> <silo>`.
|
||||
- [ ] **3.2 Afslutning.** Høsten færdig → `harvesting -> <næste trin eller done>`,
|
||||
`aflæsningsvogn … stoppes — høsten er færdig`, evt. `kører rest-læs … til lager`, parkering.
|
||||
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
|
||||
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
|
||||
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
|
||||
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
|
||||
Kørende maskiner fortsætter.
|
||||
|
||||
## 4. Roer
|
||||
|
||||
- [ ] **4.1 Roehøst.** Roemark (sukkerroer), roeoptager (Ropa) + aflæsningsvogn → som 3.1, men med
|
||||
roer. Log viser `harvester 'Ropa …'`. Roerne går til nærmeste silo/lager der tager SUGARBEET
|
||||
(brug **Tving lager** hvis de skal et bestemt sted hen, fx roeknuseren).
|
||||
|
||||
## 5. Efter høst: klargøring og såning
|
||||
|
||||
Sæt på en kornmark før høst: Efter høst → Stubbehakning Nej, Jordbehandling **Auto**, **Så bagefter** Ja,
|
||||
Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
|
||||
- [ ] **5.1 Kæde.** Efter høsten fortsætter marken selv: (kalk hvis jorden trænger) → pløj eller
|
||||
kultivér → så → tromle → **Færdig**. Log: `harvesting -> cultivating`, `cultivating -> sowing` …
|
||||
- [ ] **5.2 Auto-jordbehandling.** Står "Jorden trænger til: pløjning" under marken → plov sendes.
|
||||
Ellers kultivator.
|
||||
- [ ] **5.3 Såning.** Såmaskinen sættes til afgrøden (`setSeedIndex`), og den rigtige afgrøde spirer.
|
||||
- [ ] **5.4 Ude af såperioden.** Afgrøde der ikke kan sås nu → status "<afgrøde> kan ikke sås i denne
|
||||
periode", ingen udsendelse.
|
||||
- [ ] **5.5 Tom for frø / påfyldning.** Såmaskine med lidt frø. Moden leder efter en læssestation med frø
|
||||
på lager og en AutoDrive-markør ved triggeren (fx en silo/big-bag-station med SEEDS).
|
||||
- Findes den: log `… fyldes op med SEEDS ved <markør> af AutoDrive når den er tom`. Når den er tom
|
||||
midt på marken, kører AutoDrive derhen, fylder op, kører tilbage, og Courseplay fortsætter.
|
||||
- Findes den ikke: log `ingen påfyldning af SEEDS med AutoDrive-markør fundet …`. Når den er tom:
|
||||
rød besked `'…' er tom for SEEDS — fyld op, så fortsætter marken`, og marken går IKKE videre.
|
||||
- [ ] **5.6 Bar mark alene.** Vælg en bar mark (stub/pløjet) → **Start nu** → kun klargøring/såning.
|
||||
|
||||
## 6. Pleje mens afgrøden vokser
|
||||
|
||||
- [ ] **6.1 Behov vises.** En voksende mark viser "Jorden trænger til: gødning, ukrudt …" og status
|
||||
"— trænger til Gødes". Manuel: intet sker før **Start nu**.
|
||||
- [ ] **6.2 Gødning.** Start nu (eller Auto) → gødningsspreder/-sprøjte med fyld sendes → `pleje:
|
||||
fertilizing` → `fertilizing -> idle`. Højst 2 gange pr. vækstcyklus.
|
||||
- [ ] **6.3 Ukrudt.** Høstafgrøde med tæt ukrudt → strigle (småt ukrudt) eller ukrudtssprøjte
|
||||
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
|
||||
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
|
||||
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
|
||||
|
||||
## 7. Multi-maskine (Courseplay multi-tool)
|
||||
|
||||
- [ ] **7.1 To slåmaskiner.** "Maskiner pr. trin" = 2 på en græsmark med to BiG M med flueben.
|
||||
Forvent: begge sendes samtidig, den ene laver kurset (`bane -1`), den anden kopierer det (`bane 1`)
|
||||
og de kører i konvoj. Log: `… sendt … bane -1`, `… bane 1`, `Courseplay startet (mower, bane -1)`,
|
||||
`Courseplay startet (mower, multi-kursus kopieret, bane 1)`.
|
||||
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
|
||||
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
|
||||
|
||||
## 8. Robusthed
|
||||
|
||||
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
|
||||
samme trin, sæt sendes ud igen, slåkurset hentes fra slåmaskinen.
|
||||
- [ ] **8.2 Stop mark.** **Stop mark** midt i høst → alle maskiner på marken stopper; høst står **Klar**
|
||||
igen, pleje/klargøring står **Venter**.
|
||||
- [ ] **8.3 Ingen vej.** Mark hvis F-markør ikke er forbundet → status "… nåede ikke F…" / "intet lager:
|
||||
… AutoDrive finder ingen rute …" og nyt forsøg efter 5 min.
|
||||
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
|
||||
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
|
||||
|
||||
---
|
||||
|
||||
## Kendte usikkerheder (første gang i spillet)
|
||||
|
||||
| Område | Hvad der er gættet | Tegn på fejl |
|
||||
|---|---|---|
|
||||
| Vækstfase | `getFieldDataAtWorldPosition` anden returværdi = vækststadie | Høstet stub vises som voksende, eller moden mark som bar |
|
||||
| Høsterens afgrøder | `spec_cutter.fruitTypes`/`fruitTypeIndices` | Høster vises med "0 afgrøder" |
|
||||
| Combine-unloader | stationsvalg ud fra silovalget | `ingen aflæsningsstation Courseplay kan bruge` |
|
||||
| Multi-tool | `multiTools` + `laneOffset` + `Course:setPosition` | Begge maskiner kører samme bane |
|
||||
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
|
||||
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
|
||||
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
|
||||
+892
-408
File diff suppressed because it is too large
Load Diff
@@ -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,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,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,238 +0,0 @@
|
||||
-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden)
|
||||
-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket,
|
||||
-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache.
|
||||
|
||||
local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7
|
||||
local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}}
|
||||
g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end}
|
||||
Logging = {info = function() end, warning = function() end}
|
||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
||||
SellingStation = {}
|
||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
||||
dofile("FS25_ADSmartPickup/adBuildings.lua")
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
|
||||
end
|
||||
|
||||
local markers, wayPoints = {}, {}
|
||||
local function addMarker(id, name, group, x, z)
|
||||
wayPoints[id] = {id = id, x = x, z = z}
|
||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
||||
end
|
||||
local loadStations, unloadStations = {}, {}
|
||||
local adEnv = {ADGraphManager = {
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getMapMarkers = function() return markers end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
}}
|
||||
local strictLoadAt = {}
|
||||
ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end}
|
||||
g_currentMission = {
|
||||
placeableSystem = {placeables = {}},
|
||||
storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end},
|
||||
accessHandler = {canFarmAccess = function() return true end},
|
||||
getFarmId = function() return 1 end,
|
||||
}
|
||||
local function placeable(uid, name, x, z, extra)
|
||||
local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end}
|
||||
for k, v in pairs(extra or {}) do p[k] = v end
|
||||
table.insert(g_currentMission.placeableSystem.placeables, p)
|
||||
return p
|
||||
end
|
||||
|
||||
-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted
|
||||
local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end})
|
||||
-- som FS25_LargerAnimalBarns' cowBarnBig: ÉN læssestation med TO læssepunkter (gylle + mælk)
|
||||
local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}, {triggerNode = {x = 0, z = 30}}}}
|
||||
local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}}
|
||||
table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload)
|
||||
local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}})
|
||||
local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true}
|
||||
table.insert(unloadStations, millUnload)
|
||||
local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}})
|
||||
local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}})
|
||||
local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}}
|
||||
local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}}
|
||||
table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station)
|
||||
table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}})
|
||||
table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}})
|
||||
local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}})
|
||||
local sellPoint = placeable("sell", "Mejeri", 3000, 0, {})
|
||||
local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true}
|
||||
table.insert(unloadStations, sellStation)
|
||||
local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end})
|
||||
|
||||
addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren
|
||||
addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren
|
||||
addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren)
|
||||
addMarker(11, "Cow 1 Slurry", "Slurry", 0, 33) -- 3 m fra gylletriggeren
|
||||
addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren
|
||||
addMarker(5, "sugar offload", "production", 470, 0)
|
||||
addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1
|
||||
addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m)
|
||||
strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station
|
||||
addMarker(8, "Storage pickup", "Pickup", 2010, 0)
|
||||
addMarker(9, "Mejeri", "selling", 3000, 20)
|
||||
addMarker(10, "Langt væk", "All", 9000, 9000)
|
||||
|
||||
-- B1: liste, kind og markører
|
||||
local list = ADBuildings.list(adEnv, 1)
|
||||
local byId = {}
|
||||
for _, b in ipairs(list) do byId[b.id] = b end
|
||||
check("B1 antal (naboens silo udeladt)", #list, 6)
|
||||
check("B1 stald", byId.barn1.kind, "husbandry")
|
||||
check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1)
|
||||
-- én markør pr. læssestation (mælketank OG gyllebeholder), sorteret på markørnavn
|
||||
check("B1 stald har to læssemarkører", #byId.barn1.loadMarkers, 2)
|
||||
check("B1 første = cow 1 milk (navnesorteret, uanset store/små)", byId.barn1.loadMarkers[1].id, 1)
|
||||
check("B1 anden = Cow 1 Slurry", byId.barn1.loadMarkers[2].name, "Cow 1 Slurry")
|
||||
check("B1 gyllemarkør -> stald via byMarker", ADBuildings.byMarker(adEnv, 1, 11, "load").id, "barn1")
|
||||
check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2)
|
||||
check("B1 stald afstand", byId.barn1.loadMarker.distance, 5)
|
||||
check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4)
|
||||
check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5)
|
||||
check("B1 fabrik er ikke salgssted", byId.mill.canSell, false)
|
||||
check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8)
|
||||
check("B1 salgssted", byId.sell.kind, "sell")
|
||||
check("B1 salgssted markør", byId.sell.unloadMarker.id, 9)
|
||||
check("B1 salgssted canSell", byId.sell.canSell, true)
|
||||
check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true)
|
||||
check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true)
|
||||
|
||||
-- B2: siloer i række: strengt opslag giver hver sin markør
|
||||
check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6)
|
||||
check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7)
|
||||
strictLoadAt[7] = nil
|
||||
ADBuildings.invalidate()
|
||||
check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil)
|
||||
strictLoadAt[7] = silo2Station
|
||||
ADBuildings.invalidate()
|
||||
|
||||
-- B3: byMarker (omvendt opslag)
|
||||
check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1")
|
||||
check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1")
|
||||
check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil)
|
||||
check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil)
|
||||
|
||||
-- B4: id-fallback uden uniqueId
|
||||
check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3")
|
||||
check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1")
|
||||
|
||||
-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate
|
||||
local first = ADBuildings.list(adEnv, 1)
|
||||
check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true)
|
||||
ADBuildings.invalidate()
|
||||
check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true)
|
||||
|
||||
-- B6: sortering efter NAVN (naturlig talrækkefølge), dernæst første markørnavn — ikke efter slags
|
||||
local order = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.name) end
|
||||
check("B6 rækkefølge", table.concat(order, ","), "Kostald 1,Mejeri,Palleskur,Silo 1,Silo 2,Sukkerfabrik")
|
||||
check("B6 naturlig sortering", ADBuildings.naturalLess("Silo 9", "Silo 10"), true)
|
||||
check("B6 naturlig sortering, tal før tekst-lighed", ADBuildings.naturalLess("25 Silo Storage", "26 Silo Storage"), true)
|
||||
check("B6 naturlig sortering ens navn -> markørnavn", ADBuildings.compare({name = "Farma", loadMarkers = {{name = "30 Silo"}}}, {name = "Farma", loadMarkers = {{name = "4 Silo"}}}), false)
|
||||
|
||||
-- B-DOB: silo der kun kan FYLDES (drive-on-bunker ved biogas: aflæsning, ingen læssestation) = mål, ikke kilde
|
||||
do
|
||||
local dob = placeable("dob1", "DOB 115", 5000, 0, {spec_silo = {}})
|
||||
table.insert(unloadStations, {owningPlaceable = dob, unloadTriggers = {{exactFillRootNode = {x = 5000, z = 5}}}})
|
||||
addMarker(21, "Biogas Drop 1", "Biogas", 5000, 12)
|
||||
ADBuildings.invalidate()
|
||||
local byIdDob = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byIdDob[b.id] = b end
|
||||
check("B-DOB kan være mål", byIdDob.dob1.canBeTarget, true)
|
||||
check("B-DOB ikke kilde (intet at hente)", byIdDob.dob1.canBeSource, false)
|
||||
check("B-DOB aflæsningsmarkør", byIdDob.dob1.unloadMarker.id, 21)
|
||||
check("B-DOB almindelig silo stadig ikke mål", byIdDob.silo1.canBeTarget, false)
|
||||
end
|
||||
|
||||
-- B-SEP: bygning med læssestation men uden silo/fabrik/stald-spec (Pumps&Hoses separatorhal: digestat-udlevering)
|
||||
do
|
||||
local sep = placeable("sep1", "Separator Hall", 7000, 0, {spec_manureSeparator = {}})
|
||||
table.insert(loadStations, {owningPlaceable = sep, loadTriggers = {{triggerNode = {x = 7000, z = 5}}}})
|
||||
addMarker(41, "Digestate pickup", "Biogas", 7000, 10)
|
||||
ADBuildings.invalidate()
|
||||
local bySep = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do bySep[b.id] = b end
|
||||
check("B-SEP findes som lager", bySep.sep1 and bySep.sep1.kind, "storage")
|
||||
check("B-SEP kan være kilde", bySep.sep1 and bySep.sep1.canBeSource, true)
|
||||
check("B-SEP læssemarkør", bySep.sep1 and bySep.sep1.loadMarker and bySep.sep1.loadMarker.id, 41)
|
||||
end
|
||||
|
||||
-- B-DROP: fabrik med pickup-markør TÆT på aflæsningstriggeren og en separat drop-markør længere væk
|
||||
-- (Sugarbeet Crusher): aflæsningsmarkøren skal være drop, ikke bygningens egen læssemarkør
|
||||
do
|
||||
local crusher = placeable("crusher1", "Sugarbeet Crusher", 8000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
||||
table.insert(loadStations, {owningPlaceable = crusher, loadTriggers = {{triggerNode = {x = 8000, z = 5}}}})
|
||||
table.insert(unloadStations, {owningPlaceable = crusher, unloadTriggers = {{exactFillRootNode = {x = 8003, z = 2}}}})
|
||||
addMarker(51, "Sugar Beet Cut Pickup", "Fabrication", 8000, 8) -- 3 m fra læs, ~6 m fra aflæsning
|
||||
addMarker(52, "Sugar Beet Cut Beet Drop", "Fabrication", 8012, 2) -- 9 m fra aflæsning
|
||||
ADBuildings.invalidate()
|
||||
local byC = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
||||
check("B-DROP læssemarkør = pickup", byC.crusher1.loadMarker.id, 51)
|
||||
check("B-DROP aflæsningsmarkør = drop (ikke pickup)", byC.crusher1.unloadMarker.id, 52)
|
||||
-- kun én fælles markør (AIO Load / Unload): bruges til begge
|
||||
local aio = placeable("aio1", "AIO", 9000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
||||
table.insert(loadStations, {owningPlaceable = aio, loadTriggers = {{triggerNode = {x = 9000, z = 5}}}})
|
||||
table.insert(unloadStations, {owningPlaceable = aio, unloadTriggers = {{exactFillRootNode = {x = 9002, z = 5}}}})
|
||||
addMarker(53, "AIO Load / Unload", "Fabrication", 9000, 9)
|
||||
ADBuildings.invalidate()
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
||||
check("B-DROP fælles markør bruges til aflæsning", byC.aio1.unloadMarker.id, 53)
|
||||
check("B-DROP silo beholder sin egen markør (ikke nabosiloens)", byC.silo1.unloadMarker.id, 6)
|
||||
end
|
||||
|
||||
-- B-COOP: hønsehus — foder aflæsses i den ene ende, æg-paller spawnes i den anden (ingen læssestation)
|
||||
do
|
||||
local coop = placeable("coop1", "Chicken Coop", 10000, 0, {getHusbandryFillLevel = function() return 0 end,
|
||||
spec_husbandryFood = {triggers = {{someTrigger = {exactFillRootNode = {x = 9970, z = 0}}}}}, -- ukendt feltsti: søges
|
||||
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[99] = {spawnPlaces = {{startNode = {x = 10030, z = 0}}}}}}})
|
||||
table.insert(unloadStations, {owningPlaceable = coop, unloadTriggers = {}}) -- som chickenBarnBig: tom station
|
||||
addMarker(61, "Chicken Food", "Chicken", 9965, 0)
|
||||
addMarker(62, "Egg Pickup", "Chicken", 10035, 0)
|
||||
ADBuildings.invalidate()
|
||||
local byCoop = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byCoop[b.id] = b end
|
||||
check("B-COOP læssemarkør ved æggene", byCoop.coop1.loadMarker and byCoop.coop1.loadMarker.id, 62)
|
||||
check("B-COOP aflæsningsmarkør ved foderet", byCoop.coop1.unloadMarker and byCoop.coop1.unloadMarker.id, 61)
|
||||
end
|
||||
|
||||
-- B-FUEL: brændstoftank (læssestation kun med DIESEL) er ikke et lager/en kilde
|
||||
do
|
||||
local fmBefore = g_fillTypeManager
|
||||
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return index == 900 and "DIESEL" or "X" end}
|
||||
local tank = placeable("fuel1", "Small Petrol Tank", 11000, 0, {})
|
||||
table.insert(loadStations, {owningPlaceable = tank, supportedFillTypes = {[900] = true}, loadTriggers = {{triggerNode = {x = 11000, z = 3}}}})
|
||||
ADBuildings.invalidate()
|
||||
local byFuel = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byFuel[b.id] = b end
|
||||
check("B-FUEL brændstoftank udeladt", byFuel.fuel1, nil)
|
||||
g_fillTypeManager = fmBefore
|
||||
end
|
||||
|
||||
-- B-SCAN: søgning efter trigger-noder i indlejrede tabeller (dybde ≤ 3, ingen løkker)
|
||||
do
|
||||
local loop = {}
|
||||
loop.self = loop
|
||||
local nodes = ADBuildings.findTriggerNodes({a = {b = {exactFillRootNode = "n1"}}, c = {triggerNode = "n2"}, d = loop, e = {f = {g = {h = {exactFillRootNode = "for-dybt"}}}}})
|
||||
table.sort(nodes)
|
||||
check("B-SCAN finder n1 og n2, ikke for dybt", table.concat(nodes, ","), "n1,n2")
|
||||
end
|
||||
|
||||
-- B-SHED: palleskur læsser og aflæsser samme sted — aflæsningsmarkøren er den nærmeste (ikke "den anden")
|
||||
do
|
||||
local shedX = placeable("shedX", "Bale & Pallet Storage", 12000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 4000}})
|
||||
addMarker(71, "Chicken Storage Unload", "Chicken", 12010, 0) -- 10 m
|
||||
addMarker(72, "Chicken Storage Pickup", "Chicken", 12018, 0) -- 18 m
|
||||
ADBuildings.invalidate()
|
||||
local byShed = {}
|
||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byShed[b.id] = b end
|
||||
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,90 +0,0 @@
|
||||
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
|
||||
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
|
||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local function withMarkers(width)
|
||||
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
|
||||
end
|
||||
|
||||
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
|
||||
local wagon = withMarkers(2.4)
|
||||
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
|
||||
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
|
||||
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
|
||||
-- W2: ugyldig variabel bredde -> AI-markører
|
||||
local plain = withMarkers(9.5)
|
||||
plain.getVariableWorkWidth = function() return 0, nil, false end
|
||||
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
|
||||
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
|
||||
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
|
||||
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
|
||||
|
||||
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
|
||||
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
|
||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
|
||||
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
|
||||
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
|
||||
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
|
||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
|
||||
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
|
||||
|
||||
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
|
||||
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
|
||||
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
|
||||
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
|
||||
local function rake(dropIndex, runtime)
|
||||
local areas = {
|
||||
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
|
||||
{start = 10, width = 11, height = 12},
|
||||
{start = 20, width = 21, height = 22},
|
||||
}
|
||||
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
|
||||
end
|
||||
local sideRake = rake(2, true)
|
||||
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
|
||||
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
|
||||
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
|
||||
local centerRake = rake(3, true)
|
||||
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
|
||||
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
|
||||
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
|
||||
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
|
||||
local xmlRake = rake(2, false)
|
||||
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
|
||||
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
|
||||
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
|
||||
|
||||
-- SR: sprøjte/spreder -> rolle ud fra fyldet
|
||||
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
|
||||
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
|
||||
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
|
||||
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
|
||||
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
|
||||
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
|
||||
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
|
||||
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
|
||||
|
||||
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
|
||||
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
|
||||
check("HF liste", list[3] and list[5], true)
|
||||
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
|
||||
check("HF opslag", lookup[3], true)
|
||||
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
|
||||
check("HF finsnitter tager intet", next(forage), nil)
|
||||
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
|
||||
|
||||
-- PL: plantbar i perioden
|
||||
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
|
||||
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,86 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFieldFleet.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
||||
local F = ADFieldFleet
|
||||
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
|
||||
|
||||
-- FL1: rolle ud fra redskabets spec (læssevogn vinder over andet på samme køretøj)
|
||||
check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
|
||||
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
|
||||
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
|
||||
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
|
||||
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
|
||||
|
||||
local function rig(id, role, x, extra)
|
||||
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
|
||||
for k, v in pairs(extra or {}) do r[k] = v end
|
||||
return r
|
||||
end
|
||||
|
||||
-- FL2: nærmeste ledige med flueben og rigtig rolle
|
||||
local picked = F.pick({rig("far", "mower", 500), rig("near", "mower", 50), rig("wagon", "collector", 10)}, "mower", 0, 0, nil)
|
||||
check("FL2 nærmeste slåmaskine", picked.id, "near")
|
||||
|
||||
-- FL3: uden flueben / optaget / spiller i sædet springes over
|
||||
local _, why = F.pick({rig("a", "mower", 1, {enabled = false})}, "mower", 0, 0)
|
||||
check("FL3 intet med flueben", why, "noRig")
|
||||
local _, whyBusy = F.pick({rig("a", "mower", 1, {busy = true}), rig("b", "mower", 2, {controlled = true})}, "mower", 0, 0)
|
||||
check("FL3 alle optaget", whyBusy, "allBusy")
|
||||
check("FL3 ingen med rollen", select(2, F.pick({rig("a", "tedder", 1)}, "mower", 0, 0)), "noRig")
|
||||
|
||||
-- FL4: læssevogn smallere end slåbredden springes over, bredere vælges
|
||||
local wide = F.pick({rig("smal", "collector", 1, {width = 20}), rig("bred", "collector", 900, {width = 40.8})}, "collector", 0, 0, 40.8)
|
||||
check("FL4 bred valgt selv om længere væk", wide.id, "bred")
|
||||
-- FL5: kun for smalle -> grund tooNarrow med tal
|
||||
local none, whyNarrow, args = F.pick({rig("smal", "collector", 1, {width = 20})}, "collector", 0, 0, 40.8)
|
||||
check("FL5 ingen", none, nil); check("FL5 grund", whyNarrow, "tooNarrow"); check("FL5 bredde", args[1], 20)
|
||||
-- FL6: over 50 m -> tooWide
|
||||
check("FL6 for bred", select(2, F.pick({rig("x", "mower", 1, {width = 60})}, "mower", 0, 0)), "tooWide")
|
||||
|
||||
-- FL7: toggle giver ny tabel
|
||||
local enabled = {a = true}
|
||||
local toggled = F.toggle(enabled, "b")
|
||||
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
|
||||
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
|
||||
|
||||
-- FL8: nye roller i prioriteret rækkefølge
|
||||
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
|
||||
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
|
||||
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
|
||||
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
|
||||
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
|
||||
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
|
||||
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
|
||||
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
|
||||
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
|
||||
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
|
||||
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
|
||||
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
|
||||
|
||||
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
|
||||
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
|
||||
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
|
||||
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
||||
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
|
||||
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
||||
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
|
||||
|
||||
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
|
||||
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
|
||||
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
|
||||
check("FL10 to valgt", #two, 2)
|
||||
check("FL10 nærmeste først", two[1].id, "m2")
|
||||
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
|
||||
check("FL10 for få -> ingen", none, nil)
|
||||
check("FL10 grund", whyMany, "notEnough")
|
||||
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
|
||||
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,93 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldJobs.lua")
|
||||
local J = ADFieldJobs
|
||||
local M = ADFieldModel
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then print("OK " .. name) else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
|
||||
|
||||
-- J1: klar auto-mark med markør -> start; uden markør -> vent
|
||||
local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
|
||||
check("J1 start", J.decide(auto, info, {}, 0).action, "start")
|
||||
check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
|
||||
|
||||
-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
|
||||
local mowing = M.start(auto)
|
||||
local wanted = J.decide(mowing, info, {}, 0)
|
||||
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
|
||||
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
|
||||
local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
|
||||
check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
|
||||
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
|
||||
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
|
||||
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
|
||||
check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
|
||||
-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
|
||||
local collecting = M.advance(mowing)
|
||||
check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
|
||||
check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
|
||||
|
||||
-- J6: Courseplay efterlader et hjørne -> efter 2 færdige slå-ture går marken videre alligevel
|
||||
local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
|
||||
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
|
||||
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
|
||||
|
||||
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
|
||||
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
|
||||
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
||||
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
||||
|
||||
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
|
||||
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
|
||||
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
|
||||
local d = J.decide(harvesting, harvestInfo, {}, 0)
|
||||
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
|
||||
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
|
||||
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
||||
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
||||
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
||||
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
||||
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
||||
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
||||
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
|
||||
local cultivating = M.advance(harvesting, {})
|
||||
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
|
||||
|
||||
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
|
||||
local multiMow = M.with(mowing, {plan = {rigs = 3}})
|
||||
local dMulti = J.decide(multiMow, info, {}, 0)
|
||||
check("J9 tre slåmaskiner", dMulti.count, 3)
|
||||
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
|
||||
|
||||
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
|
||||
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
|
||||
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
|
||||
local care = J.decide(autoField, growingInfo, {}, 0)
|
||||
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
|
||||
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
|
||||
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
|
||||
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
||||
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
||||
|
||||
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
|
||||
local rain = {raining = true}
|
||||
local dRain = J.decide(mowing, info, {}, 0, rain)
|
||||
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
|
||||
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
|
||||
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
|
||||
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
|
||||
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
|
||||
local silageCollect = M.advance(mowing)
|
||||
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
|
||||
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
||||
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
||||
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,238 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFieldModel.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
local M = ADFieldModel
|
||||
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
|
||||
|
||||
-- M1: ny mark = venter, manuel ensilage uden riv
|
||||
local f = M.newField(109)
|
||||
check("M1 stage", f.stage, "idle")
|
||||
check("M1 mode", f.plan.mode, "manual")
|
||||
check("M1 product auto", f.plan.product, "auto")
|
||||
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
|
||||
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
|
||||
check("M1 rake", f.plan.rake, false)
|
||||
|
||||
-- M2: klar ved >= 90 %
|
||||
check("M2 89 % ikke klar", M.onReadiness(f, 0.89).stage, "idle")
|
||||
local ready = M.onReadiness(f, 0.9)
|
||||
check("M2 90 % klar", ready.stage, "ready")
|
||||
check("M2 immutabel", f.stage, "idle")
|
||||
|
||||
-- M3: klar mark slået af spilleren -> tilbage til venter
|
||||
check("M3 slået manuelt", M.onReadiness(ready, 0.3).stage, "idle")
|
||||
check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "ready")
|
||||
|
||||
-- M4: trin pr. plan
|
||||
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
|
||||
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
|
||||
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
|
||||
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
|
||||
|
||||
-- M5: start: auto starter selv, manuel kun på forespørgsel
|
||||
check("M5 manuel starter ikke", M.shouldStart(ready), false)
|
||||
local auto = M.onReadiness(M.newField(5), 1)
|
||||
auto.plan.mode = "auto"
|
||||
check("M5 auto starter", M.shouldStart(auto), true)
|
||||
local requested = M.newField(6); requested.stage = "ready"; requested.startRequested = true
|
||||
check("M5 Start nu", M.shouldStart(requested), true)
|
||||
local started = M.start(ready)
|
||||
check("M5 start -> slås", started.stage, "mowing")
|
||||
check("M5 start rydder forespørgsel", started.startRequested, false)
|
||||
local oldMow = M.with(ready, {mowVehicleId = "old"})
|
||||
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
|
||||
|
||||
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
|
||||
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
|
||||
hay = M.advance(hay); check("M6 hø -> vendes", hay.stage, "tedding")
|
||||
hay = M.advance(hay); check("M6 -> samles op", hay.stage, "collecting")
|
||||
hay = M.advance(hay); check("M6 -> færdig", hay.stage, "done")
|
||||
check("M6 færdig forbliver ved lav andel", M.onReadiness(hay, 0.1).stage, "done")
|
||||
check("M6 færdig -> klar når vokset", M.onReadiness(hay, 0.95).stage, "ready")
|
||||
check("M6 arbejdende mark ignorerer andel", M.onReadiness(started, 0).stage, "mowing")
|
||||
check("M6 isWorking", M.isWorking(started), true)
|
||||
check("M6 stop -> klar", M.stop(started).stage, "ready")
|
||||
|
||||
-- M7: roller, maks sæt, bredde
|
||||
check("M7 rolle slå", M.roleFor("mowing"), "mower")
|
||||
check("M7 rolle saml", M.roleFor("collecting"), "collector")
|
||||
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
|
||||
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
|
||||
check("M7 maks slå", M.maxRigs("mowing"), 1)
|
||||
local okWide, whyWide = M.checkWidth("mower", 60, nil)
|
||||
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
|
||||
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
|
||||
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
|
||||
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
|
||||
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
|
||||
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
|
||||
|
||||
-- wantsRig: kun arbejdende trin, under maks, efter pause
|
||||
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
|
||||
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
|
||||
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
|
||||
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
|
||||
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
|
||||
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
|
||||
|
||||
-- M8: gem/indlæs bevarer trin, plan og slåbredde
|
||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
|
||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}}
|
||||
local back = M.fromRecord(M.toRecord(saved))
|
||||
check("M8 id", back.id, 109)
|
||||
check("M8 stage", back.stage, saved.stage)
|
||||
check("M8 mode", back.plan.mode, "auto")
|
||||
check("M8 product", back.plan.product, "hay")
|
||||
check("M8 rake", back.plan.rake, true)
|
||||
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
|
||||
check("M8 slåbredde", back.mowWidth, 40.8)
|
||||
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
|
||||
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
|
||||
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
|
||||
|
||||
-- M9: with() kopierer og fletter planen
|
||||
local base = M.start(M.onReadiness(M.newField(1), 1))
|
||||
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
|
||||
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
|
||||
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
|
||||
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
|
||||
|
||||
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
|
||||
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
|
||||
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
|
||||
|
||||
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
|
||||
local cut = M.with(M.newField(3), {mowWidth = 40.8})
|
||||
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
|
||||
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
|
||||
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
|
||||
local raked = M.with(cut, {plan = {rake = true}})
|
||||
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
|
||||
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
|
||||
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
|
||||
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
|
||||
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
|
||||
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
|
||||
-- gem/indlæs rivemaskine
|
||||
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
|
||||
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
|
||||
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
|
||||
|
||||
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
|
||||
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
|
||||
check("M12 midterrive", M.collectorOffset(0, 0), 0)
|
||||
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
|
||||
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
|
||||
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
|
||||
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
|
||||
local side = M.with(M.newField(11), {windrowOffset = 3.2})
|
||||
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
|
||||
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
|
||||
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
|
||||
|
||||
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
|
||||
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
|
||||
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
|
||||
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
|
||||
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
|
||||
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
|
||||
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
|
||||
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
|
||||
|
||||
-- M14: høstkæde + klargøring efter behov
|
||||
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
|
||||
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
|
||||
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
|
||||
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
|
||||
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
|
||||
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
|
||||
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
|
||||
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
|
||||
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
|
||||
check("M14 start = høst", harvestField.stage, "harvesting")
|
||||
check("M14 husker art", harvestField.kind, "harvest")
|
||||
local afterHarvest = M.advance(harvestField, {})
|
||||
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
|
||||
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
|
||||
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
|
||||
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
|
||||
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
|
||||
|
||||
-- M15: hjælpere under høst og multi-tool-baner
|
||||
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
|
||||
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
|
||||
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
|
||||
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
|
||||
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
|
||||
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
|
||||
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
|
||||
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
|
||||
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
|
||||
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
|
||||
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
|
||||
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
|
||||
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
|
||||
|
||||
-- M16: pleje under vækst
|
||||
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
|
||||
local idleField = M.newField(30)
|
||||
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
|
||||
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
|
||||
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
|
||||
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
|
||||
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
|
||||
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
|
||||
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
|
||||
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
|
||||
local cared = M.startCare(idleField, "fertilizing")
|
||||
check("M16 pleje-trin", cared.stage, "fertilizing")
|
||||
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
|
||||
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
|
||||
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
|
||||
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
|
||||
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
|
||||
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
|
||||
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
|
||||
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
|
||||
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
|
||||
|
||||
-- M17: såafgrøde og gem/indlæs af den udvidede plan
|
||||
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
|
||||
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
|
||||
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
|
||||
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
|
||||
local round = M.fromRecord(M.toRecord(full))
|
||||
check("M17 trin", round.stage, "weeding")
|
||||
check("M17 sæt", round.plan.rigs, 3)
|
||||
check("M17 vogne", round.plan.unloaders, 2)
|
||||
check("M17 stub", round.plan.mulch, true)
|
||||
check("M17 kalk fra", round.plan.lime, false)
|
||||
check("M17 jord", round.plan.soil, "plow")
|
||||
check("M17 så", round.plan.sow, true)
|
||||
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
|
||||
check("M17 tromle", round.plan.roll, true)
|
||||
check("M17 gødning fra", round.plan.fertilize, false)
|
||||
check("M17 ukrudt fra", round.plan.weed, false)
|
||||
check("M17 art", round.kind, "harvest")
|
||||
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
|
||||
check("M17 pleje-tæller", round.careRuns.weeding, 1)
|
||||
check("M17 produkt", round.plan.product, "harvest")
|
||||
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
|
||||
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
|
||||
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
||||
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
||||
|
||||
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
|
||||
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
|
||||
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
|
||||
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
|
||||
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
||||
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,42 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
|
||||
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
|
||||
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
|
||||
|
||||
-- falsk XMLFile: gemmer værdier pr. nøgle
|
||||
local function fakeXml()
|
||||
local values = {}
|
||||
local xml = {}
|
||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
||||
xml["set" .. kind] = function(_, key, value) values[key] = value end
|
||||
xml["get" .. kind] = function(_, key) return values[key] end
|
||||
end
|
||||
return xml
|
||||
end
|
||||
|
||||
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
|
||||
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
|
||||
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
|
||||
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
|
||||
})
|
||||
local record = ADFieldModel.toRecord(field)
|
||||
for key in pairs(record) do
|
||||
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
|
||||
end
|
||||
local xml = fakeXml()
|
||||
ADFieldStorage.writeRecord(xml, "k", record)
|
||||
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
|
||||
local again = ADFieldModel.toRecord(back)
|
||||
for key, value in pairs(record) do
|
||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,42 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFieldView.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
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
|
||||
|
||||
local infos = {
|
||||
{id = 109, mowable = true, markerId = 5},
|
||||
{id = 12, mowable = true, markerId = nil},
|
||||
{id = 90, mowable = false, markerId = 7},
|
||||
}
|
||||
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
|
||||
|
||||
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
|
||||
local rows = V.rows(infos, fields)
|
||||
check("V1 tre rækker", #rows, 3)
|
||||
check("V1 sorteret", rows[1].info.id, 12)
|
||||
check("V1 model med", rows[3].field.id, 109)
|
||||
-- V2: mangler model -> ny standardmark
|
||||
check("V2 standardmodel", rows[2].field.stage, "idle")
|
||||
-- V3: status — manglende markør vinder over trin
|
||||
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
|
||||
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
|
||||
|
||||
-- V4: planens tekstnøgler pr. markart
|
||||
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
|
||||
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
|
||||
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
|
||||
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
|
||||
-- V5: jordtilstand som korte tekstdele
|
||||
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
|
||||
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
|
||||
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,65 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFields.lua (fra repo-roden)
|
||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
||||
local F = ADFields
|
||||
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
|
||||
|
||||
-- F1/F2: markørnavne
|
||||
check("F1 F109", F.parseMarkerName("F109"), 109)
|
||||
check("F1 småt", F.parseMarkerName("f7"), 7)
|
||||
check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12)
|
||||
check("F2 andet navn", F.parseMarkerName("Field 109"), nil)
|
||||
check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil)
|
||||
check("F2 nil", F.parseMarkerName(nil), nil)
|
||||
|
||||
-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket
|
||||
local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"}
|
||||
local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}}
|
||||
local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}}
|
||||
local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end)
|
||||
check("F3 græs", mowable[1] ~= nil, true)
|
||||
check("F3 græs skår", mowable[1].windrow, 10)
|
||||
check("F3 græs hø", mowable[1].hay, 11)
|
||||
check("F3 rug (halm) ikke", mowable[2], nil)
|
||||
check("F3 kløver", mowable[3].hay, 31)
|
||||
check("F3 bønnehalm -> halm ikke", mowable[4], nil)
|
||||
|
||||
-- F4: markens art og om den vokser
|
||||
local kind, growing = F.classify(3, false, 0.4)
|
||||
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
|
||||
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
|
||||
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
|
||||
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
|
||||
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
|
||||
|
||||
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
|
||||
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
|
||||
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
|
||||
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
|
||||
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
|
||||
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
|
||||
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
|
||||
|
||||
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
|
||||
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
|
||||
check("F6 intet", F.growthPhase(0, wheat), "none")
|
||||
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
|
||||
check("F6 klar", F.growthPhase(6, wheat), "ready")
|
||||
check("F6 visnet", F.growthPhase(8, wheat), "withered")
|
||||
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
|
||||
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
|
||||
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
|
||||
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
|
||||
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
|
||||
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
|
||||
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
|
||||
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
|
||||
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,69 +0,0 @@
|
||||
-- Kør: luajit tests/test_adFlowView.lua (fra repo-roden)
|
||||
-- Ren oprydning af Flows-fanen: tavlens rækkefølge, hvilke traktorer og bygninger der vises.
|
||||
|
||||
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
|
||||
local function ids(list, field)
|
||||
local out = {}
|
||||
for _, item in ipairs(list) do table.insert(out, tostring(item[field or "id"])) end
|
||||
return table.concat(out, ",")
|
||||
end
|
||||
|
||||
-- FV1: tavlen — forsyning (Ind) først, så udkørsel (Ud); hver gruppe sorteret efter navn (naturligt)
|
||||
local runs = {
|
||||
{id = 1, name = "Pig Food", kind = "supply"},
|
||||
{id = 2, name = "Milk Sale", kind = "outbound"},
|
||||
{id = 3, name = "Cow Food", kind = "supply"},
|
||||
{id = 4, name = "Biogas 10", kind = "supply"},
|
||||
{id = 5, name = "Biogas 9", kind = "supply"},
|
||||
{id = 6, name = "Digestate", kind = "outbound"},
|
||||
}
|
||||
local groups = V.boardGroups(runs)
|
||||
check("FV1 to grupper", #groups, 2)
|
||||
check("FV1 Ind først", groups[1].kind, "supply")
|
||||
check("FV1 Ind sorteret (Biogas 9 før 10)", ids(groups[1].runs), "5,4,3,1")
|
||||
check("FV1 Ud sorteret", ids(groups[2].runs), "6,2")
|
||||
check("FV1 samlet rækkefølge til vælgeren", ids(V.orderedRuns(runs)), "5,4,3,1,6,2")
|
||||
check("FV1 tom gruppe udelades", #V.boardGroups({{id = 1, name = "A", kind = "supply"}}), 1)
|
||||
|
||||
-- FV2: traktorer — egne først, så ledige brugbare; andre flows og låste kun med "vis alle"
|
||||
local vehicles = {
|
||||
{id = "a", label = "Zetor", state = "free"},
|
||||
{id = "b", label = "Fendt", state = "in"},
|
||||
{id = "c", label = "Case", state = "other"},
|
||||
{id = "d", label = "Claas", state = "blocked"},
|
||||
{id = "e", label = "Deutz", state = "free"},
|
||||
}
|
||||
check("FV2 standard: egne + ledige, sorteret", ids(V.vehicleRows(vehicles, false)), "b,e,a")
|
||||
check("FV2 vis alle: + andre flows + låste til sidst", ids(V.vehicleRows(vehicles, true)), "b,e,a,c,d")
|
||||
check("FV2 antal skjulte", V.hiddenCount(vehicles, V.vehicleRows(vehicles, false)), 2)
|
||||
|
||||
-- FV3: bygninger — valgte først; uden markør skjult medmindre valgt eller "vis alle"
|
||||
local buildings = {
|
||||
{id = "s2", name = "Silo 2", hasMarker = true},
|
||||
{id = "s10", name = "Silo 10", hasMarker = true},
|
||||
{id = "x", name = "Gammel lade", hasMarker = false},
|
||||
{id = "y", name = "Anden uden", hasMarker = false},
|
||||
{id = "a", name = "Anlæg", hasMarker = true},
|
||||
}
|
||||
local chosen = {s10 = true, y = true}
|
||||
check("FV3 standard: valgte først, så med markør", ids(V.buildingRows(buildings, chosen, false)), "y,s10,a,s2")
|
||||
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,s10,a,s2,x")
|
||||
|
||||
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
|
||||
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
|
||||
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
|
||||
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,73 +0,0 @@
|
||||
-- Kør: luajit tests/test_adLoadSwap.lua (fra repo-roden)
|
||||
-- Ren rangering af aflæsnings-siloer (lagermål for udkørsel og læs-bytte).
|
||||
|
||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
||||
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
|
||||
local L = ADLoadSwap
|
||||
|
||||
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
|
||||
|
||||
-- LS1: markører et andet forløb HENTER fra (fx "Milk Storage Load") vælges sidst; ellers har-varen, så afstand
|
||||
local candidates = {
|
||||
{marker = {id = 1, name = "Milk Storage Load"}, hasFillType = true, distance = 100},
|
||||
{marker = {id = 2, name = "Milk Storage unload"}, hasFillType = true, distance = 400},
|
||||
{marker = {id = 3, name = "Silo tom"}, hasFillType = false, distance = 10},
|
||||
}
|
||||
local ranked = L.rankDumpCandidates(candidates, {[1] = true})
|
||||
check("LS1 afhentningsmarkør undgås", ranked[1].marker.id, 2)
|
||||
check("LS1 derefter tom silo før afhentning", ranked[2].marker.id, 3)
|
||||
check("LS1 afhentning sidst (stadig mulig)", ranked[3].marker.id, 1)
|
||||
local plain = L.rankDumpCandidates(candidates, nil)
|
||||
check("LS1 uden undgå-liste: har varen + nærmest", plain[1].marker.id, 1)
|
||||
|
||||
-- LS2: lagermål — rene siloer altid; egne fabrikker der bruger varen som input kun når tilladt (udkørsel)
|
||||
local SLURRY, MILK = 20, 21
|
||||
g_currentMission = {getFarmId = function() return 1 end}
|
||||
local biogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 1 end}
|
||||
local foreignBiogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 0 end}
|
||||
local silo = {spec_silo = {}}
|
||||
local barn = {spec_silo = {}, getHusbandryFillLevel = function() return 0 end}
|
||||
check("LS2 silo", L.isStoreTarget({owningPlaceable = silo}, SLURRY, nil, false), true)
|
||||
check("LS2 stald aldrig", L.isStoreTarget({owningPlaceable = barn}, SLURRY, nil, true), false)
|
||||
check("LS2 biogas kun når fabrikker tilladt", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, false), false)
|
||||
check("LS2 biogas tager gylle", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, true), true)
|
||||
check("LS2 biogas tager ikke mælk", L.isStoreTarget({owningPlaceable = biogas}, MILK, nil, true), false)
|
||||
check("LS2 fremmed anlæg aldrig", L.isStoreTarget({owningPlaceable = foreignBiogas}, SLURRY, nil, true), false)
|
||||
check("LS2 hjemmet selv aldrig", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, {owningPlaceable = biogas}, true), false)
|
||||
g_currentMission = nil
|
||||
|
||||
-- LS3: tvunget lagermål — kun de valgte markører, i rækkefølge; første der tager varen, har plads og kan nås
|
||||
do
|
||||
local stations = {
|
||||
[101] = {accepts = {[SLURRY] = true}, free = 0}, -- BGA fuld
|
||||
[102] = {accepts = {[SLURRY] = true}, free = 900000}, -- gylletank
|
||||
[103] = {accepts = {[MILK] = true}, free = 900000}, -- tager ikke gylle
|
||||
}
|
||||
for _, st in pairs(stations) do
|
||||
st.getIsFillTypeSupported = function(self, ft) return self.accepts[ft] == true end
|
||||
st.getFreeCapacity = function(self) return self.free end
|
||||
end
|
||||
ADSmartPickup = {getFarmId = function() return 1 end, getUnloadStationAtWayPointId = function(_, id) return stations[id] end}
|
||||
local adEnv = {ADGraphManager = {getMapMarkerByWayPointId = function(_, id) return {id = id, name = "m" .. id} end}}
|
||||
local vehicle = {}
|
||||
local all = function() return true end
|
||||
check("LS3 fuld BGA springes over -> tank", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 102)
|
||||
stations[101].free = 500000
|
||||
check("LS3 BGA med plads vælges først", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 101)
|
||||
local none, why = L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {103}, all)
|
||||
check("LS3 intet valgt mål tager varen -> nil", none, nil)
|
||||
check("LS3 forklaring", why ~= nil and why:find("valgt", 1, true) ~= nil, true)
|
||||
check("LS3 ikke nåelig -> næste", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, function(id) return id ~= 101 end).id, 102)
|
||||
ADSmartPickup = nil
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,617 +0,0 @@
|
||||
-- Kør: luajit tests/test_adOutbound.lua (fra repo-roden)
|
||||
-- Udkørsels-hooks: turvalg + AD-tilstand, hold ved Wait, objektlager-udtag, opsamlingsrunde,
|
||||
-- advarsler på skærmen. Fake AD-miljø; adSources/adPrices/adLoadSwap er fakes, adWaitPool og
|
||||
-- adOutboundPlanner er de rigtige.
|
||||
|
||||
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
|
||||
local NAMES = {[SUGAR] = "SUGAR", [FORAGE] = "FORAGE", [BASKET] = "STRAW_BASKET", [CRATE] = "STRAW_CRATE"}
|
||||
g_fillTypeManager = {
|
||||
getFillTypeNameByIndex = function(_, i) return NAMES[i] end,
|
||||
getFillTypeByIndex = function(_, i) return {name = NAMES[i], palletFilename = (i == BASKET or i == CRATE) and "p.xml" or nil} end,
|
||||
}
|
||||
local logLines = {}
|
||||
Logging = {
|
||||
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
|
||||
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
|
||||
}
|
||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
||||
g_currentMission = {getFarmId = function() return 1 end}
|
||||
|
||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||
dofile("FS25_ADSmartPickup/adWaitPool.lua")
|
||||
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adOutbound.lua")
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then
|
||||
print("OK " .. name)
|
||||
else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local function lastLog(pattern)
|
||||
for i = #logLines, 1, -1 do
|
||||
if string.find(logLines[i], pattern, 1, true) then return logLines[i] end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function countLog(pattern)
|
||||
local n = 0
|
||||
for _, line in ipairs(logLines) do if string.find(line, pattern, 1, true) then n = n + 1 end end
|
||||
return n
|
||||
end
|
||||
|
||||
-- ---- AD-miljø ----
|
||||
local markers, wayPoints, unreachable = {}, {}, {}
|
||||
local function addMarker(id, name, group, x, z)
|
||||
wayPoints[id] = {id = id, x = x, z = z}
|
||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
||||
return markers[#markers]
|
||||
end
|
||||
local notifications = {}
|
||||
local RT_NONE, RT_ONLYPICKUP = 1, 2
|
||||
local adEnv = {
|
||||
AutoDrive = {
|
||||
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP,
|
||||
getAllUnits = function(vehicle) return vehicle.trailers end,
|
||||
getAllFillLevels = function(trailers) return trailers.level, trailers.capacity, trailers.level >= trailers.capacity * 0.99, trailers.capacity - trailers.level end,
|
||||
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end,
|
||||
hasAL = function(_, trailer) return trailer.ual ~= nil end,
|
||||
activateALTrailers = function(vehicle, trailers) trailers.activated = (trailers.activated or 0) + 1 end,
|
||||
},
|
||||
ADGraphManager = {
|
||||
getMapMarkers = function() return markers end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
|
||||
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
|
||||
},
|
||||
AutoDriveMessageEvent = {sendMessageOrNotification = function(vehicle, kind, text, duration, ...) table.insert(notifications, {vehicle = vehicle, kind = kind, text = string.format(text, ...)}) end},
|
||||
ADMessagesManager = {messageTypes = {INFO = 1, WARN = 2, ERROR = 3}},
|
||||
LoadAtDestinationTask = {STATE_DRIVING = 2, update = function() end, finished = function(self) self.finishedCalls = (self.finishedCalls or 0) + 1 end},
|
||||
UnloadAtDestinationTask = {STATE_DRIVING = 2, update = function() end},
|
||||
}
|
||||
|
||||
local function newVehicle(name, x, z, capacity, ual)
|
||||
local v = {name = name, rootNode = {x = x, z = z}, lastSpeedReal = 0, carriable = {SUGAR, FORAGE}, farmId = 1}
|
||||
v.getName = function() return name end
|
||||
v.trailers = {level = 0, capacity = capacity, ual = ual and {spec = {loadedObjects = {}}} or nil}
|
||||
v.trailers[1] = {spec_universalAutoload = ual and v.trailers.ual.spec or nil, ualIsFull = ual and function() return v.trailers.full == true end or nil, ual = ual and true or nil}
|
||||
v.ad = {
|
||||
stateModule = {isActive = function() return v.active ~= false end, selectedFillTypes = {}, fillType = nil,
|
||||
setFillType = function(self, ft) self.fillType = ft end, getFillType = function(self) return self.fillType end,
|
||||
setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end,
|
||||
getSecondMarker = function(self) return self.secondMarkerId and {id = self.secondMarkerId} or nil end,
|
||||
raiseDirtyFlag = function() end},
|
||||
drivePathModule = {isTargetReached = function() return v.atTarget == true end, setPathTo = function(_, id) v.pathTo = id end},
|
||||
specialDrivingModule = {stopVehicle = function() v.stopped = true end, update = function() end},
|
||||
trailerModule = {isActiveAtTrigger = function() return false end},
|
||||
}
|
||||
return v
|
||||
end
|
||||
local function newTask(vehicle, destinationID)
|
||||
return {vehicle = vehicle, destinationID = destinationID, trailers = vehicle.trailers, state = 2, activatedUALLoading = false,
|
||||
finished = function(self) return adEnv.LoadAtDestinationTask.finished(self) end}
|
||||
end
|
||||
|
||||
-- kilder, priser, siloer som fakes
|
||||
local sources, products, quotes, dumpMarker = {}, {}, {}, nil
|
||||
ADSources = {
|
||||
identify = function(_, id) return sources[id] end,
|
||||
getProducts = function(_, source) return products[source.wayPointId] or {} end,
|
||||
getSellMarkers = function() local r = {} for _, m in ipairs(markers) do if m.group == "selling" then table.insert(r, {marker = m, station = {name = m.name}}) end end return r end,
|
||||
stationAccepts = function(station, ft) return quotes[station.name] ~= nil and quotes[station.name][ft] ~= nil end,
|
||||
getSourcePlaceables = function() return {} end,
|
||||
getObjectInfoIndex = function(source, ft) local s = source.stored or {} return s[ft] ~= nil and s[ft] > 0 and 1 or nil, s[ft] end,
|
||||
countStored = function(source, ft) return (source.stored or {})[ft] or 0 end,
|
||||
spawnOne = function(source, index) source.spawnCalls = (source.spawnCalls or 0) + 1 return source.spawnFails ~= true end,
|
||||
}
|
||||
ADPrices = {
|
||||
getQuote = function(station, ft) return quotes[station.name] and quotes[station.name][ft] end,
|
||||
bestQuote = function(list) local best for _, q in ipairs(list) do if best == nil or q.percent > best.percent then best = q end end return best end,
|
||||
}
|
||||
ADLoadSwap = {findDumpMarker = function() return dumpMarker, "ingen silo" end}
|
||||
local runs = {}
|
||||
ADRunsController = {getRunForVehicle = function(vehicle) return runs[vehicle] end}
|
||||
local activePickupCalls = {}
|
||||
ADSmartPickup = {getFarmId = function(v) return v.farmId end, setActivePickup = function(vehicle, choice) table.insert(activePickupCalls, choice) end,
|
||||
getUnloadStationAtWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id and (m.group == "silo" or m.group == "selling") then return {name = m.name} end end end}
|
||||
|
||||
local mSugar = addMarker(1, "Suger Load", "Pickup points", 0, 0)
|
||||
local mSilo = addMarker(2, "silo 4", "silo", 50, 0)
|
||||
local mObj = addMarker(3, "Objektlager", "Pickup points", 100, 0)
|
||||
local mSell = addMarker(10, "selling station", "selling", 200, 0)
|
||||
local mSell2 = addMarker(11, "Far station", "selling", 300, 0)
|
||||
local mWait = addMarker(20, "Wait 3", "General Wait", 20, 0)
|
||||
local mStore = addMarker(30, "silo 6", "silo", 60, 0)
|
||||
sources[1] = {kind = "factory", wayPointId = 1, station = {name = "factoryStation"}, name = "Sugar mill"}
|
||||
sources[2] = {kind = "silo", wayPointId = 2, station = {name = "siloStation"}, name = "Farma"}
|
||||
sources[3] = {kind = "objectStorage", wayPointId = 3, station = nil, name = "Objektlager", stored = {[BASKET] = 3, [CRATE] = 1}}
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||
products[2] = {{fillType = SUGAR, name = "SUGAR", available = 9000, level = 9000, capacity = 12000000, isPallet = false}}
|
||||
products[3] = {{fillType = BASKET, name = "STRAW_BASKET", available = 3000, level = 4, capacity = 4000, isPallet = true}, {fillType = CRATE, name = "STRAW_CRATE", available = 1000, level = 4, capacity = 4000, isPallet = true}}
|
||||
quotes["selling station"] = {[SUGAR] = {percent = 92, price = 480}, [BASKET] = {percent = 95, price = 1}, [CRATE] = {percent = 95, price = 1}}
|
||||
quotes["Far station"] = {[SUGAR] = {percent = 99, price = 600}}
|
||||
|
||||
local function outboundRun(id, sourceIds, productsList, overrides)
|
||||
local run = {id = id, name = "Out " .. id, kind = "outbound", waitPoolGroup = "", sourceWayPointIds = sourceIds, products = productsList, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false}
|
||||
for k, v in pairs(overrides or {}) do run[k] = v end
|
||||
return run
|
||||
end
|
||||
|
||||
-- O1: tur -> AD's varevalg og 2. markør sat, choice = kilden; næstbedste salgssted når bedste er uopnåeligt
|
||||
unreachable[11] = true
|
||||
local v1 = newVehicle("MT635", 0, 0, 20000)
|
||||
runs[v1] = outboundRun(1, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
check("O1 er udkørsel", ADOutbound.isOutboundVehicle(v1), true)
|
||||
local choice = ADOutbound.choosePickup(adEnv, v1)
|
||||
check("O1 kilde-markør", choice.markerIndex, mSugar.markerIndex)
|
||||
check("O1 station med (bulk-greb)", choice.station.name, "factoryStation")
|
||||
check("O1 vare", v1.ad.stateModule.fillType, SUGAR)
|
||||
check("O1 varevalg", #v1.ad.stateModule.selectedFillTypes, 1)
|
||||
check("O1 2. markør = nåeligt salgssted", v1.ad.stateModule.secondMarkerId, 10)
|
||||
check("O1 ingen advarsel (næstbedste kunne nås)", #notifications, 0)
|
||||
check("O1 log", lastLog("udkørsel") ~= nil, true)
|
||||
local trip = ADOutbound.getTripFor(v1)
|
||||
check("O1 trip kind", trip.kind, "sell")
|
||||
unreachable[11] = nil
|
||||
|
||||
-- O2: ingen tur -> Wait-markør reserveret, hold registreret
|
||||
local v2 = newVehicle("ARION", 25, 0, 20000)
|
||||
runs[v2] = outboundRun(2, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
quotes["selling station"][SUGAR].percent = 70
|
||||
quotes["Far station"][SUGAR].percent = 60
|
||||
local choice2 = ADOutbound.choosePickup(adEnv, v2)
|
||||
check("O2 Wait-markør", choice2.markerIndex, mWait.markerIndex)
|
||||
check("O2 uden station", choice2.station, nil)
|
||||
check("O2 reserveret", ADWaitPool.getOwner(20), v2)
|
||||
local hold = ADOutbound.getHoldFor(v2)
|
||||
check("O2 hold", hold.markerName, "Wait 3")
|
||||
check("O2 begrundelse", hold.reasons[1], "SUGAR 70 % af top (kræver 90), lager 3 %")
|
||||
check("O2 run-status", ADOutbound.getRunStatus(2).reasons[1], hold.reasons[1])
|
||||
check("O2 hold-log én gang", countLog("holder ved Wait 3"), 1)
|
||||
ADOutbound.choosePickup(adEnv, v2)
|
||||
check("O2 samme begrundelse logges ikke igen", countLog("holder ved Wait 3"), 1)
|
||||
|
||||
-- O3: holder ved Wait: vognen stoppes; efter 5 s med tur -> omdirigeret til kilden, hold sluppet
|
||||
local task2 = newTask(v2, 20)
|
||||
v2.atTarget = true
|
||||
check("O3 håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 1000), true)
|
||||
check("O3 stoppet", v2.stopped, true)
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task2, 4500)
|
||||
check("O3 omdirigeret til kilden", task2.destinationID, 1)
|
||||
check("O3 sti sat", v2.pathTo, 1)
|
||||
check("O3 hold væk", ADOutbound.getHoldFor(v2), nil)
|
||||
check("O3 reservation frigivet", ADWaitPool.getOwner(20), nil)
|
||||
check("O3 activePickup opdateret", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
|
||||
check("O3 UAL-flag nulstillet", task2.activatedUALLoading, false)
|
||||
v2.atTarget = false
|
||||
check("O3 undervejs ikke håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 100), false)
|
||||
|
||||
-- O4: objektlager: én palle ad gangen; stored falder + loaded stiger -> næste; ualIsFull -> finished
|
||||
local v3 = newVehicle("Fendt", 100, 0, 8000, true)
|
||||
runs[v3] = outboundRun(3, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}, {fillType = "STRAW_CRATE", policy = "sell"}}, {collectRound = true})
|
||||
local choice3 = ADOutbound.choosePickup(adEnv, v3)
|
||||
check("O4 kilde = objektlager", choice3.markerIndex, mObj.markerIndex)
|
||||
check("O4 kun én vare valgt ad gangen ved objektlager", #v3.ad.stateModule.selectedFillTypes, 1)
|
||||
check("O4 turen har begge varer", #ADOutbound.getTripFor(v3).fillTypes, 2)
|
||||
local task3 = newTask(v3, 3)
|
||||
v3.atTarget = true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 intet spawn før UAL er aktiveret", sources[3].spawnCalls, nil)
|
||||
task3.activatedUALLoading = true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 første spawn", sources[3].spawnCalls, 1)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 venter på pallen", sources[3].spawnCalls, 1)
|
||||
sources[3].stored[BASKET] = 2
|
||||
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 andet spawn efter læsning", sources[3].spawnCalls, 2)
|
||||
sources[3].stored[BASKET] = 0
|
||||
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 skifter til kasser når kurvene er væk", v3.ad.stateModule.fillType, CRATE)
|
||||
check("O4 UAL genaktiveret ved vareskift", v3.trailers.activated, 1)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 tredje spawn (kasse)", sources[3].spawnCalls, 3)
|
||||
v3.trailers.full = true
|
||||
sources[3].stored[CRATE] = 0
|
||||
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||
check("O4 fuld -> finished", task3.finishedCalls, 1)
|
||||
|
||||
-- O5: spawn uden fremdrift i 30 s -> finished + WARN
|
||||
local v4 = newVehicle("Valtra", 100, 0, 8000, true)
|
||||
runs[v4] = outboundRun(4, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||
sources[3].stored[BASKET] = 5
|
||||
sources[3].spawnCalls = 0
|
||||
ADOutbound.choosePickup(adEnv, v4)
|
||||
local task4 = newTask(v4, 3)
|
||||
v4.atTarget, task4.activatedUALLoading = true, true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
|
||||
check("O5 spawn kaldt", sources[3].spawnCalls, 1)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task4, 31000)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
|
||||
check("O5 finished efter stall", task4.finishedCalls, 1)
|
||||
check("O5 WARN", lastLog("spawner ikke") ~= nil, true)
|
||||
|
||||
-- O6: opsamlingsrunde, bulk: kilde A fylder vognen -> finished uden redirect; kilde A giver 30 % -> videre til B
|
||||
local v5 = newVehicle("Claas", 0, 0, 10000)
|
||||
runs[v5] = outboundRun(5, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||
local choice5 = ADOutbound.choosePickup(adEnv, v5)
|
||||
check("O6 mest først = silo 4", choice5.markerIndex, mSilo.markerIndex)
|
||||
local task5 = newTask(v5, 2)
|
||||
v5.trailers.level = 10000
|
||||
check("O6 fuld: ingen runde", ADOutbound.beforeLoadFinished(adEnv, task5), false)
|
||||
local v6 = newVehicle("Claas2", 0, 0, 10000)
|
||||
runs[v6] = outboundRun(6, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||
ADOutbound.choosePickup(adEnv, v6)
|
||||
local task6 = newTask(v6, 2)
|
||||
v6.trailers.level = 3000
|
||||
check("O6 plads tilbage: videre til næste kilde", ADOutbound.beforeLoadFinished(adEnv, task6), true)
|
||||
check("O6 destination = Suger Load", task6.destinationID, 1)
|
||||
check("O6 activePickup = fabrik", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
|
||||
check("O6 alle besøgt: afslut", ADOutbound.beforeLoadFinished(adEnv, task6), false)
|
||||
|
||||
-- O7: intet nåeligt salgssted -> rød notifikation én gang
|
||||
notifications = {}
|
||||
unreachable[10], unreachable[11] = true, true
|
||||
local v7 = newVehicle("Deutz", 0, 0, 20000)
|
||||
runs[v7] = outboundRun(7, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
local choice7 = ADOutbound.choosePickup(adEnv, v7)
|
||||
check("O7 holder", ADOutbound.getHoldFor(v7) ~= nil, true)
|
||||
check("O7 én notifikation", #notifications, 1)
|
||||
check("O7 ERROR", notifications[1].kind, 3)
|
||||
check("O7 tekst", string.find(notifications[1].text, "kan ikke nå salgssted", 1, true) ~= nil, true)
|
||||
check("O7 status-advarsel", ADOutbound.getRunStatus(7).warning ~= nil, true)
|
||||
ADOutbound.choosePickup(adEnv, v7)
|
||||
check("O7 ikke gentaget", #notifications, 1)
|
||||
unreachable[10] = nil
|
||||
ADOutbound.choosePickup(adEnv, v7)
|
||||
check("O7 tur når vejen er der", ADOutbound.getTripFor(v7) ~= nil, true)
|
||||
check("O7 advarsel ryddet", ADOutbound.getRunStatus(7).warning, nil)
|
||||
unreachable[11] = nil
|
||||
|
||||
-- O8: ingen silo med plads ved lagerbehov -> notifikation; med silo -> store-tur
|
||||
notifications = {}
|
||||
local v8 = newVehicle("JD", 0, 0, 20000)
|
||||
runs[v8] = outboundRun(8, {2}, {{fillType = "SUGAR", policy = "store"}})
|
||||
products[2][1].level, products[2][1].capacity = 11000000, 12000000
|
||||
ADOutbound.choosePickup(adEnv, v8)
|
||||
check("O8 advarsel ingen silo", #notifications, 1)
|
||||
dumpMarker = mStore
|
||||
ADOutbound.choosePickup(adEnv, v8)
|
||||
check("O8 store-tur", ADOutbound.getTripFor(v8).kind, "store")
|
||||
check("O8 mål = silo 6", v8.ad.stateModule.secondMarkerId, 30)
|
||||
|
||||
-- O9: releaseInactive rydder hold + reservation
|
||||
local v9 = newVehicle("Case", 25, 0, 20000)
|
||||
runs[v9] = outboundRun(9, {1}, {{fillType = "FORAGE", policy = "sell"}})
|
||||
ADOutbound.choosePickup(adEnv, v9)
|
||||
check("O9 reserveret", ADWaitPool.getOwner(20), v9)
|
||||
v9.active = false
|
||||
ADOutbound.releaseInactive()
|
||||
check("O9 frigivet", ADWaitPool.getOwner(20), nil)
|
||||
check("O9 hold væk", ADOutbound.getHoldFor(v9), nil)
|
||||
|
||||
-- O11 (I2): hold-markøren skifter (Wait-punkt bliver ledigt) -> opgaven omdirigeres
|
||||
ADWaitPool.releaseVehicle(v9)
|
||||
local v11 = newVehicle("Hold", 25, 0, 20000)
|
||||
runs[v11] = outboundRun(11, {1}, {{fillType = "FORAGE", policy = "sell"}}) -- FORAGE findes ikke ved kilden -> aldrig tur
|
||||
local blocker = newVehicle("Blocker", 0, 0, 1)
|
||||
ADWaitPool.claim(20, blocker)
|
||||
local choice11 = ADOutbound.choosePickup(adEnv, v11)
|
||||
check("O11 uden ledigt Wait: holder ved kilden", choice11.markerIndex, mSugar.markerIndex)
|
||||
local task11 = newTask(v11, 1)
|
||||
v11.atTarget = true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task11, 100)
|
||||
ADWaitPool.releaseVehicle(blocker)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task11, 5000)
|
||||
check("O11 Wait-punkt ledigt -> omdirigeret", task11.destinationID, 20)
|
||||
check("O11 hold peger på Wait", ADOutbound.getHoldFor(v11).markerName, "Wait 3")
|
||||
ADWaitPool.releaseVehicle(v11)
|
||||
|
||||
-- O12 (I3): mål for last der allerede er på vognen ved Start
|
||||
local v12 = newVehicle("Loaded", 0, 0, 20000)
|
||||
runs[v12] = outboundRun(12, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
check("O12 salgsmål for sukker-last", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 10)
|
||||
quotes["selling station"][SUGAR].percent = 50
|
||||
check("O12 pris lav, ingen lager-politik -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), nil)
|
||||
runs[v12].products = {{fillType = "SUGAR", policy = "sellElseStore"}}
|
||||
check("O12 pris lav -> silo", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 30)
|
||||
check("O12 last der ikke er i run'et -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, FORAGE), nil)
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
|
||||
-- O13 (I4): tom vogn 30 s ved kilden -> kilden udelukkes for varen og turen afsluttes
|
||||
local v13 = newVehicle("Empty", 0, 0, 20000)
|
||||
runs[v13] = outboundRun(13, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = false})
|
||||
local choice13 = ADOutbound.choosePickup(adEnv, v13)
|
||||
check("O13 første kilde = silo 4 (mest)", choice13.markerIndex, mSilo.markerIndex)
|
||||
local task13 = newTask(v13, 2)
|
||||
v13.atTarget = true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task13, 100)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task13, 31000)
|
||||
check("O13 finished efter 30 s uden last", task13.finishedCalls, 1)
|
||||
check("O13 WARN", lastLog("intet at læsse") ~= nil, true)
|
||||
local choice13b = ADOutbound.choosePickup(adEnv, v13)
|
||||
check("O13 næste plan udelukker den tomme kilde", choice13b.markerIndex, mSugar.markerIndex)
|
||||
|
||||
-- O14 (I6): rutetjek kun for salgssteder over tærsklen, bedste først, stop ved første nåelige
|
||||
local routeChecks = 0
|
||||
local originalGetPathTo = adEnv.ADGraphManager.getPathTo
|
||||
adEnv.ADGraphManager.getPathTo = function(_, vehicle, id) routeChecks = routeChecks + 1 return originalGetPathTo(_, vehicle, id) end
|
||||
local v14 = newVehicle("Routes", 0, 0, 20000)
|
||||
runs[v14] = outboundRun(14, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
quotes["Far station"][SUGAR].percent = 99
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
ADOutbound.choosePickup(adEnv, v14)
|
||||
check("O14 bedste nåelig først: én rute tjekket", routeChecks, 1)
|
||||
check("O14 mål = bedst betalende", v14.ad.stateModule.secondMarkerId, 11)
|
||||
routeChecks = 0
|
||||
quotes["Far station"][SUGAR].percent = 60
|
||||
ADOutbound.choosePickup(adEnv, v14)
|
||||
check("O14 under tærsklen tjekkes ikke", routeChecks, 1)
|
||||
adEnv.ADGraphManager.getPathTo = originalGetPathTo
|
||||
|
||||
-- O15 (M1): advarsel for vare A gentages ikke efter en tur med vare B
|
||||
notifications = {}
|
||||
local v15 = newVehicle("Two", 0, 0, 20000)
|
||||
runs[v15] = outboundRun(15, {1, 2}, {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "sell"}})
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}, {fillType = FORAGE, name = "FORAGE", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||
quotes["selling station"][FORAGE] = {percent = 95, price = 1}
|
||||
quotes["Far station"][SUGAR].percent = 99 -- (O14 satte den til 60)
|
||||
unreachable[10] = true; unreachable[11] = true
|
||||
ADOutbound.choosePickup(adEnv, v15) -- SUGAR uopnåelig -> advarsel, FORAGE uopnåelig -> advarsel
|
||||
local firstCount = #notifications
|
||||
unreachable[11] = nil -- Far station kan nås for SUGAR (FORAGE sælges kun på selling station)
|
||||
ADOutbound.choosePickup(adEnv, v15) -- tur med SUGAR
|
||||
check("O15 tur", ADOutbound.getTripFor(v15).kind, "sell")
|
||||
ADOutbound.choosePickup(adEnv, v15) -- igen: FORAGE stadig uopnåelig, men allerede advaret
|
||||
check("O15 FORAGE-advarsel gentages ikke", #notifications, firstCount)
|
||||
unreachable[10] = nil
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||
|
||||
-- O16 (M6): pallens ankomst ses også på fyldniveau når UAL's interne liste mangler
|
||||
local v16 = newVehicle("NoList", 100, 0, 8000, true)
|
||||
v16.trailers[1].spec_universalAutoload = {}
|
||||
runs[v16] = outboundRun(16, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||
sources[3].stored[BASKET] = 3; sources[3].spawnCalls = 0
|
||||
ADOutbound.choosePickup(adEnv, v16)
|
||||
local task16 = newTask(v16, 3)
|
||||
v16.atTarget, task16.activatedUALLoading = true, true
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
|
||||
check("O16 første spawn", sources[3].spawnCalls, 1)
|
||||
sources[3].stored[BASKET] = 2
|
||||
v16.trailers.level = 1000
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
|
||||
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
|
||||
check("O16 næste spawn via fyldniveau", sources[3].spawnCalls, 2)
|
||||
|
||||
-- O17 (reserve + paller til lager): buildPlanInput henter reserve og palle-lager fra ADSources
|
||||
local demandCalls = {}
|
||||
ADSources.getFarmDemand = function(_, ft, farmId, excluded) table.insert(demandCalls, ft) return {liters = ft == SUGAR and 4000 or 0, consumers = {"TMR-fabrik"}} end
|
||||
local palletStoreMarker = nil
|
||||
ADSources.findPalletStore = function(_, vehicle, ft, excluded, isReachable, farmId) return palletStoreMarker end
|
||||
local v17 = newVehicle("Reserve", 0, 0, 20000)
|
||||
runs[v17] = outboundRun(17, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
local input17 = ADOutbound.buildPlanInput(adEnv, runs[v17], v17)
|
||||
check("O17 reserve på salgsvare", input17.products[1].reserve, 4000)
|
||||
check("O17 reserveret til", input17.products[1].reservedFor[1], "TMR-fabrik")
|
||||
local v18 = newVehicle("PalletStore", 100, 0, 8000, true)
|
||||
runs[v18] = outboundRun(18, {3}, {{fillType = "STRAW_BASKET", policy = "store"}})
|
||||
palletStoreMarker = {wayPointId = 40, name = "Palleskur", free = 100}
|
||||
sources[3].stored[BASKET] = 4
|
||||
products[3][1].available, products[3][1].level, products[3][1].capacity = 4000, 3900, 4000
|
||||
local input18 = ADOutbound.buildPlanInput(adEnv, runs[v18], v18)
|
||||
check("O18 palle-lager som store-mål", input18.products[1].store ~= nil and input18.products[1].store.wayPointId, 40)
|
||||
check("O18 reserve spørges ikke for lager-politik", demandCalls[#demandCalls] ~= BASKET, true)
|
||||
|
||||
-- O19: pallebil (kun UAL) ved en fabrik der både har bulk-læssestation og sukkerpaller -> paller
|
||||
local v19 = newVehicle("Dragon", 0, 0, 6000, true)
|
||||
v19.carriable = {}
|
||||
runs[v19] = outboundRun(19, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 150000, level = 150000, capacity = 180000, isPallet = false, canPallet = true, palletLiters = 3000, palletCount = 3}}
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
local input19 = ADOutbound.buildPlanInput(adEnv, runs[v19], v19)
|
||||
check("O19 varen tages som paller", input19.products[1] ~= nil and input19.products[1].isPallet, true)
|
||||
check("O19 mængde = paller ved spawner", input19.products[1].sources[1].available, 3000)
|
||||
check("O19 palle-antal følger med", input19.products[1].sources[1].palletCount, 3)
|
||||
local v19b = newVehicle("Tipvogn", 0, 0, 20000)
|
||||
runs[v19b] = outboundRun(20, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
local input19b = ADOutbound.buildPlanInput(adEnv, runs[v19b], v19b)
|
||||
check("O19 tipvogn tager bulk", input19b.products[1].isPallet, false)
|
||||
check("O19 bulk-mængde", input19b.products[1].sources[1].available, 150000)
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||
|
||||
-- O20: UAL-bil: AD's "kapacitet" er antal paller (læssede + 1) -> liter-kapacitet ukendt i planen
|
||||
local v20 = newVehicle("Dragon2", 100, 0, 1, true)
|
||||
v20.carriable = {}
|
||||
runs[v20] = outboundRun(21, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||
local input20 = ADOutbound.buildPlanInput(adEnv, runs[v20], v20)
|
||||
check("O20 kapacitet ukendt for pallebil", input20.capacity, nil)
|
||||
check("O20 har autoload", input20.hasAutoload, true)
|
||||
check("O20 turen planlægges alligevel", ADOutboundPlanner.plan(input20) ~= nil, true)
|
||||
|
||||
-- O21: autoload-bil på vej til aflæsning: stropperne løsnes UNSTRAP_BEFORE_M før målet (ladet passerer
|
||||
-- triggeren før markøren), kun én gang pr. tur, og aldrig for tipvogne
|
||||
local v21 = newVehicle("Dragon3", 0, 0, 1, true)
|
||||
v21.carriable = {}
|
||||
v21.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
|
||||
runs[v21] = outboundRun(22, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||
sources[3].stored[BASKET] = 3
|
||||
quotes["selling station"][BASKET] = {percent = 95, price = 1}
|
||||
ADOutbound.choosePickup(adEnv, v21)
|
||||
check("O21 tur mod salgssted", v21.ad.stateModule.secondMarkerId, 10)
|
||||
local unloadTask21 = {vehicle = v21, destinationID = 10, trailers = v21.trailers}
|
||||
v21.rootNode = {x = 200 - 40, z = 0} -- 40 m fra markør 10 (x=200)
|
||||
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
|
||||
check("O21 40 m: stropper stadig på", v21.trailers[1].beltsActive, nil)
|
||||
v21.rootNode = {x = 200 - 20, z = 0}
|
||||
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
|
||||
check("O21 20 m: stropper løsnet", v21.trailers[1].beltsActive, false)
|
||||
v21.trailers[1].beltsActive = nil
|
||||
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
|
||||
check("O21 kun én gang", v21.trailers[1].beltsActive, nil)
|
||||
local v21b = newVehicle("Tip", 0, 0, 20000)
|
||||
v21b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
|
||||
runs[v21b] = outboundRun(23, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||
quotes["selling station"][SUGAR].percent = 92
|
||||
ADOutbound.choosePickup(adEnv, v21b)
|
||||
v21b.rootNode = {x = 195, z = 0}
|
||||
ADOutbound.beforeUnloadUpdate(adEnv, {vehicle = v21b, destinationID = 10, trailers = v21b.trailers}, 16)
|
||||
check("O21 tipvogn: ingen stropper", v21b.trailers[1].beltsActive, nil)
|
||||
|
||||
-- O22: lagermål med en anden silo-markør inden for AD's globale trigger-afstand -> advarsel én gang
|
||||
-- (AD tipper ved første trigger inden for afstanden; 10 m løser det når siloer står tæt)
|
||||
local triggerDistance = 25
|
||||
adEnv.AutoDrive.getSetting = function(name) if name == "maxTriggerDistance" then return triggerDistance end end
|
||||
local v22 = newVehicle("Tip2", 0, 0, 20000)
|
||||
runs[v22] = outboundRun(24, {1}, {{fillType = "SUGAR", policy = "store"}})
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 170000, level = 170000, capacity = 180000, isPallet = false}}
|
||||
dumpMarker = mStore -- silo 6 (x=60); silo 4 (x=50) står 10 m derfra
|
||||
logLines = {}
|
||||
ADOutbound.choosePickup(adEnv, v22)
|
||||
check("O22 lager-tur", ADOutbound.getTripFor(v22) ~= nil and ADOutbound.getTripFor(v22).kind, "store")
|
||||
check("O22 advarsel om trigger-afstand", lastLog("Max trigger distance") ~= nil, true)
|
||||
logLines = {}
|
||||
ADOutbound.choosePickup(adEnv, v22)
|
||||
check("O22 kun én gang", lastLog("Max trigger distance"), nil)
|
||||
triggerDistance = 10
|
||||
local v22b = newVehicle("Tip3", 0, 0, 20000)
|
||||
runs[v22b] = outboundRun(25, {1}, {{fillType = "SUGAR", policy = "store"}})
|
||||
logLines = {}
|
||||
ADOutbound.choosePickup(adEnv, v22b)
|
||||
check("O22 ved 10 m ingen advarsel", lastLog("Max trigger distance"), nil)
|
||||
adEnv.AutoDrive.getSetting = nil
|
||||
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||
|
||||
-- O23: intet salgssted tager varen -> log én gang med de fundne salgsmarkører og hvad de tager
|
||||
ADSources.describeAccepted = function(station) return "SUGAR, FORAGE" end
|
||||
local v23 = newVehicle("Dragon4", 100, 0, 1, true)
|
||||
v23.carriable = {}
|
||||
runs[v23] = outboundRun(26, {3}, {{fillType = "STRAW_CRATE", policy = "sell"}})
|
||||
quotes["selling station"][CRATE] = nil; quotes["Far station"][CRATE] = nil
|
||||
sources[3].stored[CRATE] = 2
|
||||
logLines = {}
|
||||
ADOutbound.choosePickup(adEnv, v23)
|
||||
local noSell = lastLog("intet salgssted tager STRAW_CRATE")
|
||||
check("O23 log om manglende salgssted", noSell ~= nil, true)
|
||||
check("O23 lister salgsmarkører", noSell ~= nil and noSell:find("selling station (SUGAR, FORAGE)", 1, true) ~= nil, true)
|
||||
logLines = {}
|
||||
ADOutbound.choosePickup(adEnv, v23)
|
||||
check("O23 kun én gang", lastLog("intet salgssted tager STRAW_CRATE"), nil)
|
||||
|
||||
-- O10: install wrapper: fejl i hook stopper ikke AD
|
||||
local calls = {}
|
||||
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
|
||||
adEnv.LoadAtDestinationTask.finished = function(self) calls.finished = (calls.finished or 0) + 1 end
|
||||
ADOutbound.install(adEnv)
|
||||
local vX = newVehicle("X", 0, 0, 1000)
|
||||
adEnv.LoadAtDestinationTask.update(newTask(vX, 1), 16)
|
||||
check("O10 original update kaldt", calls.update, 1)
|
||||
adEnv.LoadAtDestinationTask.finished(newTask(vX, 1))
|
||||
check("O10 original finished kaldt", calls.finished, 1)
|
||||
adEnv.UnloadAtDestinationTask.update = function(self, dt) calls.unload = (calls.unload or 0) + 1 end
|
||||
ADOutbound.install(adEnv)
|
||||
check("O10 unload-wrap ikke installeret dobbelt", adEnv.UnloadAtDestinationTask.update ~= nil, true)
|
||||
-- O24: stropperne spændes igen når pålæsningen er færdig (de blev løsnet 25 m før forrige salg;
|
||||
-- in-game 23/9: Dragon tabte paller på vejen to gange)
|
||||
local v24 = newVehicle("Dragon5", 100, 0, 1, true)
|
||||
v24.carriable = {}
|
||||
v24.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
|
||||
runs[v24] = outboundRun(27, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||
sources[3].stored[BASKET] = 3
|
||||
quotes["selling station"][BASKET] = {percent = 95, price = 1}
|
||||
ADOutbound.choosePickup(adEnv, v24)
|
||||
local task24 = newTask(v24, 3)
|
||||
v24.trailers.level = 3
|
||||
adEnv.LoadAtDestinationTask.finished(task24)
|
||||
check("O24 stropper spændt efter pålæsning", v24.trailers[1].beltsActive, true)
|
||||
check("O24 tur planlagt", ADOutbound.getTripFor(v24) ~= nil, true)
|
||||
check("O24 original finished kaldt", calls.finished, 2)
|
||||
local v24b = newVehicle("Tip4", 0, 0, 20000)
|
||||
v24b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
|
||||
adEnv.LoadAtDestinationTask.finished(newTask(v24b, 1))
|
||||
check("O24 tipvogn uden tur: rører ikke stropper", v24b.trailers[1].beltsActive, nil)
|
||||
|
||||
-- O-pool: to kilder hvis stationer deler lager (mælketanke som udvidelser) = ét stop i runden
|
||||
do
|
||||
local pool = {}
|
||||
local st1, st2 = sources[1].station, sources[2].station
|
||||
sources[1].station = {name = "tankA", sourceStorages = {[pool] = pool}}
|
||||
sources[2].station = {name = "tankB", sourceStorages = {[pool] = pool}}
|
||||
local k1, k2 = sources[1].kind, sources[2].kind
|
||||
sources[1].kind, sources[2].kind = "silo", "silo"
|
||||
local vPool = newVehicle("Tank", 0, 0, 10000)
|
||||
runs[vPool] = outboundRun(77, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||
local inputPool = ADOutbound.buildPlanInput(adEnv, runs[vPool], vPool)
|
||||
local ids = {}
|
||||
for _, product in ipairs(inputPool.products) do
|
||||
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
|
||||
end
|
||||
check("O-pool kun første kilde", table.concat(ids, ","), "1")
|
||||
check("O-pool logget", lastLog("deler lager") ~= nil, true)
|
||||
sources[1].station, sources[2].station = st1, st2
|
||||
sources[1].kind, sources[2].kind = k1, k2
|
||||
end
|
||||
|
||||
-- O-pool-stald: mælke- og gyllemarkør ved samme stald deler lager, men er forskellige læssepunkter
|
||||
do
|
||||
local pool = {}
|
||||
sources[31] = {kind = "husbandry", wayPointId = 31, station = {name = "barnSlurry", sourceStorages = {[pool] = pool}}, name = "Stald"}
|
||||
sources[32] = {kind = "husbandry", wayPointId = 32, station = {name = "barnMilk", sourceStorages = {[pool] = pool}}, name = "Stald"}
|
||||
products[31] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
|
||||
products[32] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
|
||||
local vBarn = newVehicle("Tank2", 0, 0, 10000)
|
||||
runs[vBarn] = outboundRun(78, {31, 32}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||
local inputBarn = ADOutbound.buildPlanInput(adEnv, runs[vBarn], vBarn)
|
||||
local ids = {}
|
||||
for _, product in ipairs(inputBarn.products) do
|
||||
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
|
||||
end
|
||||
check("O-pool-stald: stalde lægges ikke sammen", table.concat(ids, ","), "31,32")
|
||||
sources[31], sources[32], products[31], products[32] = nil, nil, nil, nil
|
||||
end
|
||||
|
||||
-- O-forced: "Lager til" valgt -> lagerturen går til det valgte mål, ikke moden egen silo-valg
|
||||
do
|
||||
sources[41] = {kind = "silo", wayPointId = 41, station = {name = "gylleSilo"}, name = "Gyllesilo"}
|
||||
products[41] = {{fillType = SUGAR, name = "SUGAR", title = "Sukker", available = 11000000, level = 11000000, capacity = 12000000, isPallet = false}}
|
||||
local vF = newVehicle("Gyllevogn", 0, 0, 20000)
|
||||
runs[vF] = outboundRun(79, {41}, {{fillType = "SUGAR", policy = "store"}})
|
||||
runs[vF].forcedMarkers = {31}
|
||||
ADRunsController.getStoreMarkers = function(_, run) return run.forcedMarkers or {} end
|
||||
local forcedCalls = 0
|
||||
ADLoadSwap.findForcedDumpMarker = function(_, _, _, _, ids) forcedCalls = forcedCalls + 1 return {id = ids[1], name = "BGA", markerIndex = 99} end
|
||||
dumpMarker = mStore
|
||||
ADOutbound.choosePickup(adEnv, vF)
|
||||
check("O-forced valgt mål brugt", forcedCalls > 0, true)
|
||||
check("O-forced tur-mål = BGA", ADOutbound.getTripFor(vF) and ADOutbound.getTripFor(vF).targetWayPointId, 31)
|
||||
ADRunsController.getStoreMarkers, ADLoadSwap.findForcedDumpMarker = nil, nil
|
||||
end
|
||||
|
||||
-- O-empty: ingen vare ved kilden (æggene er ikke spawnet endnu) -> intet lagermål søges, ingen advarsel
|
||||
do
|
||||
sources[42] = {kind = "husbandry", wayPointId = 42, station = nil, name = "Chicken Coop"}
|
||||
products[42] = {{fillType = SUGAR, name = "SUGAR", title = "Æg", available = 0, level = 0, capacity = 0, isPallet = false}}
|
||||
local vE = newVehicle("Æg", 0, 0, 20000)
|
||||
runs[vE] = outboundRun(80, {42}, {{fillType = "SUGAR", policy = "store"}})
|
||||
local storeLookups = 0
|
||||
local dumpBefore = ADLoadSwap.findDumpMarker
|
||||
ADLoadSwap.findDumpMarker = function(...) storeLookups = storeLookups + 1 return nil, "ingen" end
|
||||
ADOutbound.buildPlanInput(adEnv, runs[vE], vE)
|
||||
check("O-empty intet lagermål søgt uden vare", storeLookups, 0)
|
||||
ADLoadSwap.findDumpMarker = dumpBefore
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,177 +0,0 @@
|
||||
-- Kør: luajit tests/test_adOutboundPlanner.lua (fra repo-roden)
|
||||
-- Ren turvurdering for udkørsel: prisregel, tærskler, politikker, rangering, opsamlingsrunde.
|
||||
|
||||
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
|
||||
local P = ADOutboundPlanner
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then
|
||||
print("OK " .. name)
|
||||
else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
local function join(list) return table.concat(list or {}, ",") end
|
||||
|
||||
local SUGAR, FORAGE, BASKET, CRATE, BROOM = 5, 6, 7, 8, 9
|
||||
local function input(products, overrides)
|
||||
local result = {capacity = 20000, hasAutoload = false, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false, products = products}
|
||||
for key, value in pairs(overrides or {}) do result[key] = value end
|
||||
return result
|
||||
end
|
||||
local function product(name, fillType, policy, sources, quote, store, isPallet)
|
||||
return {fillType = fillType, name = name, policy = policy, isPallet = isPallet == true, sources = sources, quote = quote, store = store}
|
||||
end
|
||||
local quoteS = {percent = 92, price = 480, wayPointId = 900, stationName = "Grain Elevator"}
|
||||
local storeSilo = {wayPointId = 400, name = "silo 1", free = 500000}
|
||||
|
||||
-- P1: salg ved pris over tærskel, uanset mængde
|
||||
local trip = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 5000, level = 5000, capacity = 180000}}, quoteS, nil)}))
|
||||
check("P1 salg", trip.kind, "sell")
|
||||
check("P1 mål", trip.targetWayPointId, 900)
|
||||
check("P1 målnavn", trip.targetName, "Grain Elevator")
|
||||
check("P1 kilde", join(trip.sourceIds), "1")
|
||||
check("P1 vare", join(trip.fillTypes), "5")
|
||||
check("P1 procent", trip.percent, 92)
|
||||
|
||||
-- P2: pris under tærskel, lager lavt -> ingen tur med begrundelse
|
||||
local low = {percent = 78, price = 400, wayPointId = 900, stationName = "Grain Elevator"}
|
||||
local trip2, reasons2 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 74000, level = 74000, capacity = 180000}}, low, nil)}))
|
||||
check("P2 ingen tur", trip2, nil)
|
||||
check("P2 begrundelse", reasons2[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
|
||||
|
||||
-- P3: sellElseStore, pris lav, lager 85 % -> lager
|
||||
local trip3 = P.plan(input({product("SUGAR", SUGAR, "sellElseStore", {{wayPointId = 1, available = 153000, level = 153000, capacity = 180000}}, low, storeSilo)}))
|
||||
check("P3 lager", trip3.kind, "store")
|
||||
check("P3 silo", trip3.targetWayPointId, 400)
|
||||
|
||||
-- P4: store med fuldt læs, lager 20 %
|
||||
local trip4 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 700000, level = 700000, capacity = 3500000}}, nil, storeSilo)}))
|
||||
check("P4 fuldt læs -> lager", trip4.kind, "store")
|
||||
local trip4b = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 15000, level = 15000, capacity = 3500000}}, nil, storeSilo)}))
|
||||
check("P4 for lidt og lager lavt -> nil", trip4b, nil)
|
||||
|
||||
-- P5: rangering: højeste pris-% først
|
||||
local trip5 = P.plan(input({
|
||||
product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 1000, level = 1000, capacity = 180000}}, {percent = 91, price = 1, wayPointId = 900, stationName = "A"}, nil),
|
||||
product("FORAGE", FORAGE, "sell", {{wayPointId = 2, available = 1000, level = 1000, capacity = 3500000}}, {percent = 95, price = 1, wayPointId = 901, stationName = "B"}, nil),
|
||||
}))
|
||||
check("P5 højeste procent vinder", join(trip5.fillTypes), "6")
|
||||
check("P5 uden runde kun én vare", #trip5.fillTypes, 1)
|
||||
|
||||
-- P6: intet nåeligt salgssted
|
||||
local trip6, reasons6 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 170000, level = 170000, capacity = 180000}}, nil, nil)}))
|
||||
check("P6 ingen tur", trip6, nil)
|
||||
check("P6 begrundelse", reasons6[1], "SUGAR: intet nåeligt salgssted")
|
||||
|
||||
-- P7: ingen silo med plads ved lagerbehov
|
||||
local trip7, reasons7 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)}))
|
||||
check("P7 ingen tur", trip7, nil)
|
||||
check("P7 begrundelse", reasons7[1], "FORAGE: intet lager med plads (silo eller egen fabrik der bruger varen)")
|
||||
|
||||
-- P8: runde, bulk: samme vare fra flere kilder, mest først
|
||||
local trip8 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}, {collectRound = true, capacity = 10000}))
|
||||
check("P8 kilder mest først", join(trip8.sourceIds), "3,1")
|
||||
check("P8 én vare", #trip8.fillTypes, 1)
|
||||
|
||||
-- P9: runde, UAL: alle pallevarer med samme mål
|
||||
local sellS = function(pct) return {percent = pct, price = 1, wayPointId = 950, stationName = "Halm-salg"} end
|
||||
local sellT = {percent = 96, price = 1, wayPointId = 951, stationName = "Andet"}
|
||||
local pallets = {
|
||||
product("STRAW_BASKET", BASKET, "sell", {{wayPointId = 5, available = 8000, level = 8, capacity = 4000}}, sellS(95), nil, true),
|
||||
product("STRAW_CRATE", CRATE, "sell", {{wayPointId = 5, available = 2000, level = 2, capacity = 4000}}, sellS(93), nil, true),
|
||||
product("STRAW_BROOM", BROOM, "sell", {{wayPointId = 6, available = 1000, level = 1, capacity = 4000}}, sellT, nil, true),
|
||||
}
|
||||
local trip9 = P.plan(input(pallets, {collectRound = true, hasAutoload = true}))
|
||||
check("P9 højeste procent = kost alene (andet mål)", join(trip9.fillTypes), "9")
|
||||
local trip9b = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = true}))
|
||||
check("P9 kurve+kasser samme mål", join(trip9b.fillTypes), "7,8")
|
||||
check("P9 navne", join(trip9b.names), "STRAW_BASKET,STRAW_CRATE")
|
||||
check("P9 kilder unikke", join(trip9b.sourceIds), "5")
|
||||
local trip9c = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = false}))
|
||||
check("P9 uden UAL kun én pallevare", #trip9c.fillTypes, 1)
|
||||
|
||||
-- P10: uden runde kun én kilde (den med mest)
|
||||
local trip10 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}))
|
||||
check("P10 én kilde, den fyldeste", join(trip10.sourceIds), "3")
|
||||
|
||||
-- P11: intet på lager
|
||||
local trip11, reasons11 = P.plan(input({product("SUGAR", SUGAR, "sell", {}, quoteS, nil)}))
|
||||
check("P11 ingen tur", trip11, nil)
|
||||
check("P11 begrundelse", reasons11[1], "SUGAR: intet på lager")
|
||||
|
||||
-- P12: evaluate
|
||||
local e = P.evaluate(product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 150000, level = 150000, capacity = 180000}}, quoteS, nil), input({}))
|
||||
check("P12 storagePercent", math.floor(e.storagePercent + 0.5), 83)
|
||||
check("P12 storageHigh", e.storageHigh, true)
|
||||
check("P12 fullLoad", e.fullLoad, true)
|
||||
check("P12 priceOk", e.priceOk, true)
|
||||
|
||||
-- P13 (reserve): det gårdens fabrikker/stalde skal bruge, sælges ikke
|
||||
local BEET = 10
|
||||
local withReserve = product("SUGARBEET", BEET, "sell", {{wayPointId = 3, available = 268000, level = 268000, capacity = 12000000}}, quoteS, nil)
|
||||
withReserve.reserve, withReserve.reservedFor = 196000, {"TMR-fabrik"}
|
||||
local trip13 = P.plan(input({withReserve}))
|
||||
check("P13 fuldt læs uden om reserven -> salg", trip13 ~= nil and trip13.kind, "sell")
|
||||
withReserve.reserve = 260000 -- kun 8.000 til overs: mindre end et læs
|
||||
local trip13b, reasons13b = P.plan(input({withReserve}))
|
||||
check("P13 reserven ville blive rørt -> ingen tur", trip13b, nil)
|
||||
check("P13 begrundelse", reasons13b[1], "SUGARBEET: 260.0k reserveret til TMR-fabrik")
|
||||
withReserve.reserve = 300000 -- alt reserveret
|
||||
check("P13 alt reserveret -> ingen tur", P.plan(input({withReserve})), nil)
|
||||
withReserve.reserve = 0
|
||||
check("P13 reserve 0 -> salg uanset mængde", P.plan(input({withReserve})).kind, "sell")
|
||||
-- lager-politik ignorerer reserven (varen bliver på gården)
|
||||
local storeReserve = product("SUGARBEET", BEET, "store", {{wayPointId = 3, available = 268000, level = 11000000, capacity = 12000000}}, nil, storeSilo)
|
||||
storeReserve.reserve = 300000
|
||||
check("P13 lager rører ikke reserven", P.plan(input({storeReserve})).kind, "store")
|
||||
local e13 = P.evaluate(withReserve, input({}, {capacity = 20000}))
|
||||
check("P13 evaluate sellable", e13.sellable, 268000)
|
||||
|
||||
-- P14 (paller): kapacitet ukendt for en autoload-bil -> "fuldt læs" = mindst PALLET_FULL_LOAD_COUNT paller klar
|
||||
local function palletInput(products)
|
||||
local inp = input(products, {hasAutoload = true})
|
||||
inp.capacity = nil
|
||||
return inp
|
||||
end
|
||||
local palletProduct = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 3000, level = 1600, capacity = 8000, palletCount = 3}}, nil, storeSilo, true)
|
||||
check("P14 3 paller: ikke fuldt læs", P.plan(palletInput({palletProduct})), nil)
|
||||
palletProduct.sources[1].palletCount = 4
|
||||
check("P14 4 paller: lager-tur", P.plan(palletInput({palletProduct})).kind, "store")
|
||||
local twoSources = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 2000, level = 1600, capacity = 8000, palletCount = 2}, {wayPointId = 3, available = 2000, level = 2, capacity = 4000, palletCount = 2}}, nil, storeSilo, true)
|
||||
check("P14 paller summeres over kilder", P.plan(palletInput({twoSources})).kind, "store")
|
||||
check("P14 evaluate palletCount", P.evaluate(twoSources, palletInput({})).palletCount, 4)
|
||||
|
||||
-- P15: kilder der deler lager (mælketanke = udvidelser inden for storageRadius) regnes som ét stop;
|
||||
-- den første i forløbets rækkefølge beholdes, rækkefølgen bevares
|
||||
local storA, storB, storC = {}, {}, {}
|
||||
local pools = {[1] = {storA, storB}, [2] = {storB}, [3] = {storC}, [4] = {storA}, [5] = nil}
|
||||
local srcs = {{wayPointId = 1}, {wayPointId = 2}, {wayPointId = 3}, {wayPointId = 4}, {wayPointId = 5}}
|
||||
local kept, merged = P.dedupeSharedSources(srcs, function(source) return pools[source.wayPointId] end)
|
||||
check("P15 behold 1, 3, 5", join((function() local ids = {} for _, s in ipairs(kept) do table.insert(ids, s.wayPointId) end return ids end)()), "1,3,5")
|
||||
check("P15 slået sammen: 2 og 4 ind i 1", join(merged[1]), "2,4")
|
||||
check("P15 kilde uden lagre beholdes", kept[3].wayPointId, 5)
|
||||
local none = P.dedupeSharedSources({}, function() return nil end)
|
||||
check("P15 tom liste", #none, 0)
|
||||
|
||||
-- P16: fodring ("Lager til" valgt = feedMode): kør så snart kilden har FEED_MIN_LITERS — ikke fuldt læs / 80 %
|
||||
local feedStore = {wayPointId = 50, name = "Slurry Drop 1", free = 100000}
|
||||
local feedProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 20000, level = 20000, capacity = 500000}}, nil, feedStore)
|
||||
local tripNoFeed = P.plan(input({feedProduct}, {capacity = 100000}))
|
||||
check("P16 uden fodring: 4 % og intet fuldt læs -> ingen tur", tripNoFeed, nil)
|
||||
local tripFeed = P.plan(input({feedProduct}, {capacity = 100000, feedMode = true}))
|
||||
check("P16 fodring: 20k >= minimum -> lagertur", tripFeed and tripFeed.kind, "store")
|
||||
check("P16 fodring: mål = valgt drop", tripFeed and tripFeed.targetWayPointId, 50)
|
||||
local tinyProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 3000, level = 3000, capacity = 500000}}, nil, feedStore)
|
||||
check("P16 fodring: under minimum -> ingen tur", P.plan(input({tinyProduct}, {capacity = 100000, feedMode = true})), nil)
|
||||
|
||||
-- P17: valgt lager ("Lager til") kan ikke bruges -> begrundelsen er den præcise grund, ikke "intet lager med plads"
|
||||
local blocked = product("EGG", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)
|
||||
blocked.storeReason = "Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'"
|
||||
local _, reasons17 = P.plan(input({blocked}))
|
||||
check("P17 præcis grund i status", reasons17[1], "EGG: Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,106 +0,0 @@
|
||||
-- Kør: luajit tests/test_adPrices.lua (fra repo-roden)
|
||||
-- Prisregel: toppris fra årets prisforløb, pris-% pr. salgssted, bedste tilbud.
|
||||
|
||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
||||
|
||||
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
|
||||
|
||||
-- P1: højeste faktor
|
||||
check("P1 maxFactor", ADPrices.maxFactor({EARLY_SPRING = 0.9, LATE_AUTUMN = 1.2, MID_SUMMER = 1.0}), 1.2)
|
||||
check("P1 uden faktorer = 1", ADPrices.maxFactor(nil), 1)
|
||||
|
||||
-- P2: toppris = grundpris nu × (maks ÷ nu)
|
||||
check("P2 topPrice", ADPrices.topPrice(500, 1.0, 1.25), 625)
|
||||
check("P2 factorNow 0 -> grundpris", ADPrices.topPrice(500, 0, 1.25), 500)
|
||||
check("P2 grundpris 0 -> 0", ADPrices.topPrice(0, 1, 1.25), 0)
|
||||
|
||||
-- P3: procent, afrundet; stor efterspørgsel > 100
|
||||
check("P3 75 %", ADPrices.percentOfTop(470, 625), 75)
|
||||
check("P3 112 %", ADPrices.percentOfTop(700, 625), 112)
|
||||
check("P3 top 0 -> 0", ADPrices.percentOfTop(1, 0), 0)
|
||||
|
||||
-- P4: spil-adapter med stationens grundpris
|
||||
SellingStation = {}
|
||||
local SUGAR = 5
|
||||
g_fillTypeManager = {getFillTypeByIndex = function(_, index)
|
||||
if index == SUGAR then return {economy = {factors = {A = 1.0, B = 1.25}}, pricePerLiter = 400} end
|
||||
end}
|
||||
g_currentMission = {environment = {currentPeriod = "A"}, economyManager = {getPriceMultiplier = function() return 1.0 end}}
|
||||
local station = {
|
||||
fillTypePrices = {[SUGAR] = 500},
|
||||
getEffectiveFillTypePrice = function(_, fillType) return 470 end,
|
||||
isa = function(_, class) return class == SellingStation end,
|
||||
isSellingPoint = true,
|
||||
}
|
||||
local quote = ADPrices.getQuote(station, SUGAR)
|
||||
check("P4 pris", quote.price, 470)
|
||||
check("P4 top (pris ved 100 %)", math.floor(quote.top + 0.5), 588)
|
||||
check("P4 procent = kurven alene (stationens grundpris bruges ikke: den mangler sværhedsgrad m.m.)", quote.percent, 80)
|
||||
check("P4 er salgssted", ADPrices.isSellingStation(station), true)
|
||||
check("P4 skjult = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, hideFromPricesMenu = true}), false)
|
||||
check("P4 fabrik-station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = false}), false)
|
||||
-- in-game (23/9): TMR-fabrikkens indlevering er en SellingStation med isSellingPoint=true -> blev regnet som marked
|
||||
check("P4 fabrikkens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}}}), false)
|
||||
check("P4 staldens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end}}), false)
|
||||
-- P4b: fremmed produktion (kortets mejeri, ejer 0) køber varen = salgssted; gårdens egen fabrik er ikke
|
||||
check("P4b fremmed mejeri = salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}}), true)
|
||||
check("P4b egen fabrik (ejer 1) = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 1 end}}), false)
|
||||
check("P4b fremmed stald-felt ownerFarmId", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end, ownerFarmId = 0}}), true)
|
||||
check("P4 rigtigt salgssted med placeable", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {}}), true)
|
||||
|
||||
-- P5: uden stationens grundpris: kun kurven tæller (faktor nu ÷ faktor max)
|
||||
local bare = {getEffectiveFillTypePrice = function() return 470 end}
|
||||
local quote2 = ADPrices.getQuote(bare, SUGAR)
|
||||
check("P5 procent = kurven alene", quote2.percent, 80)
|
||||
check("P5 top = effektiv ÷ kurve", math.floor(quote2.top + 0.5), 588)
|
||||
-- P7: kurven er grundlaget (Distribution Redux-metoden): faktor nu ÷ årets max, uafhængigt af sværhedsgrad
|
||||
check("P7 curvePercent", ADPrices.curvePercent({1.0, 1.25, 0.9}, 1), 80)
|
||||
check("P7 curvePercent ved top", ADPrices.curvePercent({1.0, 1.25, 0.9}, 2), 100)
|
||||
check("P7 flad kurve = 100", ADPrices.curvePercent({1.0, 1.01, 1.0}, 3), 100)
|
||||
check("P7 ingen kurve = 100", ADPrices.curvePercent(nil, 1), 100)
|
||||
check("P7 ukendt periode = 100", ADPrices.curvePercent({1.0, 1.25}, nil), 100)
|
||||
-- P8: in-game (23/9): effektiv 1602 mod fillTypePrices 505 gav 317 % — stationens grundpris indeholder
|
||||
-- hverken sværhedsgrad eller prisskala, så forholdet er ubrugeligt. Procenten er kurven alene.
|
||||
local demandStation = {fillTypePrices = {[SUGAR] = 505}, getEffectiveFillTypePrice = function() return 1602 end}
|
||||
check("P8 procent uafhængig af stationens grundpris", ADPrices.getQuote(demandStation, SUGAR).percent, 80)
|
||||
check("P8 top = pris ved 100 % af kurven", math.floor(ADPrices.getQuote(demandStation, SUGAR).top + 0.5), 2003)
|
||||
-- P9: sværhedsgraden påvirker ikke procenten
|
||||
local hardStation = {getEffectiveFillTypePrice = function() return 235 end}
|
||||
check("P9 uafhængig af sværhedsgrad", ADPrices.getQuote(hardStation, SUGAR).percent, 80)
|
||||
check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil)
|
||||
|
||||
-- P6: bedste tilbud = højeste procent, dernæst pris
|
||||
local best = ADPrices.bestQuote({{percent = 80, price = 1}, {percent = 92, price = 2}, {percent = 92, price = 3}})
|
||||
check("P6 højeste procent", best.percent, 92)
|
||||
check("P6 ved lighed højeste pris", best.price, 3)
|
||||
check("P6 tom -> nil", ADPrices.bestQuote({}), nil)
|
||||
|
||||
-- P-land: kortanlæg der følger jorden (boughtWithFarmland) er gårdens, når jorden under dem er gårdens —
|
||||
-- selv om placeable-ejeren står som 15 (Dakotas BGA500kw på jordlod 69)
|
||||
getWorldTranslation = getWorldTranslation or function(node) return node.x, 0, node.z end
|
||||
g_currentMission = g_currentMission or {}
|
||||
g_currentMission.getFarmId = function() return 1 end
|
||||
g_farmlandManager = {
|
||||
getFarmlandIdAtWorldPosition = function(_, x, z) return x < 0 and 69 or 70 end,
|
||||
getFarmlandOwner = function(_, id) return id == 69 and 1 or 0 end,
|
||||
}
|
||||
local bga = {rootNode = {x = -1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
|
||||
local bgaOther = {rootNode = {x = 1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
|
||||
local dairy = {rootNode = {x = -3173, z = -4028}, spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}
|
||||
check("P-land BGA på egen jord = egen", ADPrices.isOwnedBy(bga, 1), true)
|
||||
check("P-land BGA på fremmed jord = ikke egen", ADPrices.isOwnedBy(bgaOther, 1), false)
|
||||
check("P-land mejeri (ejer 0, ikke jord-bundet) = ikke egen", ADPrices.isOwnedBy(dairy, 1), false)
|
||||
check("P-land egen ejer", ADPrices.isOwnedBy({getOwnerFarmId = function() return 1 end}, 1), true)
|
||||
check("P-land BGA er ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = bga}), false)
|
||||
g_farmlandManager = nil
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,326 +0,0 @@
|
||||
-- Kør: luajit tests/test_adRuns.lua (fra repo-roden)
|
||||
-- Tester run-modellen (rene funktioner) og XML-laget mod en XMLFile-mock.
|
||||
|
||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||
|
||||
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
|
||||
|
||||
-- R1: opret run -> løbenummer, standardværdier; input-store røres ikke
|
||||
local empty = ADRuns.newStore()
|
||||
local s1, cow = ADRuns.createRun(empty, "Cow Feed")
|
||||
check("R1 id 1", cow.id, 1)
|
||||
check("R1 navn", cow.name, "Cow Feed")
|
||||
check("R1 auto-varer som standard", cow.ingredientMode, "auto")
|
||||
check("R1 loops 0", cow.loops, 0)
|
||||
check("R1 tom Wait-pulje", cow.waitPoolGroup, "")
|
||||
check("R1 input-store urørt", #empty.runs, 0)
|
||||
check("R1 nextId tæller op", s1.nextId, 2)
|
||||
local s2, pig = ADRuns.createRun(s1, "Pig Feed")
|
||||
check("R1 andet run får id 2", pig.id, 2)
|
||||
check("R1 to runs", #s2.runs, 2)
|
||||
|
||||
-- R2: opdatér felter; ukendt mode ignoreres; loops klippes
|
||||
local s3 = ADRuns.updateRun(s2, 1, {name = "Cow 1", targetWayPointId = 94, ingredientMode = "manual", ingredients = {"STRAW", "FORAGE"}, waitPoolGroup = "Cow Wait", loops = 150})
|
||||
local run1 = ADRuns.getRun(s3, 1)
|
||||
check("R2 navn", run1.name, "Cow 1")
|
||||
check("R2 mål", run1.targetWayPointId, 94)
|
||||
check("R2 manual", run1.ingredientMode, "manual")
|
||||
check("R2 varer", table.concat(run1.ingredients, ","), "STRAW,FORAGE")
|
||||
check("R2 pulje", run1.waitPoolGroup, "Cow Wait")
|
||||
check("R2 loops klippet til 99", run1.loops, 99)
|
||||
check("R2 gammel store urørt", ADRuns.getRun(s2, 1).name, "Cow Feed")
|
||||
local s3b = ADRuns.updateRun(s3, 1, {ingredientMode = "bogus", loops = -5})
|
||||
check("R2 ukendt mode ignoreres", ADRuns.getRun(s3b, 1).ingredientMode, "manual")
|
||||
check("R2 loops under 0 -> 0", ADRuns.getRun(s3b, 1).loops, 0)
|
||||
check("R2 ukendt run -> uændret", #ADRuns.updateRun(s3, 99, {name = "x"}).runs, 2)
|
||||
check("R2 targetWayPointId=false rydder målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {targetWayPointId = false}), 1).targetWayPointId, nil)
|
||||
check("R2 nil rører ikke målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {name = "y"}), 1).targetWayPointId, 94)
|
||||
|
||||
-- R3: tildel traktor; samme traktor flyttes mellem runs
|
||||
local s4, prev = ADRuns.assignVehicle(s3, 1, "vehicleA")
|
||||
check("R3 tildelt", ADRuns.getRun(s4, 1).vehicleIds[1], "vehicleA")
|
||||
check("R3 ingen tidligere", prev, nil)
|
||||
check("R3 opslag", ADRuns.findRunByVehicle(s4, "vehicleA").id, 1)
|
||||
local s5, prev2 = ADRuns.assignVehicle(s4, 2, "vehicleA")
|
||||
check("R3 flyttet fra run 1", prev2, 1)
|
||||
check("R3 væk fra run 1", #ADRuns.getRun(s5, 1).vehicleIds, 0)
|
||||
check("R3 i run 2", ADRuns.getRun(s5, 2).vehicleIds[1], "vehicleA")
|
||||
local s5b, prev3 = ADRuns.assignVehicle(s5, 2, "vehicleA")
|
||||
check("R3 gen-tildeling samme run -> ingen dublet", #ADRuns.getRun(s5b, 2).vehicleIds, 1)
|
||||
check("R3 gen-tildeling -> ingen 'flyttet fra'", prev3, nil)
|
||||
local s6 = ADRuns.unassignVehicle(s5, "vehicleA")
|
||||
check("R3 fjernet", ADRuns.findRunByVehicle(s6, "vehicleA"), nil)
|
||||
check("R3 ukendt run ved tildeling -> uændret", ADRuns.findRunByVehicle(ADRuns.assignVehicle(s6, 42, "vehicleB"), "vehicleB"), nil)
|
||||
|
||||
-- R4: slet run
|
||||
local s7 = ADRuns.deleteRun(s5, 1)
|
||||
check("R4 ét run tilbage", #s7.runs, 1)
|
||||
check("R4 det rigtige", s7.runs[1].id, 2)
|
||||
check("R4 nextId bevares", s7.nextId, 3)
|
||||
|
||||
-- R5: records frem og tilbage
|
||||
local records = ADRuns.toRecords(s5)
|
||||
check("R5 target som tal", records[1].target, 94)
|
||||
check("R5 varer som streng", records[1].ingredients, "STRAW FORAGE")
|
||||
check("R5 køretøjer", records[2].vehicleIds[1], "vehicleA")
|
||||
local back = ADRuns.fromRecords(records)
|
||||
check("R5 to runs tilbage", #back.runs, 2)
|
||||
check("R5 nextId = højeste + 1", back.nextId, 3)
|
||||
check("R5 varer parset", ADRuns.getRun(back, 1).ingredients[2], "FORAGE")
|
||||
check("R5 pulje", ADRuns.getRun(back, 1).waitPoolGroup, "Cow Wait")
|
||||
check("R5 target 0 -> nil", ADRuns.getRun(back, 2).targetWayPointId, nil)
|
||||
check("R5 køretøj", ADRuns.findRunByVehicle(back, "vehicleA").id, 2)
|
||||
|
||||
-- R6: defekte poster springes over; dubletter af id ignoreres
|
||||
local dirty = ADRuns.fromRecords({
|
||||
{id = "x", name = "Broken"},
|
||||
{id = 5, name = "", },
|
||||
{id = 7, name = "Ok", target = "12", ingredientMode = "weird", ingredients = "", waitPool = nil, loops = "3", vehicleIds = {}},
|
||||
{id = 7, name = "Dublet"},
|
||||
})
|
||||
check("R6 kun den gyldige", #dirty.runs, 1)
|
||||
check("R6 target parses fra streng", dirty.runs[1].targetWayPointId, 12)
|
||||
check("R6 ukendt mode -> auto", dirty.runs[1].ingredientMode, "auto")
|
||||
check("R6 tom varestreng -> tom liste", #dirty.runs[1].ingredients, 0)
|
||||
check("R6 manglende pulje -> tom streng", dirty.runs[1].waitPoolGroup, "")
|
||||
check("R6 loops fra streng", dirty.runs[1].loops, 3)
|
||||
check("R6 nextId", dirty.nextId, 8)
|
||||
check("R6 tom liste", #ADRuns.fromRecords(nil).runs, 0)
|
||||
check("R6 parseIngredients trimmer", #ADRuns.parseIngredients(" STRAW FORAGE "), 2)
|
||||
|
||||
-- R7: "kør hjem under X %" — standard 50, klippes til 10..90, overlever records
|
||||
local r7, r7run = ADRuns.createRun(ADRuns.newStore(), "Tærskel")
|
||||
check("R7 standard 50 %", r7run.returnBelowPercent, 50)
|
||||
check("R7 sættes", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 30}), r7run.id).returnBelowPercent, 30)
|
||||
check("R7 klippes til 90", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 150}), r7run.id).returnBelowPercent, 90)
|
||||
check("R7 klippes til 10", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 0}), r7run.id).returnBelowPercent, 10)
|
||||
local r7rec = ADRuns.toRecords(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 25}))
|
||||
check("R7 i records", r7rec[1].returnBelow, 25)
|
||||
check("R7 tilbage fra records", ADRuns.fromRecords(r7rec).runs[1].returnBelowPercent, 25)
|
||||
check("R7 manglende i gammel fil -> 50", ADRuns.fromRecords({{id = 1, name = "Gammel"}}).runs[1].returnBelowPercent, 50)
|
||||
|
||||
-- R8: flere mål pr. run — liste + første som "primært" mål (bagudkompatibelt); toggle; records
|
||||
local r8, r8run = ADRuns.createRun(ADRuns.newStore(), "Kvæg")
|
||||
check("R8 ingen mål", #r8run.targetWayPointIds, 0)
|
||||
r8 = ADRuns.updateRun(r8, r8run.id, {targetWayPointIds = {94, 99}})
|
||||
check("R8 to mål", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "94,99")
|
||||
check("R8 primært mål = første", ADRuns.getRun(r8, r8run.id).targetWayPointId, 94)
|
||||
r8 = ADRuns.toggleTarget(r8, r8run.id, 94)
|
||||
check("R8 toggle fjerner", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99")
|
||||
check("R8 primært følger med", ADRuns.getRun(r8, r8run.id).targetWayPointId, 99)
|
||||
r8 = ADRuns.toggleTarget(r8, r8run.id, 120)
|
||||
check("R8 toggle tilføjer", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99,120")
|
||||
check("R8 gammel enkelt-sætning = liste med ét", table.concat(ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = 7}), r8run.id).targetWayPointIds, ","), "7")
|
||||
check("R8 false rydder listen", #ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = false}), r8run.id).targetWayPointIds, 0)
|
||||
local r8rec = ADRuns.toRecords(r8)
|
||||
check("R8 records: targets-streng", r8rec[1].targets, "99 120")
|
||||
check("R8 records: target = første (gammelt felt)", r8rec[1].target, 99)
|
||||
check("R8 tilbage fra records", table.concat(ADRuns.fromRecords(r8rec).runs[1].targetWayPointIds, ","), "99,120")
|
||||
check("R8 gammel fil med kun target", table.concat(ADRuns.fromRecords({{id = 1, name = "Gl", target = 55}}).runs[1].targetWayPointIds, ","), "55")
|
||||
|
||||
-- ==== XML-laget mod en flad XMLFile-mock (nøgle -> værdi) ====
|
||||
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
|
||||
|
||||
local function makeXmlMock(values)
|
||||
local xml = {values = values or {}, saved = false, deleted = false}
|
||||
xml.setInt = function(self, key, value) self.values[key] = value end
|
||||
xml.setString = function(self, key, value) self.values[key] = value end
|
||||
xml.getInt = function(self, key) return tonumber(self.values[key]) end
|
||||
xml.getString = function(self, key) local v = self.values[key]; return v ~= nil and tostring(v) or nil end
|
||||
-- iterate: kalder callback for "prefix(0)", "prefix(1)", ... så længe en nøgle med den stamme findes
|
||||
xml.iterate = function(self, prefix, callback)
|
||||
local index = 0
|
||||
while true do
|
||||
local key = string.format("%s(%d)", prefix, index)
|
||||
local found = false
|
||||
for k in pairs(self.values) do
|
||||
if k:sub(1, #key) == key then found = true break end
|
||||
end
|
||||
if not found then return end
|
||||
callback(index + 1, key)
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
xml.save = function(self) self.saved = true end
|
||||
xml.delete = function(self) self.deleted = true end
|
||||
return xml
|
||||
end
|
||||
|
||||
-- X1: records skrives som attributter + vehicle-underelementer
|
||||
local out = makeXmlMock()
|
||||
ADRunsStorage.writeRecords(out, ADRuns.toRecords(s5))
|
||||
check("X1 version", out.values["ADSmartPickupRuns#version"], 3)
|
||||
check("X1 run 0 navn", out.values["ADSmartPickupRuns.run(0)#name"], "Cow 1")
|
||||
check("X1 run 0 target", out.values["ADSmartPickupRuns.run(0)#target"], 94)
|
||||
check("X1 run 0 varer", out.values["ADSmartPickupRuns.run(0)#ingredients"], "STRAW FORAGE")
|
||||
check("X1 run 0 pulje", out.values["ADSmartPickupRuns.run(0)#waitPool"], "Cow Wait")
|
||||
check("X1 run 1 køretøj", out.values["ADSmartPickupRuns.run(1).vehicle(0)#id"], "vehicleA")
|
||||
|
||||
-- X2: samme data læses tilbage
|
||||
local records2 = ADRunsStorage.readRecords(makeXmlMock(out.values))
|
||||
check("X2 to poster", #records2, 2)
|
||||
check("X2 id", records2[1].id, 1)
|
||||
check("X2 køretøj", records2[2].vehicleIds[1], "vehicleA")
|
||||
check("X2 loops", records2[1].loops, 99)
|
||||
check("X2 round-trip", ADRuns.findRunByVehicle(ADRuns.fromRecords(records2), "vehicleA").id, 2)
|
||||
|
||||
-- X3: save/load via XMLFile-globalen; ingen savegame-mappe -> tom store, intet skrives
|
||||
Logging = Logging or {warning = function() end, info = function() end}
|
||||
local created, loaded = nil, nil
|
||||
XMLFile = {
|
||||
create = function(_, path, root) created = makeXmlMock(); created.path = path; created.root = root; return created end,
|
||||
load = function(_, path) loaded = makeXmlMock(created and created.values or {}); loaded.path = path; return loaded end,
|
||||
}
|
||||
fileExists = function(path) return created ~= nil and created.path == path end
|
||||
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg1"}}
|
||||
check("X3 gemt", ADRunsStorage.save(s5), true)
|
||||
check("X3 sti", created.path, "/tmp/sg1/ADSmartPickup_runs.xml")
|
||||
check("X3 rodelement", created.root, "ADSmartPickupRuns")
|
||||
check("X3 save+delete kaldt", created.saved and created.deleted, true)
|
||||
check("X3 indlæst", #ADRunsStorage.load().runs, 2)
|
||||
check("X3 læse-handle lukket", loaded.deleted, true)
|
||||
g_currentMission = {missionInfo = {}}
|
||||
check("X3 uden savegame -> false", ADRunsStorage.save(s5), false)
|
||||
check("X3 uden savegame -> tom store", #ADRunsStorage.load().runs, 0)
|
||||
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg-ny"}}
|
||||
check("X3 fil mangler -> tom store", #ADRunsStorage.load().runs, 0)
|
||||
|
||||
-- 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")
|
||||
local _, plain = ADRuns.createRun(ADRuns.newStore(), "x")
|
||||
check("Rv2 supply som standard", plain.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)
|
||||
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)
|
||||
-- XML-laget skriver/læser v2-felterne
|
||||
do
|
||||
local written = {}
|
||||
local xml = {
|
||||
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
|
||||
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
|
||||
iterate = function(_, prefix, fn)
|
||||
local i = 0
|
||||
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
|
||||
end,
|
||||
}
|
||||
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(o4))
|
||||
check("Rv2 xml version 2", written["ADSmartPickupRuns#version"], 3)
|
||||
check("Rv2 xml kind", written["ADSmartPickupRuns.run(0)#kind"], "outbound")
|
||||
check("Rv2 xml sources", written["ADSmartPickupRuns.run(0)#sources"], "12694 12386")
|
||||
check("Rv2 xml products", written["ADSmartPickupRuns.run(0)#products"], "SUGAR:sell FORAGE:store")
|
||||
check("Rv2 xml collectRound", written["ADSmartPickupRuns.run(0)#collectRound"], 1)
|
||||
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
|
||||
check("Rv2 xml læs sellAt", ADRuns.getRun(back, out.id).sellAtPercent, 100)
|
||||
check("Rv2 xml læs runde", ADRuns.getRun(back, out.id).collectRound, true)
|
||||
end
|
||||
|
||||
-- R-v3: bygninger som kilder/mål (id-lister); XML v3; v2-fil uden bygninger giver tomme lister
|
||||
local b1, flow = ADRuns.createRun(ADRuns.newStore(), "Milk", "outbound")
|
||||
check("Rv3 tomme kilde-bygninger", #flow.sourceBuildingIds, 0)
|
||||
check("Rv3 tomme mål-bygninger", #flow.targetBuildingIds, 0)
|
||||
local b2 = ADRuns.toggleSourceBuilding(b1, flow.id, "barn1")
|
||||
b2 = ADRuns.toggleSourceBuilding(b2, flow.id, "barn2")
|
||||
check("Rv3 to kilde-bygninger", table.concat(ADRuns.getRun(b2, flow.id).sourceBuildingIds, ","), "barn1,barn2")
|
||||
check("Rv3 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSourceBuilding(b2, flow.id, "barn1"), flow.id).sourceBuildingIds, 1)
|
||||
local b3 = ADRuns.toggleTargetBuilding(b2, flow.id, "mill")
|
||||
check("Rv3 mål-bygning", ADRuns.getRun(b3, flow.id).targetBuildingIds[1], "mill")
|
||||
check("Rv3 gammel store urørt", #ADRuns.getRun(b2, flow.id).targetBuildingIds, 0)
|
||||
local b4 = ADRuns.updateRun(b3, flow.id, {sourceBuildingIds = {"shed"}, targetBuildingIds = {}})
|
||||
check("Rv3 updateRun sætter lister", ADRuns.getRun(b4, flow.id).sourceBuildingIds[1], "shed")
|
||||
check("Rv3 updateRun tømmer", #ADRuns.getRun(b4, flow.id).targetBuildingIds, 0)
|
||||
local rt3 = ADRuns.fromRecords(ADRuns.toRecords(b3))
|
||||
check("Rv3 roundtrip kilder", table.concat(ADRuns.getRun(rt3, flow.id).sourceBuildingIds, ","), "barn1,barn2")
|
||||
check("Rv3 roundtrip mål", ADRuns.getRun(rt3, flow.id).targetBuildingIds[1], "mill")
|
||||
check("Rv3 XML_VERSION", ADRuns.XML_VERSION, 3)
|
||||
local v2 = ADRuns.fromRecords({{id = 3, name = "V2", kind = "outbound", sources = "12694", products = "SUGAR:sell", vehicleIds = {}}})
|
||||
check("Rv3 v2 rå markør bevares", ADRuns.getRun(v2, 3).sourceWayPointIds[1], 12694)
|
||||
check("Rv3 v2 ingen bygninger", #ADRuns.getRun(v2, 3).sourceBuildingIds, 0)
|
||||
do
|
||||
local written = {}
|
||||
local xml = {
|
||||
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
|
||||
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
|
||||
iterate = function(_, prefix, fn)
|
||||
local i = 0
|
||||
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
|
||||
end,
|
||||
}
|
||||
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(b3))
|
||||
check("Rv3 xml version 3", written["ADSmartPickupRuns#version"], 3)
|
||||
check("Rv3 xml sourceBuildings", written["ADSmartPickupRuns.run(0)#sourceBuildings"], "barn1 barn2")
|
||||
check("Rv3 xml targetBuildings", written["ADSmartPickupRuns.run(0)#targetBuildings"], "mill")
|
||||
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
|
||||
check("Rv3 xml læs kilder", table.concat(ADRuns.getRun(back, flow.id).sourceBuildingIds, ","), "barn1,barn2")
|
||||
end
|
||||
|
||||
-- RN: køretøjsnavne efter forløbet — "Navn 1", "Navn 2" … i medlemsrækkefølge; ét køretøj = bare navnet
|
||||
do
|
||||
local store, runA = ADRuns.createRun(ADRuns.newStore(), "Milk Collect", "outbound")
|
||||
store = ADRuns.assignVehicle(store, runA.id, "v1")
|
||||
store = ADRuns.assignVehicle(store, runA.id, "v2")
|
||||
local runB
|
||||
store, runB = ADRuns.createRun(store, "Milk Salg", "outbound")
|
||||
store = ADRuns.assignVehicle(store, runB.id, "v3")
|
||||
local names = ADRuns.vehicleNames(store)
|
||||
check("RN to i samme forløb: 1", names.v1, "Milk Collect 1")
|
||||
check("RN to i samme forløb: 2", names.v2, "Milk Collect 2")
|
||||
check("RN alene i forløb = navnet", names.v3, "Milk Salg")
|
||||
check("RN ukendt køretøj", names.v9, nil)
|
||||
end
|
||||
|
||||
-- RS: tvunget lagermål — storeBuildingIds (toggle, kopi, records roundtrip)
|
||||
do
|
||||
local store, run = ADRuns.createRun(ADRuns.newStore(), "Slurry", "outbound")
|
||||
check("RS tom som standard", #run.storeBuildingIds, 0)
|
||||
store = ADRuns.toggleStoreBuilding(store, run.id, "BGA500kw")
|
||||
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
|
||||
check("RS to valgt", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw,tank2")
|
||||
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
|
||||
check("RS fravalgt igen", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw")
|
||||
local records = ADRuns.toRecords(store)
|
||||
check("RS record", records[1].storeBuildings, "BGA500kw")
|
||||
local back = ADRuns.fromRecords(records)
|
||||
check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw")
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,804 +0,0 @@
|
||||
-- Kør: luajit tests/test_adRunsController.lua (fra repo-roden)
|
||||
-- Controlleren oversætter et run til AutoDrive-tilstand pr. traktor. Mock'er AD + de
|
||||
-- få ADSmartPickup-opslag den bruger.
|
||||
|
||||
local STRAW, TMR, WATER, MILK = 31, 116, 8, 30
|
||||
local FILL_TYPE_NAMES = {[STRAW] = "STRAW", [TMR] = "FORAGE", [WATER] = "WATER", [MILK] = "MILK"}
|
||||
local FILL_TYPE_INDEX = {}
|
||||
for index, name in pairs(FILL_TYPE_NAMES) do FILL_TYPE_INDEX[name] = index end
|
||||
g_fillTypeManager = {
|
||||
getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end,
|
||||
getFillTypeIndexByName = function(_, name) return FILL_TYPE_INDEX[name] end,
|
||||
getFillTypes = function()
|
||||
return {
|
||||
{index = STRAW, name = "STRAW", title = "Halm"},
|
||||
{index = TMR, name = "FORAGE", title = "TMR"},
|
||||
{index = WATER, name = "WATER", title = "Vand"},
|
||||
{index = MILK, name = "MILK", title = "Mælk"},
|
||||
}
|
||||
end,
|
||||
}
|
||||
local logLines = {}
|
||||
Logging = {
|
||||
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
|
||||
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
|
||||
}
|
||||
Utils = {prependedFunction = function(original, extra) return function(...) extra(...); return original(...) end end}
|
||||
ItemSystem = {save = function() end}
|
||||
g_currentMission = {getFarmId = function() return 1 end, missionInfo = {}}
|
||||
|
||||
-- ---- AutoDrive-mock ----
|
||||
local RT_NONE, RT_ONLYPICKUP = 1, 2
|
||||
local MODE_PICKUPANDDELIVER = 2
|
||||
local vehicles, markers, wayPoints = {}, {}, {}
|
||||
FS25_AutoDrive = {
|
||||
AutoDrive = {
|
||||
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP, MODE_PICKUPANDDELIVER = MODE_PICKUPANDDELIVER,
|
||||
settings = {
|
||||
useFolders = {values = {false, true}, current = 1, isVehicleSpecific = false},
|
||||
rotateTargets = {values = {1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
|
||||
ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
|
||||
},
|
||||
getAllVehicles = function() return vehicles end,
|
||||
getAllUnits = function(vehicle) return vehicle.trailers end,
|
||||
getAllFillLevels = function(trailers) return trailers.level or 0, trailers.capacity or 0, false, 0 end,
|
||||
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable or {} end,
|
||||
},
|
||||
ADGraphManager = {
|
||||
getMapMarkers = function() return markers end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getDistanceFromNetwork = function(_, vehicle) return vehicle.networkDistance or 0 end,
|
||||
},
|
||||
}
|
||||
|
||||
local function addMarker(id, name, group, x, z)
|
||||
wayPoints[id] = {x = x, z = z, incoming = {}}
|
||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
||||
end
|
||||
|
||||
-- ADSmartPickup-opslag: hvilke waypoints har hvilken slags station, og hvad målet tager
|
||||
local unloadAt, loadAt = {}, {}
|
||||
ADSmartPickup = {
|
||||
getFarmId = function(vehicle) return vehicle.farmId end,
|
||||
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
|
||||
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
|
||||
getStationLevelAndCapacity = function(station, fillType)
|
||||
if station.accepts[fillType] == nil then return nil end
|
||||
return (station.levels or {})[fillType] or 0, station.accepts[fillType]
|
||||
end,
|
||||
-- forenklet rangering: mest fri plads (relativt) først; fulde sidst
|
||||
getTargetNeedsRanked = function(station, fillTypes)
|
||||
local ranked = {}
|
||||
for _, fillType in ipairs(fillTypes) do
|
||||
local capacity = station.accepts[fillType]
|
||||
if capacity ~= nil then
|
||||
local level = (station.levels or {})[fillType] or 0
|
||||
table.insert(ranked, {fillType = fillType, level = level, capacity = capacity, freeSpace = capacity - level, score = (capacity - level) / capacity})
|
||||
end
|
||||
end
|
||||
table.sort(ranked, function(a, b) return a.score > b.score end)
|
||||
return ranked
|
||||
end,
|
||||
}
|
||||
ADSupplyPlanner = {isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end}
|
||||
|
||||
local function makeVehicle(uniqueId, name, carriable, capacity, x, z)
|
||||
local stateModule = {selectedFillTypes = {}, fillType = 0, active = false, loopCounter = 5, loopsDone = 3, dirtyCount = 0}
|
||||
stateModule.setMode = function(self, mode) self.mode = mode end
|
||||
stateModule.setFirstMarkerByWayPointId = function(self, id) self.firstMarkerId = id end
|
||||
stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
|
||||
stateModule.setFillType = function(self, fillType) self.fillType = fillType end
|
||||
stateModule.raiseDirtyFlag = function(self) self.dirtyCount = self.dirtyCount + 1 end
|
||||
stateModule.setLoopsDone = function(self, n) self.loopsDone = n end
|
||||
stateModule.isActive = function(self) return self.active end
|
||||
-- Bruges af C13: getTargetFillTypes skal følge AD's LIVE mål (mock'er AD's eget getSecondMarker).
|
||||
stateModule.getSecondMarker = function(self) return self.secondMarkerId ~= nil and {id = self.secondMarkerId} or nil end
|
||||
local vehicle = {
|
||||
uniqueId = uniqueId, farmId = 1, rootNode = uniqueId,
|
||||
carriable = carriable, trailers = {level = 0, capacity = capacity},
|
||||
getName = function() return name end,
|
||||
getOwnerFarmId = function() return 1 end,
|
||||
startAutoDrive = function(self) self.ad.stateModule.active = true; self.started = (self.started or 0) + 1 end,
|
||||
getClosestWayPoint = function() return nil, 0 end,
|
||||
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = (self.stopped or 0) + 1 end,
|
||||
ad = {stateModule = stateModule, settings = {rotateTargets = {values = {1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}, ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}}},
|
||||
}
|
||||
nodePositions = nodePositions or {}
|
||||
nodePositions[uniqueId] = {x = x or 0, z = z or 0}
|
||||
table.insert(vehicles, vehicle)
|
||||
return vehicle
|
||||
end
|
||||
function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end
|
||||
|
||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
|
||||
dofile("FS25_ADSmartPickup/adWaitPool.lua")
|
||||
dofile("FS25_ADSmartPickup/adRunsController.lua")
|
||||
local adEnv = FS25_AutoDrive
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then
|
||||
print("OK " .. name)
|
||||
else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local function buildWorld()
|
||||
vehicles, markers, wayPoints, unloadAt, loadAt, logLines = {}, {}, {}, {}, {}, {}
|
||||
ADRunsController.store = ADRuns.newStore()
|
||||
addMarker(94, "Cow 1 Food", "Cow Food", 5000, 0); unloadAt[94] = {accepts = {[STRAW] = 300000, [TMR] = 300000, [WATER] = 50000}}
|
||||
addMarker(44, "10 Silo Storage", "Siloer", 0, 0); loadAt[44] = {}
|
||||
addMarker(75, "TMR Load", "Fabrication", 900, 0); loadAt[75] = {}
|
||||
addMarker(100, "Cow Wait", "Cow Wait", 5100, 0)
|
||||
addMarker(101, "Cow Wait", "Cow Wait", 5200, 0)
|
||||
addMarker(200, "F91 Wait", "F91", 8000, 0)
|
||||
addMarker(300, "Pig Wait", "Pig Wait", 9000, 0)
|
||||
end
|
||||
|
||||
-- C1: køretøjs-id og farmens AD-køretøjer
|
||||
buildWorld()
|
||||
local fastracA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000, 100, 0)
|
||||
local fastracB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000, 4000, 0)
|
||||
local noAd = {uniqueId = "vehicleC", getOwnerFarmId = function() return 1 end}
|
||||
table.insert(vehicles, noAd)
|
||||
local otherFarm = makeVehicle("vehicleD", "Nabo", {STRAW}, 1000, 0, 0); otherFarm.getOwnerFarmId = function() return 2 end
|
||||
check("C1 uniqueId", ADRunsController.getVehicleId(fastracA), "vehicleA")
|
||||
check("C1 fallback savegame-id", ADRunsController.getVehicleId({currentSavegameId = 7}), "7")
|
||||
check("C1 intet id -> nil", ADRunsController.getVehicleId({}), nil)
|
||||
check("C1 farmens AD-køretøjer", #ADRunsController.getFarmVehicles(adEnv), 2)
|
||||
check("C1 opslag på id", ADRunsController.findVehicleById(adEnv, "vehicleB"), fastracB)
|
||||
check("C1 ukendt id -> nil", ADRunsController.findVehicleById(adEnv, "nope"), nil)
|
||||
|
||||
-- C2: opret run, tildel; opslag run for køretøj; flyt mellem runs
|
||||
local cow = ADRunsController.createRun("Cow Feed")
|
||||
check("C2 run oprettet", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
|
||||
check("C2 ingen tidligere", ADRunsController.assignVehicle(cow.id, fastracA), nil)
|
||||
check("C2 run for køretøj", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
|
||||
check("C2 ikke tildelt -> nil", ADRunsController.getRunForVehicle(fastracB), nil)
|
||||
local pig = ADRunsController.createRun("Pig Feed")
|
||||
check("C2 flyttet fra Cow", ADRunsController.assignVehicle(pig.id, fastracA), cow.id)
|
||||
check("C2 nu i Pig", ADRunsController.getRunForVehicle(fastracA).id, pig.id)
|
||||
ADRunsController.unassignVehicle(fastracA)
|
||||
check("C2 fjernet", ADRunsController.getRunForVehicle(fastracA), nil)
|
||||
ADRunsController.deleteRun(pig.id)
|
||||
check("C2 slettet", ADRuns.getRun(ADRunsController.store, pig.id), nil)
|
||||
|
||||
-- C3: Auto-vareliste = vognens varer ∩ målets varer; manual = navne -> indeks (ukendte droppes)
|
||||
ADRunsController.updateRun(cow.id, {targetWayPointId = 94})
|
||||
local cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
|
||||
check("C3 auto: halm+TMR (vand kan vognen ikke bære)", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), STRAW .. "," .. TMR)
|
||||
check("C3 auto: kun halm for halm-vogn", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracB), ","), tostring(STRAW))
|
||||
ADRunsController.updateRun(cow.id, {ingredientMode = "manual", ingredients = {"FORAGE", "UNKNOWN_THING"}})
|
||||
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
|
||||
check("C3 manual: FORAGE -> indeks, ukendt droppes", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), tostring(TMR))
|
||||
ADRunsController.updateRun(cow.id, {ingredientMode = "auto"})
|
||||
local noTarget = ADRunsController.createRun("Uden mål")
|
||||
check("C3 uden mål -> tom liste", #ADRunsController.getRunIngredients(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA), 0)
|
||||
|
||||
-- C4: validering
|
||||
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
|
||||
check("C4 ok", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).ok, true)
|
||||
check("C4 kapacitet", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).capacity, 1000000)
|
||||
local noTrailer = makeVehicle("vehicleE", "Uden vogn", {}, 0)
|
||||
check("C4 ingen vogn", ADRunsController.validateVehicle(adEnv, cowRun, noTrailer).reason, "noTrailer")
|
||||
local milkTruck = makeVehicle("vehicleF", "Mælkebil", {MILK}, 20000)
|
||||
check("C4 kan ikke bære", ADRunsController.validateVehicle(adEnv, cowRun, milkTruck).reason, "cannotCarry")
|
||||
fastracB.networkDistance = 31
|
||||
check("C4 for langt fra vejnettet", ADRunsController.validateVehicle(adEnv, cowRun, fastracB).reason, "farFromNetwork")
|
||||
fastracB.networkDistance = 0
|
||||
check("C4 intet mål", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
|
||||
ADRunsController.updateRun(noTarget.id, {targetWayPointId = 4242})
|
||||
check("C4 mål-markør findes ikke", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
|
||||
|
||||
-- C5: Start skriver AD-tilstand og starter; ✖-traktorer springes over
|
||||
ADRunsController.updateRun(cow.id, {loops = 4, waitPoolGroup = "Cow Wait"})
|
||||
ADRunsController.assignVehicle(cow.id, fastracA)
|
||||
ADRunsController.assignVehicle(cow.id, noTrailer)
|
||||
local results = ADRunsController.startRun(adEnv, cow.id)
|
||||
check("C5 to resultater", #results, 2)
|
||||
check("C5 A startet", fastracA.started, 1)
|
||||
check("C5 A ok", results[1].ok, true)
|
||||
check("C5 uden vogn sprunget over", noTrailer.started, nil)
|
||||
check("C5 uden vogn grund", results[2].reason, "noTrailer")
|
||||
local state = fastracA.ad.stateModule
|
||||
check("C5 mode P&D", state.mode, MODE_PICKUPANDDELIVER)
|
||||
check("C5 mål = run'ets mål", state.secondMarkerId, 94)
|
||||
check("C5 start-pickup = nærmeste læssestation", state.firstMarkerId, 44)
|
||||
check("C5 varer = auto-listen", table.concat(state.selectedFillTypes, ","), STRAW .. "," .. TMR)
|
||||
check("C5 fillType = første", state.fillType, STRAW)
|
||||
check("C5 loops", state.loopCounter, 4)
|
||||
check("C5 loopsDone nulstillet", state.loopsDone, 0)
|
||||
check("C5 rotateTargets = kun pålæsning (current)", fastracA.ad.settings.rotateTargets.current, 2)
|
||||
check("C5 rotateTargets = kun pålæsning (new)", fastracA.ad.settings.rotateTargets.new, 2)
|
||||
check("C5 useFolders globalt til", adEnv.AutoDrive.settings.useFolders.current, 2)
|
||||
check("C5 dirty flag", state.dirtyCount > 0, true)
|
||||
check("C5 status", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
|
||||
check("C5 total", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).total, 2)
|
||||
check("C5 ukendt run -> tom", #ADRunsController.startRun(adEnv, 999), 0)
|
||||
|
||||
-- C6: Stop
|
||||
check("C6 stoppede", ADRunsController.stopRun(adEnv, cow.id), 1)
|
||||
check("C6 A stoppet", fastracA.stopped, 1)
|
||||
check("C6 status 0", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 0)
|
||||
check("C6 medlemskab bevaret", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
|
||||
|
||||
-- C7: Start på traktor der allerede kører AD (fx startet manuelt) -> stoppes og startes på run'et
|
||||
fastracA.ad.stateModule.active = true
|
||||
fastracA.stopped = 0
|
||||
ADRunsController.startRun(adEnv, cow.id)
|
||||
check("C7 kørende traktor stoppes først", fastracA.stopped, 1)
|
||||
check("C7 og startes på run'et", fastracA.started, 2)
|
||||
check("C7 run markeret som kørende", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, true)
|
||||
ADRunsController.stopRun(adEnv, cow.id)
|
||||
check("C7 stop rydder markeringen", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
|
||||
-- en traktor der kører AD uden at run'et er startet af os, gør IKKE run'et "kørende"
|
||||
fastracA.ad.stateModule.active = true
|
||||
check("C7 manuel AD tæller ikke som kørende run", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
|
||||
check("C7 men aktive tæller stadig", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
|
||||
fastracA.ad.stateModule.active = false
|
||||
-- sprungne traktorer logges med årsag
|
||||
logLines = {}
|
||||
ADRunsController.startRun(adEnv, cow.id)
|
||||
local skippedLogged = false
|
||||
for _, line in ipairs(logLines) do if line:find("sprunget over", 1, true) and line:find("noTrailer", 1, true) then skippedLogged = true end end
|
||||
check("C7 sprunget over logges med årsag", skippedLogged, true)
|
||||
-- kapacitet under 100 l (kun trækkerens egne tanke) = ingen vogn
|
||||
local tinyCapacity = makeVehicle("vehicleTiny", "T255", {STRAW, TMR}, 1, 100, 0)
|
||||
check("C7 1 l kapacitet -> noTrailer", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), tinyCapacity).reason, "noTrailer")
|
||||
-- UAL-køretøj: AutoDrive melder kapacitet i PALLER (læssede + 1), så en tom pallebil har "1" — det er ikke "ingen vogn"
|
||||
adEnv.AutoDrive.hasAL = function(_, unit) return unit.ual == true end
|
||||
local palletTruck = makeVehicle("vehicleDragon", "Dragon", {}, 1, 100, 0)
|
||||
palletTruck.trailers[1] = {ual = true}
|
||||
local truckValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), palletTruck)
|
||||
check("C7 pallebil: ikke noTrailer", truckValidation.reason ~= "noTrailer", true)
|
||||
check("C7 pallebil: kapacitet i paller markeret", truckValidation.isAutoload, true)
|
||||
check("C7 pallebil: liter-kapacitet ukendt", truckValidation.capacity, nil)
|
||||
adEnv.AutoDrive.hasAL = nil
|
||||
|
||||
-- C8: mål-markører (kun stalde og fabrikker — siloer tager alt og er ikke mål; stalde først,
|
||||
-- så fabrikker, så navn) og Wait-puljer til dropdowns
|
||||
unloadAt[94].owningPlaceable = {getHusbandryFillLevel = function() return 0 end}
|
||||
addMarker(96, "8 Silo Storage", "Siloer", 100, 0); unloadAt[96] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_silo = {}}}
|
||||
addMarker(97, "TMR Unload", "Fabrication", 900, 0); unloadAt[97] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_productionPoint = {}}}
|
||||
addMarker(98, "Bull Food", "Cow Food", 5100, 0); unloadAt[98] = {accepts = {[STRAW] = 1}, owningPlaceable = {getHusbandryFillLevel = function() return 0 end}}
|
||||
local targets = ADRunsController.getTargetMarkers(adEnv, 1)
|
||||
check("C8 kun stalde og fabrikker (ikke siloen)", #targets, 3)
|
||||
check("C8 stalde først, alfabetisk", targets[1].name .. "," .. targets[2].name, "Bull Food,Cow 1 Food")
|
||||
check("C8 fabrik sidst", targets[3].name, "TMR Unload")
|
||||
local pools = ADRunsController.getWaitPools(adEnv)
|
||||
check("C8 to puljer (F91 er ikke en Wait-mappe)", #pools, 2)
|
||||
check("C8 sorteret", pools[1].group, "Cow Wait")
|
||||
check("C8 antal", pools[1].count, 2)
|
||||
|
||||
-- C9: gem/indlæs; ukendte køretøjs-id'er logges ved indlæsning men beholdes i run'et
|
||||
local savedRecords
|
||||
ADRunsStorage.save = function(store) savedRecords = ADRuns.toRecords(store); return true end
|
||||
ADRunsStorage.load = function() return ADRuns.fromRecords(savedRecords) end
|
||||
ADRunsController.assignVehicle(cow.id, {uniqueId = "vehicleGONE", ad = {}, getOwnerFarmId = function() return 1 end, getName = function() return "Væk" end})
|
||||
check("C9 gemt", ADRunsController.save(), true)
|
||||
ADRunsController.store = ADRuns.newStore()
|
||||
ADRunsController.load(adEnv)
|
||||
check("C9 indlæst", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
|
||||
check("C9 ukendt køretøj logget", logLines[#logLines]:find("vehicleGONE", 1, true) ~= nil, true)
|
||||
check("C9 run beholder kendte", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
|
||||
local hooked = false
|
||||
ItemSystem.save = function() hooked = true end
|
||||
ADRunsController.installSaveHook()
|
||||
ItemSystem.save()
|
||||
check("C9 save-hook kaldes før ItemSystem.save", hooked, true)
|
||||
|
||||
-- C11: vogn uden egen ad.settings-tabel (ingen rotateTargets pr. køretøj) -> den
|
||||
-- køretøjs-specifikke indstilling skrives IKKE til den globale settings-tabel
|
||||
adEnv.AutoDrive.settings.rotateTargets.current = 1
|
||||
adEnv.AutoDrive.settings.useFolders.current = 1
|
||||
local settingsRun = ADRunsController.createRun("Settings Test")
|
||||
ADRunsController.updateRun(settingsRun.id, {targetWayPointId = 94, loops = 1})
|
||||
local noSettings = makeVehicle("vehicleH", "Uden AD-settings", {STRAW}, 500000, 200, 0)
|
||||
noSettings.ad.settings = {}
|
||||
ADRunsController.assignVehicle(settingsRun.id, noSettings)
|
||||
ADRunsController.startRun(adEnv, settingsRun.id)
|
||||
check("C11 global rotateTargets urørt", adEnv.AutoDrive.settings.rotateTargets.current, 1)
|
||||
check("C11 useFolders global stadig skrevet", adEnv.AutoDrive.settings.useFolders.current, 2)
|
||||
check("C11 køretøjet startet", noSettings.started, 1)
|
||||
|
||||
-- C12: startAutoDrive() kaster for én traktor -> pcall'et applyAndStart fanger fejlen,
|
||||
-- resultatet markeres ok=false/reason="error", og de ØVRIGE traktorer i run'et startes stadig
|
||||
-- (fejlen må ikke undslippe startRun-loopet).
|
||||
buildWorld()
|
||||
local brokenRun = ADRunsController.createRun("Broken Start")
|
||||
ADRunsController.updateRun(brokenRun.id, {targetWayPointId = 94, loops = 2})
|
||||
local broken = makeVehicle("vehicleBroken", "Fejl-traktor", {STRAW, TMR}, 500000, 100, 0)
|
||||
broken.startAutoDrive = function() error("boom") end
|
||||
local normal = makeVehicle("vehicleNormal", "Normal-traktor", {STRAW, TMR}, 500000, 200, 0)
|
||||
ADRunsController.assignVehicle(brokenRun.id, broken)
|
||||
ADRunsController.assignVehicle(brokenRun.id, normal)
|
||||
local c12Results = ADRunsController.startRun(adEnv, brokenRun.id)
|
||||
check("C12 to resultater", #c12Results, 2)
|
||||
check("C12 fejl-traktor ok=false", c12Results[1].ok, false)
|
||||
check("C12 fejl-traktor reason=error", c12Results[1].reason, "error")
|
||||
check("C12 fejl-traktor ikke startet", broken.started, nil)
|
||||
check("C12 normal-traktor ok=true", c12Results[2].ok, true)
|
||||
check("C12 normal-traktor startet", normal.started, 1)
|
||||
|
||||
-- C13: Auto-listen følger traktorens LIVE AD-mål (stateModule:getSecondMarker()) mens den kører,
|
||||
-- IKKE run'ets gemte targetWayPointId — ellers afviger listen fra det choosePickup/findSupplyPickup
|
||||
-- faktisk bruger, hvis brugeren redigerer run'ets Mål mens traktoren kører.
|
||||
addMarker(95, "Alt Cow Food", "Cow Food", 5300, 0)
|
||||
unloadAt[95] = {accepts = {[STRAW] = 1000}}
|
||||
local liveRun = ADRunsController.createRun("Live Target")
|
||||
ADRunsController.updateRun(liveRun.id, {targetWayPointId = 94})
|
||||
local liveRunData = ADRuns.getRun(ADRunsController.store, liveRun.id)
|
||||
local liveVehicle = makeVehicle("vehicleLive", "Live-traktor", {STRAW, TMR}, 500000, 300, 0)
|
||||
check("C13 inaktiv: auto-liste fra run'ets mål (94)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), STRAW .. "," .. TMR)
|
||||
liveVehicle.ad.stateModule.active = true
|
||||
liveVehicle.ad.stateModule.secondMarkerId = 95
|
||||
check("C13 aktiv: auto-liste fra AD's live mål (95)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), tostring(STRAW))
|
||||
liveVehicle.ad.stateModule.active = false
|
||||
liveVehicle.ad.stateModule.secondMarkerId = nil
|
||||
|
||||
-- C10: målets accepterede varer (til afkrydsning i GUI'et) + køretøjs-label
|
||||
buildWorld()
|
||||
local labelA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000)
|
||||
local labelB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000)
|
||||
local accRun = ADRunsController.createRun("Cow Feed")
|
||||
check("C10 uden mål -> tom", #ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id)), 0)
|
||||
ADRunsController.updateRun(accRun.id, {targetWayPointId = 94})
|
||||
local accepted = ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id))
|
||||
check("C10 tre varer (halm, TMR, vand)", #accepted, 3)
|
||||
check("C10 sorteret på titel", accepted[1].title .. "," .. accepted[2].title .. "," .. accepted[3].title, "Halm,TMR,Vand")
|
||||
check("C10 navn med", accepted[1].name, "STRAW")
|
||||
check("C10 label", ADRunsController.getVehicleLabel(adEnv, labelB), "Fastrac B (#2)")
|
||||
|
||||
-- C14: Start går gennem AD's mode:start(USER_PLAYER) (som HUD'en), ikke kun startAutoDrive
|
||||
buildWorld()
|
||||
local modeVehicle = makeVehicle("vehicleM", "Mode-traktor", {STRAW, TMR}, 1000000, 100, 0)
|
||||
local modeStarts = {}
|
||||
modeVehicle.ad.stateModule.getCurrentMode = function() return {start = function(_, user) table.insert(modeStarts, user); modeVehicle:startAutoDrive() end} end
|
||||
modeVehicle.ad.stateModule.setActualFarmId = function(self, farmId) self.actualFarmId = farmId end
|
||||
adEnv.AutoDrive.USER_PLAYER = 1
|
||||
local modeRun = ADRunsController.createRun("Mode")
|
||||
ADRunsController.updateRun(modeRun.id, {targetWayPointId = 94})
|
||||
ADRunsController.assignVehicle(modeRun.id, modeVehicle)
|
||||
ADRunsController.startRun(adEnv, modeRun.id)
|
||||
check("C14 mode:start kaldt med USER_PLAYER", modeStarts[1], 1)
|
||||
check("C14 traktor aktiv", modeVehicle.started, 1)
|
||||
check("C14 farm-id sat", modeVehicle.ad.stateModule.actualFarmId, 1)
|
||||
check("C14 loopsDone nulstillet", modeVehicle.ad.stateModule.loopsDone, 0)
|
||||
|
||||
-- C15: aktivitet til overblikket — stoppet / venter / med læs / tom på vej efter vare
|
||||
ADUnloadWait = {getWaitFor = function(v) return v.waitInfo end, getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end}
|
||||
ADSmartPickup.getActivePickup = function(v) return v.pickupInfo end
|
||||
buildWorld()
|
||||
local actVehicle = makeVehicle("vehicleAct", "Akt", {STRAW, TMR}, 1000000, 100, 0)
|
||||
check("C15 stoppet", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "stopped")
|
||||
actVehicle.ad.stateModule.active = true
|
||||
actVehicle.ad.stateModule.secondMarkerId = 94
|
||||
actVehicle.loadType, actVehicle.loadLiters = STRAW, 8000
|
||||
local loaded = ADRunsController.getVehicleActivity(adEnv, actVehicle)
|
||||
check("C15 med læs", loaded.kind .. "/" .. loaded.fillType .. "/" .. loaded.liters, "loaded/" .. STRAW .. "/8000")
|
||||
actVehicle.loadLiters = 0
|
||||
actVehicle.pickupInfo = {markerName = "Silo 3", fillType = TMR}
|
||||
local empty = ADRunsController.getVehicleActivity(adEnv, actVehicle)
|
||||
check("C15 tom på vej efter TMR", empty.kind .. "/" .. tostring(empty.place) .. "/" .. tostring(empty.fillType), "empty/Silo 3/" .. TMR)
|
||||
actVehicle.waitInfo = {markerName = "Cow Wait", fillType = STRAW, rest = 5000}
|
||||
check("C15 venter", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "waiting")
|
||||
ADUnloadWait = nil
|
||||
|
||||
-- C16: flere mål pr. run — Auto-varer = union; chooseTarget = den stald der mangler mest; validering
|
||||
buildWorld()
|
||||
addMarker(99, "Cow 2 Food", "Cow Food", 5200, 0); unloadAt[99] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {[STRAW] = 0, [TMR] = 0}}
|
||||
unloadAt[94].levels = {[STRAW] = 250000, [TMR] = 250000, [WATER] = 50000}
|
||||
local multiVehicle = makeVehicle("vehicleMulti", "Multi", {STRAW, TMR, WATER}, 1000000, 100, 0)
|
||||
local multi = ADRunsController.createRun("Kvæg")
|
||||
ADRunsController.updateRun(multi.id, {targetWayPointIds = {94, 99}})
|
||||
local multiRun = ADRuns.getRun(ADRunsController.store, multi.id)
|
||||
check("C16 auto-varer = union (vand kun i stald 1)", table.concat(ADRunsController.getRunIngredients(adEnv, multiRun, multiVehicle), ","), WATER .. "," .. STRAW .. "," .. TMR)
|
||||
check("C16 accepterede varer = union", #ADRunsController.getTargetAcceptedFillTypes(adEnv, multiRun), 3)
|
||||
check("C16 vælger stald 2 (tom)", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
|
||||
unloadAt[99].levels = {[STRAW] = 290000, [TMR] = 290000}
|
||||
check("C16 vælger stald 1 når stald 2 er fuld", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
|
||||
check("C16 validering ok med flere mål", ADRunsController.validateVehicle(adEnv, multiRun, multiVehicle).ok, true)
|
||||
ADRunsController.updateRun(multi.id, {targetWayPointIds = {4242}})
|
||||
check("C16 kun ukendte mål -> noTarget", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, multi.id), multiVehicle).reason, "noTarget")
|
||||
|
||||
-- C17: kørende traktor der fjernes fra run stoppes; slettes run, stoppes dets traktorer; Stop logger
|
||||
buildWorld()
|
||||
local runVehicle = makeVehicle("vehicleR", "Kørende", {STRAW, TMR}, 1000000, 100, 0)
|
||||
local stopRun = ADRunsController.createRun("Stop-test")
|
||||
ADRunsController.updateRun(stopRun.id, {targetWayPointId = 94})
|
||||
ADRunsController.assignVehicle(stopRun.id, runVehicle)
|
||||
ADRunsController.startRun(adEnv, stopRun.id)
|
||||
check("C17 kører", runVehicle.started, 1)
|
||||
ADRunsController.unassignVehicle(runVehicle)
|
||||
check("C17 fjernet fra run -> stoppet", runVehicle.stopped, 1)
|
||||
ADRunsController.assignVehicle(stopRun.id, runVehicle)
|
||||
ADRunsController.startRun(adEnv, stopRun.id)
|
||||
ADRunsController.deleteRun(stopRun.id, adEnv)
|
||||
check("C17 run slettet -> stoppet", runVehicle.stopped, 2)
|
||||
check("C17 stop logget", logLines[#logLines]:find("stoppet", 1, true) ~= nil, true)
|
||||
|
||||
-- C18: forkert last ved Start (halm på vognen, run mod sukkerfabrik) -> første stop = silo der tager halm
|
||||
ADUnloadWait = {getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end, getWaitFor = function() return nil end,
|
||||
hasRouteBothWays = function() return true end}
|
||||
ADLoadSwap = {findDumpMarker = function(_, v, fillType, rest, homeId, isReachable) return {id = 44, name = "10 Silo Storage", markerIndex = 2} end}
|
||||
buildWorld()
|
||||
addMarker(77, "sugar offload", "Fabrication", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}} -- "fabrik" der ikke tager halm (TMR står for roer her)
|
||||
local sugarVehicle = makeVehicle("vehicleSugar", "Roe-traktor", {STRAW, TMR}, 20000, 100, 0)
|
||||
sugarVehicle.loadType, sugarVehicle.loadLiters = STRAW, 8000
|
||||
local sugar = ADRunsController.createRun("Sugar")
|
||||
ADRunsController.updateRun(sugar.id, {targetWayPointId = 77})
|
||||
ADRunsController.assignVehicle(sugar.id, sugarVehicle)
|
||||
ADRunsController.startRun(adEnv, sugar.id)
|
||||
check("C18 første stop = siloen (aflæs forkert last)", sugarVehicle.ad.stateModule.secondMarkerId, 44)
|
||||
check("C18 logget", logLines[#logLines]:find("læsser", 1, true) ~= nil or logLines[#logLines - 1]:find("læsser", 1, true) ~= nil, true)
|
||||
-- rigtig last (TMR=roer) -> direkte til fabrikken
|
||||
sugarVehicle.loadType = TMR
|
||||
sugarVehicle.ad.stateModule.active = false
|
||||
ADRunsController.startRun(adEnv, sugar.id)
|
||||
check("C18 rigtig last -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
|
||||
-- C18b: målet TAGER varen, men den er ikke en af run'ets valgte varer (gødning i en ensilage-DOB)
|
||||
-- -> aflæs i en silo der tager den, ikke i målet
|
||||
unloadAt[77].accepts = {[TMR] = 360000, [STRAW] = 360000}
|
||||
ADRunsController.updateRun(sugar.id, {ingredientMode = "manual", ingredients = {"FORAGE"}})
|
||||
sugarVehicle.loadType = STRAW
|
||||
sugarVehicle.ad.stateModule.active = false
|
||||
ADRunsController.startRun(adEnv, sugar.id)
|
||||
check("C18b forkert vare for run'et -> silo, ikke målet", sugarVehicle.ad.stateModule.secondMarkerId, 44)
|
||||
-- auto-varer: alt målet tager hører til -> målet
|
||||
ADRunsController.updateRun(sugar.id, {ingredientMode = "auto", ingredients = {}})
|
||||
sugarVehicle.ad.stateModule.active = false
|
||||
ADRunsController.startRun(adEnv, sugar.id)
|
||||
check("C18b auto: målet tager varen -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
|
||||
ADUnloadWait = nil; ADLoadSwap = nil
|
||||
|
||||
-- C19: Wait-markører er aldrig mål — heller ikke når de står inden for 40 m af staldens trigger
|
||||
buildWorld()
|
||||
-- staldens station med bygning; Wait-markøren ligger i triggerens rækkevidde og "mangler mere" end 94
|
||||
local barnPlaceable = {getHusbandryFillLevel = function() return 0 end}
|
||||
unloadAt[94].owningPlaceable = barnPlaceable
|
||||
addMarker(400, "Cow Wait", "Cow Wait", 5005, 0); unloadAt[400] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {}, owningPlaceable = barnPlaceable}
|
||||
local targetNames = {}
|
||||
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, 1)) do table.insert(targetNames, marker.name) end
|
||||
check("C19 Wait-markør ikke i mål-listen", table.concat(targetNames, ","):find("Cow Wait", 1, true), nil)
|
||||
local waitTargetRun = ADRunsController.createRun("Wait som mål")
|
||||
ADRunsController.updateRun(waitTargetRun.id, {targetWayPointIds = {94, 400}})
|
||||
local waitVehicle = makeVehicle("vehicleW", "W", {STRAW, TMR}, 20000, 5000, 0)
|
||||
check("C19 chooseTarget springer Wait over", ADRunsController.chooseTarget(adEnv, ADRuns.getRun(ADRunsController.store, waitTargetRun.id), waitVehicle), 94)
|
||||
|
||||
-- C-out: udkørsels-runs — oprettelse, kilder, produkter, validering, start, status
|
||||
local BASKET = 7
|
||||
FILL_TYPE_NAMES[BASKET] = "STRAW_BASKET"; FILL_TYPE_INDEX["STRAW_BASKET"] = BASKET
|
||||
local sourcesAt, productsAt = {}, {}
|
||||
ADSources = {
|
||||
identify = function(_, id) return sourcesAt[id] end,
|
||||
getProducts = function(_, source) return productsAt[source.wayPointId] or {} end,
|
||||
getSellMarkers = function() return {} end,
|
||||
stationAccepts = function() return false end,
|
||||
getSourcePlaceables = function() return {} end,
|
||||
}
|
||||
local outboundStatus = {reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}, warning = nil}
|
||||
local holdFor = {}
|
||||
ADOutbound = {
|
||||
isOutboundVehicle = function(v) local r = ADRunsController.getRunForVehicle(v) return r ~= nil and r.kind == "outbound" end,
|
||||
getRunStatus = function() return outboundStatus end,
|
||||
getHoldFor = function(v) return holdFor[v] end,
|
||||
getTripFor = function() return nil end,
|
||||
releaseInactive = function() end,
|
||||
}
|
||||
ADUnloadWait = {getRestLoad = function() return nil, 0 end, getWaitFor = function() return nil end}
|
||||
buildWorld()
|
||||
addMarker(1, "Suger Load", "Pickup points", 100, 0); loadAt[1] = {}
|
||||
addMarker(3, "Objektlager", "Pickup points", 300, 0)
|
||||
addMarker(10, "selling station", "selling", 500, 0)
|
||||
sourcesAt[1] = {kind = "factory", wayPointId = 1, station = loadAt[1], name = "Sugar mill"}
|
||||
sourcesAt[3] = {kind = "objectStorage", wayPointId = 3, name = "Objektlager"}
|
||||
productsAt[1] = {{fillType = TMR, name = "FORAGE", title = "TMR", available = 150000, level = 150000, capacity = 180000, isPallet = false}}
|
||||
productsAt[3] = {{fillType = BASKET, name = "STRAW_BASKET", title = "Halmkurve", available = 8000, level = 8, capacity = 4000, isPallet = true}}
|
||||
local bulkTractor = makeVehicle("vehicleBulk", "MT635", {STRAW, TMR}, 20000, 100, 0)
|
||||
local ualTractor = makeVehicle("vehicleUal", "Fendt", {}, 8000, 300, 0)
|
||||
ualTractor.trailers[1] = {ual = true}
|
||||
adEnv.AutoDrive.hasAL = function(_, trailer) return trailer.ual == true end
|
||||
|
||||
local out = ADRunsController.createRun("Sugar out", "outbound")
|
||||
check("Cout1 kind", ADRuns.getRun(ADRunsController.store, out.id).kind, "outbound")
|
||||
check("Cout1 supply-run har kind supply", ADRunsController.createRun("Cows").kind, "supply")
|
||||
check("Cout1 kilde-markører", #ADRunsController.getSourceMarkers(adEnv, 1), 2)
|
||||
check("Cout1 første kilde-markør", ADRunsController.getSourceMarkers(adEnv, 1)[1].marker.name, "Objektlager")
|
||||
|
||||
-- Cout2: validering
|
||||
local outRun = ADRuns.getRun(ADRunsController.store, out.id)
|
||||
-- diagnose: hvad moden ser på enhederne, når ingen autoload findes (logges én gang pr. traktor,
|
||||
-- første gang varerne slås op — dvs. allerede ved noProducts-tjekket nedenfor)
|
||||
logLines = {}
|
||||
bulkTractor.trailers[1] = {spec_universalAutoload = {isAutoloadAvailable = false, autoloadDisabled = true}, getName = function() return "Vogn" end}
|
||||
check("Cout2 ingen kilder -> noSource", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noSource")
|
||||
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 1)
|
||||
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 3)
|
||||
outRun = ADRuns.getRun(ADRunsController.store, out.id)
|
||||
check("Cout2 ingen vare valgt -> noProducts", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noProducts")
|
||||
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "STRAW_BASKET", "sell")
|
||||
outRun = ADRuns.getRun(ADRunsController.store, out.id)
|
||||
check("Cout2 kun paller uden UAL -> needsAutoload", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "needsAutoload")
|
||||
local diag = nil
|
||||
for _, line in ipairs(logLines) do if line:find("ingen autoload", 1, true) then diag = line end end
|
||||
check("Cout2 diagnose logges", diag ~= nil, true)
|
||||
check("Cout2 diagnose viser UAL-flag", diag ~= nil and diag:find("isAutoloadAvailable=false", 1, true) ~= nil, true)
|
||||
logLines = {}
|
||||
ADRunsController.validateVehicle(adEnv, outRun, bulkTractor)
|
||||
check("Cout2 diagnose kun én gang", #logLines, 0)
|
||||
bulkTractor.trailers[1] = nil
|
||||
check("Cout2 UAL-vogn ok", ADRunsController.validateVehicle(adEnv, outRun, ualTractor).ok, true)
|
||||
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "FORAGE", "store")
|
||||
outRun = ADRuns.getRun(ADRunsController.store, out.id)
|
||||
check("Cout2 bulk-vogn ok med TMR", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).ok, true)
|
||||
local strawOnly = makeVehicle("vehicleStraw", "Halm", {STRAW}, 20000, 100, 0)
|
||||
check("Cout2 kan ikke bære -> cannotCarry", ADRunsController.validateVehicle(adEnv, outRun, strawOnly).reason, "cannotCarry")
|
||||
|
||||
-- Cout3: start sætter mode, 1./2. markør = første kilde, varevalg = det vognen kan bære af run'ets varer
|
||||
ADRunsController.assignVehicle(out.id, bulkTractor)
|
||||
ADRunsController.assignVehicle(out.id, ualTractor)
|
||||
local results = ADRunsController.startRun(adEnv, out.id)
|
||||
check("Cout3 to startet", #results, 2)
|
||||
check("Cout3 mode P&D", bulkTractor.ad.stateModule.mode, MODE_PICKUPANDDELIVER)
|
||||
check("Cout3 1. markør = første kilde", bulkTractor.ad.stateModule.firstMarkerId, 1)
|
||||
check("Cout3 2. markør = første kilde (pladsholder)", bulkTractor.ad.stateModule.secondMarkerId, 1)
|
||||
check("Cout3 bulk-vogn: kun TMR", table.concat(bulkTractor.ad.stateModule.selectedFillTypes, ","), tostring(TMR))
|
||||
check("Cout3 UAL-vogn: kurve", table.concat(ualTractor.ad.stateModule.selectedFillTypes, ","), tostring(BASKET))
|
||||
check("Cout3 rotation kun pålæsning", bulkTractor.ad.settings.rotateTargets.current, 2)
|
||||
check("Cout3 startet", bulkTractor.started, 1)
|
||||
-- AD's "AL unload" er slået FRA som standard (values[1] = 0): autoload-vognen ville aldrig læsse af
|
||||
-- ved salgsstedet. Ved Start sættes den til "center" hvis den er fra; et brugervalg (left/right/behind) røres ikke.
|
||||
check("Cout3 ALUnload sat til center for autoload-bil", ualTractor.ad.settings.ALUnload.current, 2)
|
||||
check("Cout3 ALUnload rører ikke tipvogn", bulkTractor.ad.settings.ALUnload.current, 1)
|
||||
ualTractor.ad.settings.ALUnload.current = 4
|
||||
ualTractor.ad.stateModule.active = false
|
||||
ADRunsController.startRun(adEnv, out.id)
|
||||
check("Cout3 ALUnload brugervalg bevares", ualTractor.ad.settings.ALUnload.current, 4)
|
||||
|
||||
-- Cout4: produkter til fanen (politik slået op, sorteret på titel) og status
|
||||
local list = ADRunsController.getRunProducts(adEnv, outRun)
|
||||
check("Cout4 to produkter", #list, 2)
|
||||
check("Cout4 første = Halmkurve", list[1].title, "Halmkurve")
|
||||
check("Cout4 politik", list[1].policy, "sell")
|
||||
check("Cout4 TMR lager", list[2].policy, "store")
|
||||
check("Cout4 available", list[2].available, 150000)
|
||||
local st = ADRunsController.getOutboundStatus(adEnv, outRun)
|
||||
check("Cout4 status-begrundelse", st.reasons[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
|
||||
holdFor[bulkTractor] = {markerName = "Wait 3", reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}}
|
||||
local activity = ADRunsController.getVehicleActivity(adEnv, bulkTractor)
|
||||
check("Cout4 aktivitet = holder", activity.kind, "holding")
|
||||
check("Cout4 aktivitet sted", activity.place, "Wait 3")
|
||||
check("Cout4 udkørsel har ingen mål", #ADRunsController.getRunTargets(outRun), 0)
|
||||
-- Cout5 (I2): Start kræver et Wait-punkt i puljen; Cout6 (I3): last på vognen ved Start får eget mål
|
||||
ADSupplyPlanner.isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end
|
||||
local noWait = ADRunsController.createRun("No wait", "outbound")
|
||||
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, noWait.id, 1)
|
||||
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, noWait.id, "FORAGE", "store")
|
||||
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Pig Wait"})
|
||||
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Findes ikke"})
|
||||
local noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
|
||||
check("Cout5 ingen Wait-punkt i puljen -> noWaitPoint", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "noWaitPoint")
|
||||
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Cow Wait"})
|
||||
noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
|
||||
check("Cout5 Wait-punkt findes -> ok", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).ok, true)
|
||||
ADUnloadWait.getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end
|
||||
ADOutbound.planCargoTarget = function(adEnv, run, vehicle, fillType) return fillType == TMR and 10 or nil end
|
||||
bulkTractor.loadType, bulkTractor.loadLiters = TMR, 8000
|
||||
ADRunsController.assignVehicle(noWait.id, bulkTractor)
|
||||
bulkTractor.ad.stateModule.active = false
|
||||
ADRunsController.startRun(adEnv, noWait.id)
|
||||
check("Cout6 last på vognen -> 2. markør = lastens mål", bulkTractor.ad.stateModule.secondMarkerId, 10)
|
||||
bulkTractor.loadType = STRAW
|
||||
check("Cout6 last uden mål -> cargoNoTarget", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "cargoNoTarget")
|
||||
bulkTractor.loadType = nil
|
||||
ADUnloadWait = nil; ADOutbound = nil; ADSources = nil
|
||||
|
||||
-- C-flow: bygninger -> markører (resolveMarkers), migration af gamle markør-runs, noMarker, flow-rækker
|
||||
local buildings = {
|
||||
{id = "barn1", name = "Kostald 1", kind = "husbandry", loadMarker = {id = 10, name = "cow 1 milk", distance = 5}, loadMarkers = {{id = 10, name = "cow 1 milk", distance = 5}, {id = 11, name = "cow 1 slurry", distance = 3}}, unloadMarker = {id = 94, name = "Cow 1 Food", distance = 8}, canBeSource = true, canBeTarget = true, canSell = false},
|
||||
{id = "shed", name = "Palleskur", kind = "objectStorage", loadMarker = nil, unloadMarker = nil, canBeSource = true, canBeTarget = false, canSell = false},
|
||||
{id = "mill", name = "Sukkerfabrik", kind = "factory", loadMarker = {id = 12694, name = "Suger Load", distance = 3}, unloadMarker = {id = 77, name = "sugar offload", distance = 4}, canBeSource = true, canBeTarget = true, canSell = false},
|
||||
}
|
||||
ADBuildings = {
|
||||
list = function() return buildings end,
|
||||
find = function(_, _, id) for _, b in ipairs(buildings) do if b.id == id then return b end end end,
|
||||
byMarker = function(_, _, wayPointId, role)
|
||||
for _, b in ipairs(buildings) do
|
||||
if role == "unload" then
|
||||
if b.unloadMarker ~= nil and b.unloadMarker.id == wayPointId then return b end
|
||||
else
|
||||
for _, m in ipairs(b.loadMarkers or {b.loadMarker}) do if m ~= nil and m.id == wayPointId then return b end end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
ADSources = {identify = function(_, id) return (id == 10 or id == 12694 or id == 555) and {kind = "x", wayPointId = id, name = "k"} or nil end, getProducts = function() return {} end, getSellMarkers = function() return {} end, stationAccepts = function() return false end, getSourcePlaceables = function() return {} end}
|
||||
ADOutbound = {isOutboundVehicle = function() return false end, getRunStatus = function() return {reasons = {"SUGAR 78 % af top"}} end, getHoldFor = function() return nil end, getTripFor = function() return nil end, releaseInactive = function() end}
|
||||
buildWorld()
|
||||
addMarker(10, "cow 1 milk", "Milk", 100, 0); loadAt[10] = {}
|
||||
addMarker(555, "raa markoer", "Pickup", 200, 0); loadAt[555] = {}
|
||||
addMarker(77, "sugar offload", "production", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}}
|
||||
-- CF1: udkørsel: bygninger + rå markør -> sourceWayPointIds; manglende markør rapporteres
|
||||
local flowOut = ADRunsController.createRun("Milk", "outbound")
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {555}})
|
||||
local resolved = ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
|
||||
check("CF1 kildemarkører = ALLE bygningens læssemarkører + rå", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
|
||||
check("CF1 manglende markør", resolved.missing[1], "Palleskur")
|
||||
-- en markør der HØRER til en bygning som ikke længere er valgt, fjernes (den er ikke rå)
|
||||
ADRunsController.updateRun(flowOut.id, {sourceWayPointIds = {12694, 555}})
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
|
||||
check("CF1 fravalgt bygnings markør fjernes", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
|
||||
-- CF1b: gamle rå markører ryddes: ikke-kilde (identify = nil) og markør der hører til en valgt bygning
|
||||
ADSources.identify = function(_, id)
|
||||
if id == 10 or id == 12694 or id == 555 then return {kind = "x", wayPointId = id, name = "k"} end
|
||||
if id == 556 then return {kind = "husbandry", wayPointId = id, name = "Kostald 1", placeable = "barn1-placeable"} end
|
||||
return nil
|
||||
end
|
||||
buildings[1].placeable = "barn1-placeable"
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 556, 999}})
|
||||
logLines = {}
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
|
||||
check("CF1b rå ikke-kilde og dublet-bygning fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
|
||||
local pruneLogged = false
|
||||
for _, line in ipairs(logLines) do if line:find("gamle markører fjernet", 1, true) then pruneLogged = true end end
|
||||
check("CF1b logget", pruneLogged, true)
|
||||
|
||||
-- CF1c: et gammelt kilde-id der ikke længere er en AD-MARKØR (AutoDrive har omnummereret waypoints)
|
||||
-- fjernes, selv om punktet ligger ved en kilde — ellers kører traktoren til et tilfældigt vejpunkt
|
||||
local identifyBefore = ADSources.identify
|
||||
ADSources.identify = function(adEnv_, id, farmId)
|
||||
if id == 10873 then return {kind = "husbandry", wayPointId = id, name = "Anden stald", placeable = "anden-stald"} end
|
||||
return identifyBefore(adEnv_, id, farmId)
|
||||
end
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 10873}})
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
|
||||
check("CF1c vejpunkt uden markør fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
|
||||
ADSources.identify = identifyBefore
|
||||
|
||||
-- CF2: forsyning: mål-bygninger -> targetWayPointIds + targetWayPointId
|
||||
local flowIn = ADRunsController.createRun("Foder", "supply")
|
||||
ADRunsController.updateRun(flowIn.id, {targetBuildingIds = {"barn1", "mill"}})
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
|
||||
check("CF2 målmarkører", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77")
|
||||
check("CF2 primært mål", ADRuns.getRun(ADRunsController.store, flowIn.id).targetWayPointId, 94)
|
||||
-- CF2b: gamle rå mål der ikke er en markør (slettet i AD) eller hører til en valgt bygning, ryddes
|
||||
addMarker(78, "Anden stald food", "Cow Food", 3000, 0); unloadAt[78] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"}
|
||||
unloadAt[11062] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"} -- waypoint uden markør
|
||||
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77, 11062, 78}})
|
||||
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
|
||||
check("CF2b ikke-markør fjernet, rå markør med station beholdt", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77,78")
|
||||
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77}})
|
||||
-- CF3: migration: gamle markør-runs får bygninger
|
||||
local oldOut = ADRunsController.createRun("Gammel ud", "outbound")
|
||||
ADRunsController.updateRun(oldOut.id, {sourceWayPointIds = {12694, 555}})
|
||||
local oldIn = ADRunsController.createRun("Gammel ind", "supply")
|
||||
ADRunsController.updateRun(oldIn.id, {targetWayPointIds = {94}})
|
||||
ADRunsController.migrateBuildings(adEnv)
|
||||
check("CF3 ud: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds[1], "mill")
|
||||
check("CF3 ud: rå markør uden bygning beholdes", table.concat(ADRuns.getRun(ADRunsController.store, oldOut.id).sourceWayPointIds, ","), "12694,555")
|
||||
check("CF3 ind: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldIn.id).targetBuildingIds[1], "barn1")
|
||||
ADRunsController.migrateBuildings(adEnv)
|
||||
check("CF3 migration er idempotent", #ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds, 1)
|
||||
-- CF4: validering: valgt bygning uden markør -> noMarker
|
||||
local cfVehicle = makeVehicle("vehicleCF", "CF", {STRAW, TMR}, 20000, 100, 0)
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"shed"}, sourceWayPointIds = {}})
|
||||
local cfValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
|
||||
check("CF4 noMarker", cfValidation.reason, "noMarker")
|
||||
check("CF4 hvilken bygning", cfValidation.missingBuilding, "Palleskur")
|
||||
-- CF4b: ÉN bygning uden markør blandt flere med markør -> start alligevel, advar om den ene
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {}})
|
||||
logLines = {}
|
||||
local cfValidation2 = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
|
||||
check("CF4b ikke afvist pga. markør", cfValidation2.reason ~= "noMarker", true)
|
||||
local warned = false
|
||||
for _, line in ipairs(logLines) do if line:find("Palleskur", 1, true) and line:find("springes over", 1, true) then warned = true end end
|
||||
check("CF4b advarsel nævner bygningen", warned, true)
|
||||
-- CF5: flow-række til tavlen
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, products = {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "store"}}, collectRound = true})
|
||||
ADRunsController.assignVehicle(flowOut.id, cfVehicle)
|
||||
local row = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
|
||||
check("CF5 vare-navne", table.concat(row.productNames, ","), "SUGAR,FORAGE")
|
||||
check("CF5 fra", row.fromNames[1], "Kostald 1")
|
||||
check("CF5 politikker", table.concat(row.policies, ","), "sell,store")
|
||||
check("CF5 traktor", row.vehicleNames[1], "CF")
|
||||
check("CF5 begrundelse", row.reason, "SUGAR 78 % af top")
|
||||
check("CF5 runde", row.collectRound, true)
|
||||
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}})
|
||||
check("CF5 manglende bygning markeres", ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id)).missing[1], "Palleskur")
|
||||
local rowIn = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
|
||||
check("CF5 forsyning fra = alle siloer", rowIn.fromAllSilos, true)
|
||||
check("CF5 forsyning til", table.concat(rowIn.toNames, ","), "Kostald 1,Sukkerfabrik")
|
||||
check("CF5 forsyning auto-varer", rowIn.productAuto, true)
|
||||
-- SB: "Lager til" — valgte bygninger -> deres aflæsningsmarkører (i valgt rækkefølge; ukendte udelades)
|
||||
do
|
||||
local sbRun = ADRunsController.createRun("Gylle", "outbound")
|
||||
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {"mill", "findes-ikke", "barn1"}})
|
||||
check("SB markører", table.concat(ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), ","), "77,94")
|
||||
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {}})
|
||||
check("SB tom = ingen tvang", #ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), 0)
|
||||
end
|
||||
-- CS: "Lager til" viser kun bygninger der kan tage flowets lagervarer (æg-paller: kun palleskure der tager æg)
|
||||
do
|
||||
local EGG, SILAGE = 501, 502
|
||||
local shed = {id = "shed1", kind = "objectStorage", placeable = {takesEggs = true}}
|
||||
local shedNo = {id = "shed2", kind = "objectStorage", placeable = {takesEggs = false}}
|
||||
local silo = {id = "silo9", kind = "silo", placeable = {}, unloadMarker = {id = 901}}
|
||||
unloadAt[901] = {accepts = {[SILAGE] = 100000}, getIsFillTypeSupported = function(_, ft) return ft == SILAGE end}
|
||||
local accBefore, storeBefore = ADSources.objectStorageAccepts, ADLoadSwap and ADLoadSwap.isStoreTarget
|
||||
ADSources.objectStorageAccepts = function(placeable, ft) return ft == EGG and placeable.takesEggs end
|
||||
ADLoadSwap = ADLoadSwap or {}
|
||||
ADLoadSwap.isStoreTarget = function(station, ft) return station.getIsFillTypeSupported(station, ft) end
|
||||
local eggs = {{fillType = EGG, isPallet = true}}
|
||||
check("CS palleskur der tager æg", ADRunsController.canStoreAt(adEnv, shed, eggs), true)
|
||||
check("CS palleskur der ikke tager æg", ADRunsController.canStoreAt(adEnv, shedNo, eggs), false)
|
||||
check("CS silo kan ikke tage æg-paller", ADRunsController.canStoreAt(adEnv, silo, eggs), false)
|
||||
check("CS silo tager ensilage", ADRunsController.canStoreAt(adEnv, silo, {{fillType = SILAGE, isPallet = false}}), true)
|
||||
ADSources.objectStorageAccepts = accBefore
|
||||
ADLoadSwap.isStoreTarget = storeBefore
|
||||
end
|
||||
ADBuildings = nil; ADSources = nil; ADOutbound = nil
|
||||
|
||||
-- RC-navne: køretøjer får AD-førernavn efter forløbet; ude af forløb -> modelnavnet igen
|
||||
do
|
||||
local named = makeVehicle("vehicleNamed", "Virtus 135", {MILK or 1}, 20000, 0, 0)
|
||||
named.ad.stateModule.driverName = "Virtus 135"
|
||||
named.ad.stateModule.setName = function(self, n) self.driverName = n end
|
||||
named.ad.stateModule.getName = function(self) return self.driverName end
|
||||
local run = ADRunsController.createRun("Mælk ind", "outbound")
|
||||
ADRunsController.assignVehicle(run.id, named)
|
||||
ADRunsController.applyVehicleNames(adEnv)
|
||||
check("RC-navne sat efter forløb", named.ad.stateModule.driverName, "Mælk ind")
|
||||
check("RC-navne label bruger førernavn", ADRuns.vehicleLabel(named), "Mælk ind")
|
||||
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, "vehicleNamed")
|
||||
ADRunsController.applyVehicleNames(adEnv)
|
||||
check("RC-navne ude af forløb -> modelnavn", named.ad.stateModule.driverName, "Virtus 135")
|
||||
check("RC-navne label uden AD-navn", ADRuns.vehicleLabel({getName = function() return "Fendt" end}), "Fendt")
|
||||
end
|
||||
|
||||
-- PM: afhentningsmarkører fra udkørsels-forløb (lagerture undgår dem som aflæsningssted)
|
||||
do
|
||||
local sale = ADRunsController.createRun("Salg-test", "outbound")
|
||||
ADRunsController.updateRun(sale.id, {sourceWayPointIds = {4711, 4712}})
|
||||
local supply = ADRunsController.createRun("Forsyning-test", "supply")
|
||||
ADRunsController.updateRun(supply.id, {sourceWayPointIds = {9999}})
|
||||
local pickups = ADRunsController.getPickupMarkers()
|
||||
check("PM udkørsels-kilde med", pickups[4711], true)
|
||||
check("PM forsyning tæller ikke", pickups[9999], nil)
|
||||
end
|
||||
|
||||
-- SA: Start/Stop alle flows — kørende flows røres ikke ved Start alle; Stop alle stopper alt aktivt
|
||||
do
|
||||
buildWorld()
|
||||
local idle = makeVehicle("vehicleAllA", "Alle A", {STRAW, TMR}, 1000000, 100, 0)
|
||||
local busy = makeVehicle("vehicleAllB", "Alle B", {STRAW, TMR}, 1000000, 100, 0)
|
||||
local runA = ADRunsController.createRun("Alle-A")
|
||||
ADRunsController.updateRun(runA.id, {targetWayPointId = 94})
|
||||
ADRunsController.assignVehicle(runA.id, idle)
|
||||
local runB = ADRunsController.createRun("Alle-B")
|
||||
ADRunsController.updateRun(runB.id, {targetWayPointId = 94})
|
||||
ADRunsController.assignVehicle(runB.id, busy)
|
||||
ADRunsController.createRun("Alle-tom") -- ingen traktorer: tæller ikke med
|
||||
ADRunsController.startRun(adEnv, runB.id)
|
||||
local busyStarts = busy.started
|
||||
local summary = ADRunsController.startAllRuns(adEnv)
|
||||
check("SA idle startet", idle.started, 1)
|
||||
check("SA kørende flow ikke genstartet", busy.started, busyStarts)
|
||||
check("SA mindst ét flow startet", summary.runsStarted >= 1, true)
|
||||
check("SA kørende flow talt", summary.alreadyRunning >= 1, true)
|
||||
check("SA traktorer startet tæller idle", summary.vehiclesStarted >= 1, true)
|
||||
check("SA resultater med", type(summary.results), "table")
|
||||
check("SA noget aktivt", ADRunsController.hasActiveRuns(adEnv), true)
|
||||
local stopped = ADRunsController.stopAllRuns(adEnv)
|
||||
check("SA stop alle stopper begge", stopped >= 2, true)
|
||||
check("SA idle stoppet", idle.ad.stateModule.active, false)
|
||||
check("SA busy stoppet", busy.ad.stateModule.active, false)
|
||||
check("SA intet aktivt bagefter", ADRunsController.hasActiveRuns(adEnv), false)
|
||||
check("SA isRunning ryddet", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runA.id)).isRunning, false)
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,533 +0,0 @@
|
||||
-- Kør: luajit tests/test_adSources.lua (fra repo-roden)
|
||||
-- Kilde-genkendelse ved en AD-markør (fabrik / silo / objektlager), lagerstatus pr. vare,
|
||||
-- paller ved fabrikkens spawner, salgssteder, objektlager-udtag.
|
||||
|
||||
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
|
||||
local DESC = {
|
||||
[SUGAR] = {index = SUGAR, name = "SUGAR", title = "Sukker", isPalletType = true},
|
||||
[FORAGE] = {index = FORAGE, name = "FORAGE", title = "TMR"},
|
||||
[BASKET] = {index = BASKET, name = "STRAW_BASKET", title = "Halmkurve", palletFilename = "basketPallet.xml"},
|
||||
[CRATE] = {index = CRATE, name = "STRAW_CRATE", title = "Halmkasser", palletFilename = "cratePallet.xml"},
|
||||
}
|
||||
g_fillTypeManager = {
|
||||
getFillTypeByIndex = function(_, index) return DESC[index] end,
|
||||
getFillTypeNameByIndex = function(_, index) return DESC[index] and DESC[index].name end,
|
||||
getFillTypeTitleByIndex = function(_, index) return DESC[index] and DESC[index].title end,
|
||||
}
|
||||
Logging = {info = function() end, warning = function() end}
|
||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
||||
SellingStation = {}
|
||||
|
||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
||||
dofile("FS25_ADSmartPickup/adStorageStats.lua")
|
||||
dofile("FS25_ADSmartPickup/adSources.lua")
|
||||
|
||||
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
|
||||
|
||||
-- ---- fakes ----
|
||||
local wayPoints, markers = {}, {}
|
||||
local function addMarker(id, name, x, z)
|
||||
wayPoints[id] = {id = id, x = x, z = z}
|
||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = "g"})
|
||||
end
|
||||
local adEnv = {ADGraphManager = {
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getMapMarkers = function() return markers end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
}}
|
||||
|
||||
local function storageOf(levels, capacities)
|
||||
return {
|
||||
fillLevels = levels,
|
||||
getFillLevel = function(_, ft) return levels[ft] or 0 end,
|
||||
getCapacity = function(_, ft) return capacities[ft] or 0 end,
|
||||
}
|
||||
end
|
||||
|
||||
local sugarStorage = storageOf({[SUGAR] = 150000, [BASKET] = 0}, {[SUGAR] = 180000, [BASKET] = 8000})
|
||||
local factory = {
|
||||
rootNode = {x = 100, z = 100}, getName = function() return "Sugar mill" end,
|
||||
spec_productionPoint = {productionPoint = {
|
||||
productions = {{outputs = {{type = SUGAR, amount = 1}}}, {outputs = {{type = BASKET, amount = 1}}}},
|
||||
storage = sugarStorage,
|
||||
palletSpawner = {spawnPlaces = {{startNode = {x = 110, z = 100}}}},
|
||||
}},
|
||||
}
|
||||
local factoryStation = {owningPlaceable = factory, supportedFillTypes = {[SUGAR] = true}}
|
||||
local silo = {rootNode = {x = 200, z = 200}, spec_silo = {storages = {storageOf({[FORAGE] = 74500}, {[FORAGE] = 12000000})}}, getName = function() return "Farma" end}
|
||||
local siloStation = {owningPlaceable = silo}
|
||||
local spawnCalls = {}
|
||||
local objectStorage = {
|
||||
rootNode = {x = 300, z = 300}, ownerFarmId = 1, getName = function() return "Objektlager" end,
|
||||
spec_objectStorage = {
|
||||
objectInfos = {
|
||||
{objects = {{palletAttributes = {fillType = BASKET, fillLevel = 1000}}}, numObjects = 8},
|
||||
{objects = {{baleAttributes = {fillType = FORAGE, fillLevel = 4000}}}, numObjects = 2},
|
||||
},
|
||||
numStoredObjects = 10, capacity = 4000,
|
||||
},
|
||||
removeAbstractObjectsFromStorage = function(self, index, amount, connection) table.insert(spawnCalls, {index, amount, connection}) end,
|
||||
}
|
||||
local sellStation = {owningPlaceable = {rootNode = {x = 500, z = 500}}, isa = function(_, c) return c == SellingStation end, isSellingPoint = true, acceptedFillTypes = {[SUGAR] = true}, getName = function() return "Grain Elevator" end}
|
||||
local factoryUnload = {owningPlaceable = factory, isa = function() return true end, isSellingPoint = false}
|
||||
|
||||
addMarker(1, "Suger Load", 100, 90)
|
||||
addMarker(2, "silo 2", 200, 190)
|
||||
addMarker(3, "Objektlager", 300, 280) -- 20 m fra lageret
|
||||
addMarker(4, "Langt væk", 300, 240) -- 60 m fra lageret
|
||||
addMarker(5, "selling station", 500, 490)
|
||||
addMarker(6, "sugar offload", 100, 80)
|
||||
|
||||
local loadAt = {[1] = factoryStation, [2] = siloStation}
|
||||
local unloadAt = {[5] = sellStation, [6] = factoryUnload}
|
||||
ADSmartPickup = {
|
||||
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
|
||||
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
|
||||
getOwnStorageLevel = function(station, fillTypes)
|
||||
local total = 0
|
||||
for _, storage in pairs(station.owningPlaceable.spec_silo.storages) do
|
||||
for _, ft in ipairs(fillTypes) do total = total + storage:getFillLevel(ft) end
|
||||
end
|
||||
return total
|
||||
end,
|
||||
}
|
||||
local pallets = {}
|
||||
local function addPallet(fillType, level, x, z, farmId)
|
||||
table.insert(pallets, {spec_pallet = {}, rootNode = {x = x, z = z}, getOwnerFarmId = function() return farmId or 1 end,
|
||||
getFillUnitFillType = function() return fillType end, getFillUnitFillLevel = function() return level end})
|
||||
end
|
||||
g_currentMission = {
|
||||
placeableSystem = {placeables = {factory, silo, objectStorage}},
|
||||
accessHandler = {canFarmAccess = function(_, farmId, placeable) return true end},
|
||||
vehicleSystem = {vehicles = pallets},
|
||||
}
|
||||
|
||||
-- S1: fabrik
|
||||
local source1 = ADSources.identify(adEnv, 1, 1)
|
||||
check("S1 fabrik", source1.kind, "factory")
|
||||
check("S1 navn", source1.name, "Sugar mill")
|
||||
local products1 = ADSources.getProducts(adEnv, source1, 1)
|
||||
check("S1 to produkter", #products1, 2)
|
||||
check("S1 SUGAR available", products1[1].available, 150000)
|
||||
check("S1 SUGAR level", products1[1].level, 150000)
|
||||
check("S1 SUGAR capacity", products1[1].capacity, 180000)
|
||||
check("S1 SUGAR bulk", products1[1].isPallet, false)
|
||||
check("S1 SUGAR kan også stå på paller", products1[1].canPallet, true)
|
||||
check("S1 SUGAR paller ved spawner (ingen endnu)", products1[1].palletLiters, 0)
|
||||
check("S1 TMR-lignende bulk kan ikke paller", products1[2].canPallet, true)
|
||||
check("S1 BASKET er palle", products1[2].isPallet, true)
|
||||
check("S1 BASKET tomt", products1[2].available, 0)
|
||||
|
||||
-- S2: silo
|
||||
local source2 = ADSources.identify(adEnv, 2, 1)
|
||||
check("S2 silo", source2.kind, "silo")
|
||||
local products2 = ADSources.getProducts(adEnv, source2, 1)
|
||||
check("S2 én vare", #products2, 1)
|
||||
check("S2 FORAGE", products2[1].fillType, FORAGE)
|
||||
check("S2 level", products2[1].level, 74500)
|
||||
check("S2 capacity", products2[1].capacity, 12000000)
|
||||
|
||||
-- S3: objektlager 20 m fra markøren
|
||||
local source3 = ADSources.identify(adEnv, 3, 1)
|
||||
check("S3 objektlager", source3.kind, "objectStorage")
|
||||
local products3 = ADSources.getProducts(adEnv, source3, 1)
|
||||
check("S3 to varer", #products3, 2)
|
||||
check("S3 kurve available", products3[1].available, 8000)
|
||||
check("S3 kurve er palle", products3[1].isPallet, true)
|
||||
check("S3 level = antal objekter", products3[1].level, 10)
|
||||
check("S3 capacity", products3[1].capacity, 4000)
|
||||
check("S3 baller tæller som palle", products3[2].isPallet, true)
|
||||
local index, count = ADSources.getObjectInfoIndex(source3, BASKET)
|
||||
check("S3 objectInfo-indeks", index, 1)
|
||||
check("S3 antal", count, 8)
|
||||
check("S3 countStored", ADSources.countStored(source3, FORAGE), 2)
|
||||
check("S3 ukendt vare", ADSources.getObjectInfoIndex(source3, SUGAR), nil)
|
||||
|
||||
-- S4: paller ved fabrikkens spawner tæller med (inden for 25 m af spawn-noden)
|
||||
addPallet(BASKET, 1000, 112, 100)
|
||||
addPallet(BASKET, 1000, 115, 105)
|
||||
addPallet(BASKET, 1000, 160, 100) -- 50 m væk: tæller ikke
|
||||
addPallet(BASKET, 1000, 112, 102, 2) -- anden farm: tæller ikke
|
||||
local products4 = ADSources.getProducts(adEnv, source1, 1)
|
||||
check("S4 paller ved spawner", products4[2].available, 2000)
|
||||
check("S4 antal paller ved spawner", products4[2].palletCount, 2)
|
||||
check("S3 antal paller i objektlager", products3[1].palletCount, 8)
|
||||
check("S4 bulk uændret", products4[1].available, 150000)
|
||||
addPallet(SUGAR, 2000, 111, 101)
|
||||
local products4b = ADSources.getProducts(adEnv, source1, 1)
|
||||
check("S4 sukkerpaller tælles separat", products4b[1].palletLiters, 2000)
|
||||
check("S4 bulk-mængde uden paller", products4b[1].available, 150000)
|
||||
|
||||
-- S5: salgssteder = kun markører hvis aflæsningsstation er et rigtigt salgssted
|
||||
local sells = ADSources.getSellMarkers(adEnv, 1)
|
||||
check("S5 ét salgssted", #sells, 1)
|
||||
check("S5 markør", sells[1].marker.id, 5)
|
||||
check("S5 tager SUGAR", ADSources.stationAccepts(sells[1].station, SUGAR), true)
|
||||
check("S5 tager ikke FORAGE", ADSources.stationAccepts(sells[1].station, FORAGE), false)
|
||||
-- palletriggerens varer ligger i supportedFillTypes (det AutoDrive bruger), ikke i acceptedFillTypes (kun bulk-trigger)
|
||||
local palletSell = {acceptedFillTypes = {[SUGAR] = true}, supportedFillTypes = {[SUGAR] = true, [BASKET] = true}}
|
||||
check("S5 pallevare via supportedFillTypes", ADSources.stationAccepts(palletSell, BASKET), true)
|
||||
check("S5 ukendt vare stadig nej", ADSources.stationAccepts(palletSell, FORAGE), false)
|
||||
check("S5 acceptedNames", ADSources.describeAccepted(palletSell), "STRAW_BASKET, SUGAR")
|
||||
|
||||
-- S6: spawnOne kalder spillets udtag med 1 objekt og uden klientforbindelse
|
||||
check("S6 spawn ok", ADSources.spawnOne(source3, 1), true)
|
||||
check("S6 indeks", spawnCalls[1][1], 1)
|
||||
check("S6 antal", spawnCalls[1][2], 1)
|
||||
check("S6 connection nil", spawnCalls[1][3], nil)
|
||||
check("S6 fejl -> false", ADSources.spawnOne({kind = "objectStorage", placeable = {removeAbstractObjectsFromStorage = function() error("x") end}}, 1), false)
|
||||
|
||||
-- S7: markør uden station og >40 m fra objektlager -> nil
|
||||
check("S7 ingen kilde", ADSources.identify(adEnv, 4, 1), nil)
|
||||
check("S7 ukendt waypoint", ADSources.identify(adEnv, 99, 1), nil)
|
||||
|
||||
-- S8: hjælpere
|
||||
check("S8 isPalletFillType palle", ADSources.isPalletFillType(BASKET), true)
|
||||
check("S8 isPalletFillType palletype uden fil", ADSources.isPalletFillType(SUGAR), true)
|
||||
check("S8 isPalletFillType bulk", ADSources.isPalletFillType(FORAGE), false)
|
||||
-- C1: palle/bulk afgøres pr. kilde: fabrik med læssestation der tager varen = bulk; silo = altid bulk; objektlager = palle
|
||||
check("S8 sukker ved fabrik med læssestation = bulk", ADSources.isPalletAt(source1, SUGAR), false)
|
||||
check("S8 kurve ved fabrik (station tager dem ikke) = palle", ADSources.isPalletAt(source1, BASKET), true)
|
||||
check("S8 silo altid bulk", ADSources.isPalletAt(source2, SUGAR), false)
|
||||
check("S8 objektlager altid palle", ADSources.isPalletAt(source3, FORAGE), true)
|
||||
-- S9 (I1): ren pallefabrik uden læssestation genkendes via afstand til spawner/rootNode
|
||||
local basketFactory = {
|
||||
rootNode = {x = 600, z = 600}, getName = function() return "Straw factory" end,
|
||||
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = BASKET, amount = 1}}}}, storage = storageOf({[BASKET] = 0}, {[BASKET] = 8000}), palletSpawner = {spawnPlaces = {{startNode = {x = 610, z = 600}}}}}},
|
||||
}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, basketFactory)
|
||||
addMarker(7, "Straw factory pickup", 605, 590)
|
||||
local source9 = ADSources.identify(adEnv, 7, 1)
|
||||
check("S9 pallefabrik uden station = factory", source9.kind, "factory")
|
||||
check("S9 station nil", source9.station, nil)
|
||||
local products9 = ADSources.getProducts(adEnv, source9, 1)
|
||||
check("S9 kurve er palle", products9[1].isPallet, true)
|
||||
check("S9 fabrik foretrækkes frem for objektlager i nærheden", ADSources.identify(adEnv, 7, 1).placeable, basketFactory)
|
||||
-- S9b: fabrik fundet på afstand, men produktionspunktet HAR en læssestation (sukkerfabrik hvor markøren
|
||||
-- ikke rammer triggeren): stationen hentes fra produktionspunktet, og sukker er bulk, ikke paller
|
||||
local mill2Storage = storageOf({[SUGAR] = 1000}, {[SUGAR] = 180000})
|
||||
local mill2 = {
|
||||
rootNode = {x = 1200, z = 1200}, getName = function() return "Sugar mill 2" end,
|
||||
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = SUGAR, amount = 1}}}}, storage = mill2Storage,
|
||||
loadingStation = {supportedFillTypes = {[SUGAR] = true}}}},
|
||||
}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, mill2)
|
||||
addMarker(9, "Mill 2 load", 1210, 1190)
|
||||
local source9b = ADSources.identify(adEnv, 9, 1)
|
||||
check("S9b fabrik", source9b.kind, "factory")
|
||||
check("S9b station fra produktionspunktet", source9b.station, mill2.spec_productionPoint.productionPoint.loadingStation)
|
||||
check("S9b sukker = bulk", ADSources.isPalletAt(source9b, SUGAR), false)
|
||||
check("S9b produkt bulk", ADSources.getProducts(adEnv, source9b, 1)[1].isPallet, false)
|
||||
local excluded = ADSources.getSourcePlaceables({source1, source2})
|
||||
check("S8 udelukkede placeables", excluded[factory] and excluded[silo], true)
|
||||
|
||||
-- S10 (paller til lager): objektlager med plads der tager varen; nærmeste nåelige fra traktoren
|
||||
objectStorage.getObjectStorageSupportsFillType = function(self, ft) return ft == BASKET or ft == FORAGE end
|
||||
check("S10 tager kurve", ADSources.objectStorageAccepts(objectStorage, BASKET), true)
|
||||
check("S10 tager ikke sukker", ADSources.objectStorageAccepts(objectStorage, SUGAR), false)
|
||||
check("S10 fri pladser", ADSources.objectStorageFreeSlots(objectStorage), 3990)
|
||||
local fullShed = {rootNode = {x = 700, z = 700}, spec_objectStorage = {capacity = 10, numStoredObjects = 10, objectInfos = {}}, getName = function() return "Fuldt skur" end}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, fullShed)
|
||||
addMarker(8, "Fuldt skur", 700, 690)
|
||||
check("S10 fuldt skur 0 pladser", ADSources.objectStorageFreeSlots(fullShed), 0)
|
||||
local truck = {rootNode = {x = 650, z = 650}}
|
||||
local store = ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return true end, 1)
|
||||
check("S10 nærmeste med plads = Objektlager (skuret er fuldt)", store.wayPointId, 3)
|
||||
check("S10 navn", store.name, "Objektlager")
|
||||
check("S10 udelukket kilde -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {[objectStorage] = true}, function() return true end, 1), nil)
|
||||
check("S10 uopnåelig -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return false end, 1), nil)
|
||||
check("S10 sukker: intet skur tager det", ADSources.findPalletStore(adEnv, truck, SUGAR, {}, function() return true end, 1), nil)
|
||||
|
||||
-- S11 (reserve): gårdens forbrug det næste døgn af en vare, minus hvad forbrugerne allerede har
|
||||
local BEET, STRAW, WATER = 9, 10, 11
|
||||
DESC[BEET] = {index = BEET, name = "SUGARBEET", title = "Roer"}; DESC[STRAW] = {index = STRAW, name = "STRAW", title = "Halm"}; DESC[WATER] = {index = WATER, name = "WATER", title = "Vand"}
|
||||
local tmrStorage = storageOf({[BEET] = 20000, [FORAGE] = 500000}, {[BEET] = 2500000, [FORAGE] = 3500000})
|
||||
local tmrFactory = {
|
||||
rootNode = {x = 800, z = 800}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "TMR-fabrik" end,
|
||||
spec_productionPoint = {productionPoint = {
|
||||
productions = {{id = "tmr3", cyclesPerMinute = 200 / 60, inputs = {{type = BEET, amount = 45}}, outputs = {{type = FORAGE, amount = 240}}}},
|
||||
storage = tmrStorage,
|
||||
getIsProductionEnabled = function(self, id) return id == "tmr3" end,
|
||||
}},
|
||||
}
|
||||
local cowBarn = {
|
||||
rootNode = {x = 900, z = 900}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
|
||||
spec_husbandryStraw = {inputLitersPerHour = 500}, spec_husbandryWater = {litersPerHour = 100, automaticWaterSupply = true},
|
||||
getHusbandryIsFillTypeSupported = function(self, ft) return ft == STRAW or ft == WATER end,
|
||||
getHusbandryFillLevel = function(self, ft) return ft == STRAW and 2000 or 0 end,
|
||||
getHusbandryFreeCapacity = function(self, ft) return ft == STRAW and 19375 or 0 end,
|
||||
}
|
||||
local otherFarmBarn = {rootNode = {x = 950, z = 950}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end, spec_husbandryStraw = {inputLitersPerHour = 500}, getHusbandryIsFillTypeSupported = function() return true end, getHusbandryFillLevel = function() return 0 end, getHusbandryFreeCapacity = function() return 99999 end}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, tmrFactory)
|
||||
table.insert(g_currentMission.placeableSystem.placeables, cowBarn)
|
||||
table.insert(g_currentMission.placeableSystem.placeables, otherFarmBarn)
|
||||
local demand = ADSources.getFarmDemand(adEnv, BEET, 1, {})
|
||||
-- 45 l × (200/60) pr. min × 1440 min = 216.000 l/døgn; har 20.000 -> 196.000
|
||||
check("S11 roer: døgnforbrug minus beholdning", demand.liters, 196000)
|
||||
check("S11 forbruger-navn", demand.consumers[1], "TMR-fabrik")
|
||||
local strawDemand = ADSources.getFarmDemand(adEnv, STRAW, 1, {})
|
||||
-- 500 l/t × 24 = 12.000; har 2.000 -> 10.000 (under fri plads 19.375)
|
||||
check("S11 halm: stald", strawDemand.liters, 10000)
|
||||
check("S11 anden farm tæller ikke", #strawDemand.consumers, 1)
|
||||
check("S11 automatisk vand = intet behov", ADSources.getFarmDemand(adEnv, WATER, 1, {}).liters, 0)
|
||||
check("S11 output er ikke behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 0)
|
||||
check("S11 udelukket forbruger", ADSources.getFarmDemand(adEnv, BEET, 1, {[tmrFactory] = true}).liters, 0)
|
||||
tmrStorage.fillLevels[BEET] = 300000
|
||||
check("S11 nok på lager -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
|
||||
tmrStorage.fillLevels[BEET] = 20000
|
||||
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function() return false end
|
||||
check("S11 slukket produktion -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
|
||||
|
||||
-- S11b: staldens OUTPUT (mælk, gylle) er aldrig et behov — kun varer i foderkrybben er foder
|
||||
local MILK_OUT = 13
|
||||
local feedBarn = {
|
||||
rootNode = {x = 960, z = 960}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
|
||||
spec_husbandryFood = {litersPerHour = 2000, fillLevels = {[FORAGE] = 1000}},
|
||||
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK_OUT or ft == FORAGE end,
|
||||
getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or ft == FORAGE and 1000 or 0 end,
|
||||
getHusbandryFreeCapacity = function(self, ft) return 9999999 end,
|
||||
}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, feedBarn)
|
||||
check("S11b mælk er ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
|
||||
check("S11b foder er behov (2000 l/t × 24 − 1000)", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 47000)
|
||||
-- tom krybbe: foderet kendes fra dyretypens foderdefinition
|
||||
feedBarn.spec_husbandryFood.fillLevels = {}
|
||||
feedBarn.getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or 0 end
|
||||
g_currentMission.animalFoodSystem = {getAnimalFood = function() return {groups = {{fillTypes = {FORAGE}}}} end}
|
||||
check("S11b tom krybbe: foder stadig behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 48000)
|
||||
check("S11b tom krybbe: mælk stadig ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
|
||||
g_currentMission.animalFoodSystem = nil
|
||||
table.remove(g_currentMission.placeableSystem.placeables)
|
||||
|
||||
-- S12 (inventar): alt gården har liggende, pr. bygning, kun bygninger med indhold
|
||||
factory.ownerFarmId = 1; silo.ownerFarmId = 1; objectStorage.ownerFarmId = 1
|
||||
local inventory = ADSources.getFarmInventory(adEnv, 1)
|
||||
local byName = {}
|
||||
for _, entry in ipairs(inventory) do byName[entry.name] = entry end
|
||||
check("S12 sukkerfabrik med", byName["Sugar mill"] ~= nil, true)
|
||||
check("S12 fabrik-kind", byName["Sugar mill"].kind, "factory")
|
||||
check("S12 fabrik sukker", byName["Sugar mill"].entries[1].level, 150000)
|
||||
check("S12 fabrik sukker er output", byName["Sugar mill"].entries[1].isOutput, true)
|
||||
local beetEntry
|
||||
for _, e in ipairs(byName["TMR-fabrik"].entries) do if e.fillType == BEET then beetEntry = e end end
|
||||
check("S12 TMR-fabrik roer er input", beetEntry.isOutput, false)
|
||||
check("S12 outputs først", byName["TMR-fabrik"].entries[1].isOutput, true)
|
||||
check("S12 silo", byName["Farma"].entries[1].fillType, FORAGE)
|
||||
check("S12 objektlager kurve som antal", byName["Objektlager"].entries[1].count, 8)
|
||||
-- 10 af 4000 pladser optaget -> kurvene har 8 + 3990 ledige pladser; liter-kapacitet = pladser x 1000 l
|
||||
check("S12 objektlager pladser = egne + ledige", byName["Objektlager"].entries[1].slots, 3998)
|
||||
check("S12 objektlager kapacitet i liter", byName["Objektlager"].entries[1].capacity, 3998000)
|
||||
check("S12 stald halm", byName["Kostald"].entries[1].level, 2000)
|
||||
check("S12 stald kapacitet = niveau + fri", byName["Kostald"].entries[1].capacity, 21375)
|
||||
check("S12 tomt skur udeladt", byName["Fuldt skur"], nil)
|
||||
check("S12 anden farm udeladt", #inventory, 5)
|
||||
|
||||
-- S13: Wait-markører er hverken kilder eller lagermål, selv om et objektlager står inden for 40 m
|
||||
-- (in-game 23/9: 'cow wait 1' blev valgt som palle-lager -> Dragon holdt ved Wait med stropperne af)
|
||||
wayPoints[20] = {id = 20, x = 300, z = 275}
|
||||
table.insert(markers, {id = 20, markerIndex = #markers + 1, name = "cow wait 1", group = "cow wait"})
|
||||
check("S13 Wait-markør er ikke en kilde", ADSources.identify(adEnv, 20, 1), nil)
|
||||
local truck2 = {rootNode = {x = 300, z = 270}}
|
||||
local store13 = ADSources.findPalletStore(adEnv, truck2, BASKET, {}, function() return true end, 1)
|
||||
check("S13 palle-lager = objektlagerets egen markør, ikke Wait", store13.wayPointId, 3)
|
||||
|
||||
-- S14: stald som kilde — markør ved staldens læssestation (mælketank/gylle); varer = det stationen udleverer
|
||||
local MILK = 12
|
||||
DESC[MILK] = {index = MILK, name = "MILK", title = "Mælk"}
|
||||
local barnLevels = {[MILK] = 4200, [STRAW] = 2000}
|
||||
local milkBarn = {
|
||||
rootNode = {x = 1500, z = 1500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
|
||||
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
|
||||
getHusbandryCapacity = function(self, ft) return ft == MILK and 25000 or ft == STRAW and 21375 or 0 end,
|
||||
getHusbandryFreeCapacity = function(self, ft) return ft == MILK and 20800 or 0 end,
|
||||
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK or ft == STRAW end,
|
||||
}
|
||||
local milkStation = {owningPlaceable = milkBarn, supportedFillTypes = {[MILK] = true}}
|
||||
addMarker(10, "cow 1 milk", 1500, 1490); loadAt[10] = milkStation
|
||||
local source14 = ADSources.identify(adEnv, 10, 1)
|
||||
check("S14 stald", source14.kind, "husbandry")
|
||||
check("S14 navn", source14.name, "Kostald")
|
||||
local products14 = ADSources.getProducts(adEnv, source14, 1)
|
||||
check("S14 én vare (kun det tanken udleverer)", #products14, 1)
|
||||
check("S14 mælk", products14[1].fillType, MILK)
|
||||
check("S14 niveau", products14[1].level, 4200)
|
||||
check("S14 kapacitet", products14[1].capacity, 25000)
|
||||
check("S14 bulk", products14[1].isPallet, false)
|
||||
check("S14 isPalletAt stald = bulk", ADSources.isPalletAt(source14, MILK), false)
|
||||
check("S14 kilde-markør i listen", ADRunsController ~= nil and true or true, true)
|
||||
|
||||
-- S14b: mælketank-udvidelse (siloExtension) — læssestationen trækker fra stald + tank; niveauet skal
|
||||
-- være stationens samlede mængde, ikke kun staldens egen beholder
|
||||
milkStation.getAllFillLevels = function(self, farmId) return {[MILK] = 1275 + 136912}, 10025000 end
|
||||
local products14b = ADSources.getProducts(adEnv, source14, 1)
|
||||
check("S14b stald + udvidelsestank", products14b[1].level, 138187)
|
||||
check("S14b tilgængeligt = samlet", products14b[1].available, 138187)
|
||||
milkStation.getAllFillLevels = nil
|
||||
|
||||
-- S14c: mælketank (silo med udvidelses-lager) — tanken selv er tom, puljen via læssestationen har mælken
|
||||
local tank = {spec_silo = {storages = {{fillLevels = {}, getCapacity = function() return 10000000 end}}}}
|
||||
local tankSource = {kind = "silo", placeable = tank, wayPointId = 999, station = {getAllFillLevels = function() return {[MILK] = 500000}, 70000000 end}}
|
||||
local tankProducts = ADSources.getProducts(adEnv, tankSource, 1)
|
||||
check("S14c tom tank viser puljens mælk", tankProducts[1] and tankProducts[1].level, 500000)
|
||||
check("S14c kapacitet = puljens", tankProducts[1] and tankProducts[1].capacity, 70000000)
|
||||
|
||||
-- S14d: generelt lager (separatorhal): kilde via læssestationen, varer = stationens niveauer
|
||||
local DIGESTATE = 30
|
||||
DESC[DIGESTATE] = {index = DIGESTATE, name = "DIGESTATE", title = "Digestat"}
|
||||
local separator = {rootNode = {x = 7000, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Separator Hall" end, spec_manureSeparator = {}}
|
||||
local sepStation = {owningPlaceable = separator, supportedFillTypes = {[DIGESTATE] = true}, getAllFillLevels = function() return {[DIGESTATE] = 42000}, 200000 end}
|
||||
addMarker(77, "Digestate pickup", 7000, 10); loadAt[77] = sepStation
|
||||
local sepSource = ADSources.identify(adEnv, 77, 1)
|
||||
check("S14d separator = lager-kilde", sepSource and sepSource.kind, "storage")
|
||||
local sepProducts = sepSource and ADSources.getProducts(adEnv, sepSource, 1) or {}
|
||||
check("S14d digestat fra stationen", sepProducts[1] and sepProducts[1].fillType, DIGESTATE)
|
||||
check("S14d mængde", sepProducts[1] and sepProducts[1].level, 42000)
|
||||
|
||||
-- S14e: hønsehus — æg kommer som PALLER fra staldens egen pallespawner (spec_husbandryPallets), ingen
|
||||
-- læssestation ved æggene: markøren ved spawneren er en staldkilde, æggene er pallevarer
|
||||
local EGGS = 31
|
||||
DESC[EGGS] = {index = EGGS, name = "EGG", title = "Æg", isPallet = true}
|
||||
local coopSpawner = {spawnPlaces = {{startNode = {x = 8000, z = 0}}}}
|
||||
local coop = {rootNode = {x = 8040, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
|
||||
getHusbandryFillLevel = function() return 0 end,
|
||||
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = coopSpawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, coop)
|
||||
addPallet(EGGS, 1000, 8003, 2); addPallet(EGGS, 1000, 8006, 2); addPallet(EGGS, 800, 8009, 2)
|
||||
addMarker(78, "Egg Pickup", 8000, 12)
|
||||
local coopSource = ADSources.identify(adEnv, 78, 1)
|
||||
check("S14e hønsehus-kilde via pallespawner", coopSource and coopSource.kind, "husbandry")
|
||||
local coopProducts = coopSource and ADSources.getProducts(adEnv, coopSource, 1) or {}
|
||||
local eggs = nil
|
||||
for _, item in ipairs(coopProducts) do if item.fillType == EGGS then eggs = item end end
|
||||
check("S14e æg som pallevare", eggs and eggs.isPallet, true)
|
||||
check("S14e æg på paller ved spawneren", eggs and eggs.available, 2800)
|
||||
check("S14e tre paller", eggs and eggs.palletCount, 3)
|
||||
check("S14e isPalletAt æg", coopSource and ADSources.isPalletAt(coopSource, EGGS), true)
|
||||
|
||||
-- S15: forklaring af en markør til advarslen (hvad opslaget ser)
|
||||
check("S15 forklaring stald", ADSources.explainMarker(adEnv, 10, 1), "læssestation ved markøren hører til 'Kostald' (stald)")
|
||||
check("S15 forklaring ingen station", ADSources.explainMarker(adEnv, 4, 1), "ingen læssestation ved markøren, og ingen fabrik/objektlager inden for 40 m")
|
||||
loadAt[11] = {owningPlaceable = nil}
|
||||
wayPoints[11] = {id = 11, x = 2000, z = 2000}; table.insert(markers, {id = 11, markerIndex = #markers + 1, name = "løs", group = "g"})
|
||||
check("S15 station uden placeable", ADSources.explainMarker(adEnv, 11, 1), "læssestation ved markøren uden bygning (owningPlaceable = nil)")
|
||||
|
||||
-- S16: strengt opslag fejler, løst opslag (40 m, uanset retning) finder mælketanken -> stald; en SILO
|
||||
-- accepteres aldrig løst (tætte rækker -> nabosilo)
|
||||
local looseAt = {}
|
||||
ADSmartPickup.getLoadStationNearWayPointId = function(_, id, farmId, maxDistance) return looseAt[id] end
|
||||
addMarker(12, "cow 2 milk", 1600, 1600)
|
||||
looseAt[12] = milkStation
|
||||
check("S16 stald via løst opslag", ADSources.identify(adEnv, 12, 1).kind, "husbandry")
|
||||
addMarker(13, "silo løst", 1700, 1700)
|
||||
looseAt[13] = siloStation
|
||||
check("S16 silo aldrig via løst opslag", ADSources.identify(adEnv, 13, 1), nil)
|
||||
check("S16 forklaring nævner den løse station", ADSources.explainMarker(adEnv, 13, 1):find("silo", 1, true) ~= nil, true)
|
||||
|
||||
-- S17: stald med ÉN station og TO læssepunkter (mælk i den ene ende, gylle i den anden): hver markør
|
||||
-- giver kun de varer dens nærmeste læssepunkt udleverer (in-game 24/9, LargerAnimalBarns cowBarnBig)
|
||||
local SLURRY = 13
|
||||
DESC[SLURRY] = {index = SLURRY, name = "LIQUIDMANURE", title = "Gylle"}
|
||||
barnLevels[SLURRY] = 9000
|
||||
local twoEndBarn = {
|
||||
rootNode = {x = 2500, z = 2500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Stor kostald" end,
|
||||
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
|
||||
getHusbandryCapacity = function() return 25000 end,
|
||||
}
|
||||
local twoEndStation = {owningPlaceable = twoEndBarn, supportedFillTypes = {[MILK] = true, [SLURRY] = true},
|
||||
loadTriggers = {{triggerNode = {x = 2500, z = 2470}, fillTypes = {[MILK] = true}}, {triggerNode = {x = 2500, z = 2530}, fillTypes = {[SLURRY] = true}}}}
|
||||
addMarker(40, "Cow Milk", 2500, 2465); loadAt[40] = twoEndStation
|
||||
addMarker(41, "Cow Slurry", 2500, 2535); loadAt[41] = twoEndStation
|
||||
local milkEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1)
|
||||
check("S17 mælke-enden: én vare", #milkEnd, 1)
|
||||
check("S17 mælke-enden: mælk", milkEnd[1].fillType, MILK)
|
||||
local slurryEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 41, 1), 1)
|
||||
check("S17 gylle-enden: én vare", #slurryEnd, 1)
|
||||
check("S17 gylle-enden: gylle", slurryEnd[1].fillType, SLURRY)
|
||||
-- læssepunkt uden fillTypes-tabel: stationens varer som før
|
||||
twoEndStation.loadTriggers[1].fillTypes = nil
|
||||
check("S17 uden punkt-varer: stationens varer", #ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1), 2)
|
||||
|
||||
-- S18: gårdens forbrug og produktion pr. døgn (fabrikker + stalde) og bedste markedspris
|
||||
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function(self, id) return id == "tmr3" end
|
||||
tmrFactory.spec_productionPoint.productionPoint.productions[1].outputs = {{type = FORAGE, amount = 240}}
|
||||
cowBarn.spec_husbandryWater.automaticWaterSupply = false
|
||||
local rates = ADSources.getFarmRates(adEnv, 1)
|
||||
-- TMR-fabrik: 45 l roer × 200/60 cyklus/min × 1440 = 216.000 l/døgn; TMR ud 240 × 200/60 × 1440 = 1.152.000
|
||||
check("S18 roer forbrug", math.floor(rates[BEET].consumption + 0.5), 216000)
|
||||
check("S18 TMR produktion", math.floor(rates[FORAGE].production + 0.5), 1152000)
|
||||
check("S18 halm i stald 500 l/t", rates[STRAW].consumption, 12000)
|
||||
check("S18 vand 100 l/t", rates[WATER].consumption, 2400)
|
||||
check("S18 anden farm tæller ikke", rates[STRAW].consumption, 12000)
|
||||
-- pris: bedste salgssted der tager varen; top = prisen ved 100 % af kurven
|
||||
g_currentMission.storageSystem = {getUnloadingStations = function() return {sellStation, {isa = function() return true end, isSellingPoint = true, owningPlaceable = {}, acceptedFillTypes = {[SUGAR] = true}, getEffectiveFillTypePrice = function() return 2.0 end}} end}
|
||||
sellStation.getEffectiveFillTypePrice = function() return 1.5 end
|
||||
g_currentMission.environment = {currentPeriod = 1}
|
||||
DESC[SUGAR].economy = {factors = {0.8, 1.0}}
|
||||
local quote = ADSources.getMarketQuote(SUGAR)
|
||||
check("S18 bedste pris", quote.price, 2.0)
|
||||
check("S18 top ved 80 % af kurven", quote.top, 2.5)
|
||||
check("S18 intet salgssted -> nil", ADSources.getMarketQuote(FORAGE), nil)
|
||||
|
||||
-- S19: staldenes produktion pr. døgn fra dyrenes output-kurver (liter pr. dyr pr. døgn efter alder)
|
||||
local function curve(value) return {get = function(_, age) return age >= 12 and value or 0 end} end
|
||||
g_currentMission.animalSystem = {getSubTypeByIndex = function(_, index)
|
||||
if index == 1 then return {output = {milk = {fillType = MILK, curve = curve(360)}, liquidManure = curve(500)}} end
|
||||
end}
|
||||
g_fillTypeManager.getFillTypeIndexByName = function(_, name) for index, desc in pairs(DESC) do if desc.name == name then return index end end end
|
||||
local producingBarn = {
|
||||
rootNode = {x = 5000, z = 5000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
|
||||
getHusbandryFillLevel = function() return 0 end,
|
||||
getClusters = function() return {
|
||||
{getSubTypeIndex = function() return 1 end, getAge = function() return 24 end, getNumAnimals = function() return 10 end},
|
||||
{getSubTypeIndex = function() return 1 end, getAge = function() return 6 end, getNumAnimals = function() return 5 end}, -- kalve: 0
|
||||
} end,
|
||||
}
|
||||
table.insert(g_currentMission.placeableSystem.placeables, producingBarn)
|
||||
local rates2 = ADSources.getFarmRates(adEnv, 1)
|
||||
check("S19 mælk 10 køer × 360", rates2[MILK].production, 3600)
|
||||
check("S19 gylle via navn", rates2[SLURRY].production, 5000)
|
||||
|
||||
-- S19b: andre dyretyper — høns: æg som "pallets" med fillType-indeks + gødning via navn
|
||||
local EGG, MANURE_FT = 21, 22
|
||||
DESC[EGG] = {index = EGG, name = "EGG", title = "Æg"}
|
||||
DESC[MANURE_FT] = {index = MANURE_FT, name = "MANURE", title = "Gødning"}
|
||||
local baseGetSubType = g_currentMission.animalSystem.getSubTypeByIndex
|
||||
g_currentMission.animalSystem.getSubTypeByIndex = function(system, index)
|
||||
if index == 2 then return {output = {pallets = {fillType = EGG, curve = curve(30)}, manure = curve(10)}} end
|
||||
return baseGetSubType(system, index)
|
||||
end
|
||||
table.insert(g_currentMission.placeableSystem.placeables, {
|
||||
rootNode = {x = 6000, z = 6000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Hønsehus" end,
|
||||
getHusbandryFillLevel = function() return 0 end,
|
||||
getClusters = function() return {{subTypeIndex = 2, age = 12, numAnimals = 100}} end, -- felter i stedet for metoder
|
||||
})
|
||||
local rates3 = ADSources.getFarmRates(adEnv, 1)
|
||||
check("S19b æg 100 høns × 30", rates3[EGG] and rates3[EGG].production, 3000)
|
||||
check("S19b gødning via navn", rates3[MANURE_FT] and rates3[MANURE_FT].production, 1000)
|
||||
check("S19b mælk uændret", rates3[MILK].production, 3600)
|
||||
|
||||
-- S20: statistik tæller kun produktioner der KØRER (ikke "mangler input"/slukket), og rater
|
||||
-- ganges med spillets tidsjustering (1 / dage pr. periode)
|
||||
ProductionPoint = {PROD_STATUS = {INACTIVE = 0, RUNNING = 1, MISSING_INPUTS = 2}}
|
||||
local tmrProduction = tmrFactory.spec_productionPoint.productionPoint.productions[1]
|
||||
tmrProduction.status = ProductionPoint.PROD_STATUS.MISSING_INPUTS
|
||||
local idle = ADSources.getFarmRates(adEnv, 1)
|
||||
check("S20 fabrik der mangler input tæller ikke", idle[FORAGE], nil)
|
||||
tmrProduction.status = ProductionPoint.PROD_STATUS.RUNNING
|
||||
g_currentMission.environment = {timeAdjustment = 0.5}
|
||||
local running = ADSources.getFarmRates(adEnv, 1)
|
||||
check("S20 kørende fabrik halveret ved 2 dage/periode", math.floor(running[FORAGE].production + 0.5), 576000)
|
||||
check("S20 stald halveret", running[STRAW].consumption, 6000)
|
||||
g_currentMission.environment, ProductionPoint = nil, nil
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,80 +0,0 @@
|
||||
-- Kør: luajit tests/test_adStorageStats.lua (fra repo-roden)
|
||||
-- Lagerstatistik pr. vare: fyldning, værdi nu / ved toppris, gårdens samlede beholdning, dage tilbage.
|
||||
|
||||
dofile("FS25_ADSmartPickup/adStorageStats.lua")
|
||||
local S = ADStorageStats
|
||||
|
||||
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
|
||||
|
||||
-- ST1: fuld beregning
|
||||
local st = S.itemStats({fillType = 5, level = 150000, capacity = 180000}, 412000, {consumption = 130000, production = 48000}, {price = 1.6, top = 2.0})
|
||||
check("ST1 procent", st.percent, 83)
|
||||
check("ST1 værdi nu", st.valueNow, 240000)
|
||||
check("ST1 værdi top", st.valueTop, 300000)
|
||||
check("ST1 gårdens beholdning", st.farmTotal, 412000)
|
||||
check("ST1 dage på netto (412k / 82k)", math.floor(st.days * 10 + 0.5) / 10, 5.0)
|
||||
check("ST1 netto pr. døgn", st.net, -82000)
|
||||
|
||||
-- ST2: ingen salgssted, intet forbrug, ingen kapacitet
|
||||
local st2 = S.itemStats({fillType = 6, level = 5000, capacity = 0}, 5000, nil, nil)
|
||||
check("ST2 ingen pris -> nil", st2.valueNow, nil)
|
||||
check("ST2 intet forbrug -> dage nil", st2.days, nil)
|
||||
check("ST2 kapacitet 0 -> procent nil", st2.percent, nil)
|
||||
|
||||
-- ST3: bygningens samlede værdi (kun varer med pris)
|
||||
local total = S.buildingTotals({st, st2, S.itemStats({fillType = 7, level = 1000, capacity = 2000}, 1000, nil, {price = 2, top = 3})})
|
||||
check("ST3 samlet nu", total.valueNow, 242000)
|
||||
check("ST3 samlet top", total.valueTop, 303000)
|
||||
|
||||
-- ST4: gårdens totaler fra inventar
|
||||
local totals = S.farmTotals({{entries = {{fillType = 5, level = 100}, {fillType = 6, level = 5}}}, {entries = {{fillType = 5, level = 50}}}})
|
||||
check("ST4 sum pr. vare", totals[5], 150)
|
||||
check("ST4 anden vare", totals[6], 5)
|
||||
|
||||
-- ST5: foder-forbrug i et fælles trug fordeles efter truget indhold; tomt trug -> bedste foder
|
||||
local split = S.splitTroughConsumption(2400, {[116] = 3000, [7] = 1000}, {[116] = 1.0, [7] = 0.8})
|
||||
check("ST5 TMR 75 %", split[116], 1800)
|
||||
check("ST5 ensilage 25 %", split[7], 600)
|
||||
local empty = S.splitTroughConsumption(2400, {}, {[116] = 1.0, [7] = 0.8})
|
||||
check("ST5 tomt trug -> bedste foder", empty[116], 2400)
|
||||
check("ST5 tomt trug -> intet på ensilage", empty[7], nil)
|
||||
|
||||
-- ST6: gitter — varer i rækker á n celler, tom liste giver én tom række
|
||||
local rows = S.chunk({"a", "b", "c", "d"}, 3)
|
||||
check("ST6 to rækker", #rows, 2)
|
||||
check("ST6 første række 3", #rows[1], 3)
|
||||
check("ST6 anden række 1", rows[2][1], "d")
|
||||
check("ST6 tom liste -> én tom række", #S.chunk({}, 3), 1)
|
||||
|
||||
-- ST7: rate-kolonne: forbrug med minus, produktion med plus, intet -> streg
|
||||
local fmt = function(v) return string.format("%.1fk", v / 1000) end
|
||||
check("ST7 begge", S.rateText(14400, 3600, fmt), "-14.4k +3.6k")
|
||||
check("ST7 kun prod", S.rateText(0, 3600, fmt), "+3.6k")
|
||||
check("ST7 kun forbrug", S.rateText(2000, 0, fmt), "-2.0k")
|
||||
check("ST7 intet", S.rateText(0, 0, fmt), "-")
|
||||
|
||||
-- ST8: fane pr. bygningstype — filtrér inventar på kind, rækkefølgen bevares
|
||||
local inv = {{kind = "silo", name = "A"}, {kind = "factory", name = "B"}, {kind = "silo", name = "C"}}
|
||||
local silos = S.filterKind(inv, "silo")
|
||||
check("ST8 to siloer", #silos, 2)
|
||||
check("ST8 rækkefølge", silos[2].name, "C")
|
||||
check("ST8 ingen stalde", #S.filterKind(inv, "husbandry"), 0)
|
||||
|
||||
-- ST9: produktion ≥ forbrug -> beholdningen stiger, ingen dage
|
||||
local rising = S.itemStats({fillType = 8, level = 1000, capacity = 2000}, 6700000, {consumption = 11500000, production = 19200000}, nil)
|
||||
check("ST9 stiger -> dage nil", rising.days, nil)
|
||||
check("ST9 stiger-flag", rising.rising, true)
|
||||
check("ST1 falder -> ikke stiger", st.rising, false)
|
||||
check("ST2 intet forbrug -> ikke stiger", st2.rising, false)
|
||||
|
||||
-- ST3: palleskur — procent af PLADSER (24 paller af 4000), ikke liter mod pladsantal (600 %)
|
||||
local egg = S.itemStats({fillType = 8, level = 24000, capacity = 4000000, count = 24, slots = 4000}, 24000, nil, nil)
|
||||
check("ST3 paller med", egg.count, 24)
|
||||
check("ST3 pladser med", egg.slots, 4000)
|
||||
check("ST3 procent af pladser", egg.percent, 1)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,248 +0,0 @@
|
||||
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
|
||||
-- Tester de rene behovs-funktioner; ingen spil- eller AutoDrive-mocks nødvendige.
|
||||
|
||||
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)
|
||||
|
||||
-- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først
|
||||
check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true)
|
||||
check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true)
|
||||
check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false)
|
||||
check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false)
|
||||
check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false)
|
||||
check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true)
|
||||
check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false)
|
||||
check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true)
|
||||
check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true)
|
||||
|
||||
-- Fabriksstyret forsyning: hvilke produktioner "ønsker" input?
|
||||
local MISSING_INPUTS, INACTIVE = 2, 0
|
||||
check("tændt produktion ønskes", ADSupplyPlanner.isProductionWanted(true, INACTIVE, MISSING_INPUTS), true)
|
||||
check("auto-slukket pga. mangel ønskes stadig", ADSupplyPlanner.isProductionWanted(false, MISSING_INPUTS, MISSING_INPUTS), true)
|
||||
check("manuelt slukket ønskes ikke", ADSupplyPlanner.isProductionWanted(false, INACTIVE, MISSING_INPUTS), false)
|
||||
check("ukendt status uden tænd -> ikke ønsket", ADSupplyPlanner.isProductionWanted(false, nil, MISSING_INPUTS), false)
|
||||
|
||||
-- collectProductionDemand: liter/min pr. vare fra ønskede produktioner, kun det vognen kan bære
|
||||
local STRAW, MINERAL, WATER = 23, 24, 25
|
||||
local productions = {
|
||||
{id = "tmr", cyclesPerMinute = 2, inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}, {type = MINERAL, amount = 1}}},
|
||||
{id = "mix", cyclesPerMinute = 1, inputs = {{type = SILAGE, amount = 50}, {type = WATER, amount = 500}}},
|
||||
{id = "off", cyclesPerMinute = 5, inputs = {{type = HAY, amount = 100}}},
|
||||
}
|
||||
local wanted = {tmr = true, mix = true}
|
||||
local canCarry = {[SILAGE] = true, [STRAW] = true, [MINERAL] = true, [HAY] = true}
|
||||
local demand = ADSupplyPlanner.collectProductionDemand(productions, function(p) return wanted[p.id] == true end, canCarry)
|
||||
check("forbrug summeres på tværs af produktioner", demand[SILAGE], 250)
|
||||
check("forbrug ganges med cyklusser/min", demand[STRAW], 20)
|
||||
check("uønsket produktion tæller ikke", demand[HAY], nil)
|
||||
check("vare vognen ikke kan bære udelades", demand[WATER], nil)
|
||||
check("tom produktionsliste -> tomt behov", next(ADSupplyPlanner.collectProductionDemand({}, function() return true end, canCarry)), nil)
|
||||
check("produktion uden inputs tåles", next(ADSupplyPlanner.collectProductionDemand({{id = "x", cyclesPerMinute = 1}}, function() return true end, canCarry)), nil)
|
||||
|
||||
-- minutesToEmpty
|
||||
check("minutter til tom", ADSupplyPlanner.minutesToEmpty(1000, 250), 4)
|
||||
check("intet forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, 0), nil)
|
||||
check("ukendt forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, nil), nil)
|
||||
|
||||
-- rankNeeds med forbrug: den der løber tør først vinder, selv om den er relativt fyldest
|
||||
local timed = ADSupplyPlanner.rankNeeds({
|
||||
{fillType = MINERAL, level = 500, capacity = 10000, litersPerMinute = 2}, -- 250 min, 95 % tom
|
||||
{fillType = SILAGE, level = 50000, capacity = 100000, litersPerMinute = 250}, -- 200 min, 50 % tom
|
||||
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, -- 4500 min
|
||||
})
|
||||
check("kortest tid til tom først", timed[1].fillType, SILAGE)
|
||||
check("dernæst næstkortest", timed[2].fillType, MINERAL)
|
||||
check("minutesToEmpty følger med", timed[1].minutesToEmpty, 200)
|
||||
check("vare med forbrug slår vare uden", ADSupplyPlanner.rankNeeds({
|
||||
{fillType = HAY, level = 0, capacity = 100000},
|
||||
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20},
|
||||
})[1].fillType, STRAW)
|
||||
check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({
|
||||
{fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20},
|
||||
}), 0)
|
||||
|
||||
-- isProductionInput: en fabriks input må aldrig stjæles som kilde — medmindre
|
||||
-- fabrikken også selv producerer varen (så er den et output, der må hentes)
|
||||
local TMR = 26
|
||||
local factory = {
|
||||
{id = "tmr", inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}}, outputs = {{type = TMR, amount = 50}}},
|
||||
{id = "pellets", inputs = {{type = TMR, amount = 10}}, outputs = {{type = HAY, amount = 5}}},
|
||||
}
|
||||
check("input til fabrikken er beskyttet", ADSupplyPlanner.isProductionInput(factory, STRAW), true)
|
||||
check("output må hentes", ADSupplyPlanner.isProductionInput(factory, HAY), false)
|
||||
check("både input og output (kæde) -> må hentes", ADSupplyPlanner.isProductionInput(factory, TMR), false)
|
||||
check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProductionInput(factory, WATER), false)
|
||||
check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false)
|
||||
check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false)
|
||||
|
||||
-- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende
|
||||
local sized = ADSupplyPlanner.rankNeeds({
|
||||
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri
|
||||
{fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri
|
||||
}, 40000)
|
||||
check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR)
|
||||
check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW)
|
||||
check("fits markeres", sized[1].fits, true)
|
||||
check("ikke-fits markeres", sized[2].fits, false)
|
||||
local noneFit = ADSupplyPlanner.rankNeeds({
|
||||
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500},
|
||||
{fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50},
|
||||
}, 40000)
|
||||
check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW)
|
||||
check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil)
|
||||
|
||||
-- Wait-punkter: MAPPEN har ordet "Wait" (uanset store/små); markørnavnet er ligegyldigt.
|
||||
-- (Brugeren har mark-ventepladser som "F91 Wait" i mark-mapperne — de må ikke ryge i puljen.)
|
||||
check("mappen 'Cow Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "Cow Wait", group = "Cow Wait"}), true)
|
||||
check("mappen 'Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true)
|
||||
check("mappen 'wait stalde' tæller", ADSupplyPlanner.isWaitMarker({name = "3", group = "wait stalde"}), true)
|
||||
check("mark-venteplads i mark-mappe tæller IKKE", ADSupplyPlanner.isWaitMarker({name = "F91 Wait", group = "Græs Marker"}), false)
|
||||
check("mappen 'Waiter' tæller ikke", ADSupplyPlanner.isWaitMarker({name = "x", group = "Waiter"}), false)
|
||||
check("uden mappe -> nej", ADSupplyPlanner.isWaitMarker({name = "Wait 1"}), false)
|
||||
|
||||
-- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle}
|
||||
local V1, V2 = {}, {}
|
||||
local waitPoints = {
|
||||
{marker = {id = 1, name = "Wait A"}, x = 0, z = 0},
|
||||
{marker = {id = 2, name = "Wait B"}, x = 100, z = 0},
|
||||
{marker = {id = 3, name = "Wait C"}, x = 300, z = 0},
|
||||
}
|
||||
check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1)
|
||||
check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2)
|
||||
check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1)
|
||||
check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil)
|
||||
check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil)
|
||||
-- fysisk optaget: et køretøj holder på punktet uden reservation (fx efter omplanlægning) -> spring over
|
||||
local occ = ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = waitPoints[1].x + 2, z = waitPoints[1].z}}, 6)
|
||||
check("optaget punkt markeres", occ[waitPoints[1].marker.id], true)
|
||||
check("optaget springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, nil, occ).marker.id, 2)
|
||||
check("egen reservation vinder over optaget (det er en selv)", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1, nil, occ).marker.id, 1)
|
||||
check("køretøj langt væk optager intet", next(ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = 9999, z = 9999}}, 6)), nil)
|
||||
-- ét køretøj mellem to punkter optager kun det NÆRMESTE
|
||||
local between = ADSupplyPlanner.markOccupiedWaitPoints({{marker = {id = 7}, x = 0, z = 0}, {marker = {id = 8}, x = 6, z = 0}}, {{x = 2, z = 0}}, 6)
|
||||
check("kun nærmeste punkt optages", (between[7] == true and between[8] == nil), true)
|
||||
-- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv)
|
||||
local unreachableA = function(candidate) return candidate.marker.id ~= 1 end
|
||||
check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2)
|
||||
check("intet nåeligt -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, function() return false end), nil)
|
||||
|
||||
-- Hjem fra Wait: fri plads >= min(rest, halvdelen af målets kapacitet)
|
||||
-- (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald)
|
||||
check("hele resten passer -> hjem", ADSupplyPlanner.canUnloadRest(35000, 35000, 300000), true)
|
||||
check("resten passer ikke endnu -> vent", ADSupplyPlanner.canUnloadRest(5000, 20000, 50000), false)
|
||||
check("kæmpe rest: halv stald fri -> hjem", ADSupplyPlanner.canUnloadRest(150000, 700000, 300000), true)
|
||||
check("kæmpe rest: under halv stald fri -> vent", ADSupplyPlanner.canUnloadRest(100000, 700000, 300000), false)
|
||||
check("ukendt kapacitet -> kun hele resten tæller", ADSupplyPlanner.canUnloadRest(100000, 700000, nil), false)
|
||||
|
||||
-- canUnloadRest med run-tærskel: "under 30 %" = fri >= 70 % af kapaciteten
|
||||
check("tærskel 30 %: 60 % fri er ikke nok", ADSupplyPlanner.canUnloadRest(60000, 90000, 100000, 0.7), false)
|
||||
check("tærskel 30 %: 70 % fri er nok", ADSupplyPlanner.canUnloadRest(70000, 90000, 100000, 0.7), true)
|
||||
check("hele resten passer vinder altid", ADSupplyPlanner.canUnloadRest(5000, 5000, 100000, 0.7), true)
|
||||
check("uden tærskel = 50 %-reglen", ADSupplyPlanner.canUnloadRest(50000, 90000, 100000), true)
|
||||
|
||||
-- rankNeeds: fælles trug (scoreOverride) + fodervægt (priority): TMR før hø før græs,
|
||||
-- men et separat halmlager der er tommere vinder over truget
|
||||
local TMR, HAYX = 40, 41
|
||||
local trough = ADSupplyPlanner.rankNeeds({
|
||||
{fillType = GRASS, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.4},
|
||||
{fillType = TMR, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 1.0},
|
||||
{fillType = HAYX, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.8},
|
||||
{fillType = HAY, level = 20000, capacity = 100000}, -- halm-agtigt separat lager, 80 % tomt
|
||||
})
|
||||
check("separat lager 80 % tomt vinder over trug 30 % tomt", trough[1].fillType, HAY)
|
||||
check("i truget: TMR (1.0) først", trough[2].fillType, TMR)
|
||||
check("så hø (0.8)", trough[3].fillType, HAYX)
|
||||
check("så græs (0.4)", trough[4].fillType, GRASS)
|
||||
check("scoreOverride bruges som score", trough[2].score, 0.3)
|
||||
|
||||
-- Bedste foder først: lavere foder (ensilage 0,8) må kun hentes når truget er nødlidende
|
||||
-- (fri ≥ EMERGENCY_TROUGH_FREE af kapaciteten), hvis der er bedre foder (TMR 1,0) i samme trug
|
||||
local tmrNeed = {fillType = 116, priority = 1.0, score = 0.6, trough = true}
|
||||
local silageNeed = {fillType = 7, priority = 0.8, score = 0.6, trough = true}
|
||||
local strawNeed = {fillType = 31, priority = 0, score = 0.9}
|
||||
check("NF1 bedste foder tillades altid", ADSupplyPlanner.isFoodAllowed(tmrNeed, 1.0), true)
|
||||
check("NF1 ensilage afvist ved halvfuldt trug", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), false)
|
||||
silageNeed.score = 0.8
|
||||
check("NF1 ensilage tilladt når truget er næsten tomt", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), true)
|
||||
check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(strawNeed, 1.0), true)
|
||||
check("NF1 intet bedre foder på listen -> tilladt", ADSupplyPlanner.isFoodAllowed({fillType = 7, priority = 0.8, score = 0.1, trough = true}, 0.8), true)
|
||||
check("NF1 bedste foderprioritet", ADSupplyPlanner.bestFoodPriority({tmrNeed, silageNeed, strawNeed}), 1.0)
|
||||
|
||||
-- Læs på vej (andre traktorer): lægges oven i målets niveau; foder deler trug -> hele trugets fri plads falder
|
||||
local pendingNeeds = {
|
||||
{fillType = 31, level = 5000, capacity = 20000}, -- halm
|
||||
{fillType = 116, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- TMR, tomt trug
|
||||
{fillType = 7, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- ensilage
|
||||
}
|
||||
local adjusted = ADSupplyPlanner.applyPending(pendingNeeds, {[116] = 20000, [31] = 20000})
|
||||
check("AP halm: niveau + på vej, højst kapacitet", adjusted[1].level, 20000)
|
||||
check("AP TMR: niveau + på vej", adjusted[2].level, 20000)
|
||||
check("AP TMR: kapacitet = niveau + resterende fri", adjusted[2].capacity, 30000)
|
||||
check("AP ensilage: truget har kun 10000 fri tilbage", adjusted[3].capacity - adjusted[3].level, 10000)
|
||||
check("AP trug-score falder", math.floor(adjusted[3].scoreOverride * 100 + 0.5), 33)
|
||||
check("AP input urørt", pendingNeeds[2].level, 0)
|
||||
check("AP intet på vej -> samme værdier", ADSupplyPlanner.applyPending(pendingNeeds, {})[1].level, 5000)
|
||||
local ranked = ADSupplyPlanner.rankNeeds(adjusted, 8000)
|
||||
check("AP halm fuld (på vej) -> kun trug-varer tilbage", #ranked, 2)
|
||||
|
||||
-- PF: parallelt foder (høns: korn 80 % / roesnitter 20 % af det de spiser) — hver gruppe har sin andel af truget
|
||||
do
|
||||
local WHEAT, BARLEY, BEETCUT = 1, 2, 3
|
||||
local groups = {{fillTypes = {WHEAT, BARLEY}, eatWeight = 0.8}, {fillTypes = {BEETCUT}, eatWeight = 0.2}}
|
||||
local levels = {[WHEAT] = 60000, [BARLEY] = 20000, [BEETCUT] = 0}
|
||||
local lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BEETCUT)
|
||||
check("PF roesnitter: andel 20k, har 0", lvl .. "/" .. cap, "0/20000")
|
||||
lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BARLEY)
|
||||
check("PF korn-gruppen samlet: 80k af 80k", lvl .. "/" .. cap, "80000/80000")
|
||||
check("PF ukendt vare -> nil", ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, 99), nil)
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,111 +0,0 @@
|
||||
-- Kør: luajit tests/test_adWaitPool.lua (fra repo-roden)
|
||||
-- Wait-puljen: kandidater pr. mappe, reservationer, nærmeste ledige, rutetjek, redirect.
|
||||
|
||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||
dofile("FS25_ADSmartPickup/adWaitPool.lua")
|
||||
|
||||
local failures = 0
|
||||
local function check(name, actual, expected)
|
||||
if actual == expected then
|
||||
print("OK " .. name)
|
||||
else
|
||||
failures = failures + 1
|
||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local markers, wayPoints = {}, {}
|
||||
local function addMarker(id, name, group, x, z)
|
||||
wayPoints[id] = {id = id, x = x, z = z, incoming = {}}
|
||||
local marker = {id = id, markerIndex = #markers + 1, name = name, group = group}
|
||||
table.insert(markers, marker)
|
||||
return marker
|
||||
end
|
||||
local unreachable = {}
|
||||
local adEnv = {
|
||||
ADGraphManager = {
|
||||
getMapMarkers = function() return markers end,
|
||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
|
||||
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
|
||||
},
|
||||
}
|
||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
||||
|
||||
addMarker(10, "cow wait 1", "cow wait", 0, 0)
|
||||
addMarker(11, "cow wait 2", "cow wait", 50, 0)
|
||||
addMarker(20, "Wait 3", "General Wait", 10, 0)
|
||||
addMarker(30, "silo 1", "silo", 5, 0)
|
||||
|
||||
-- W1: kandidater
|
||||
check("W1 kun mappen", #ADWaitPool.getCandidates(adEnv, "cow wait"), 2)
|
||||
check("W1 tom pulje = alle Wait-mapper", #ADWaitPool.getCandidates(adEnv, ""), 3)
|
||||
check("W1 nil = alle", #ADWaitPool.getCandidates(adEnv, nil), 3)
|
||||
|
||||
-- W2: nærmeste ledige fra vognens position; reserverede springes over; egen reservation OK
|
||||
local v1, v2 = {name = "v1", rootNode = {x = 45, z = 0}}, {name = "v2", rootNode = {x = 45, z = 0}}
|
||||
check("W2 nærmeste", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
|
||||
ADWaitPool.claim(11, v1)
|
||||
check("W2 reserveret springes over", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 10)
|
||||
check("W2 egen reservation genbruges", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
|
||||
|
||||
-- W3: claim / owner / release
|
||||
check("W3 ejer", ADWaitPool.getOwner(11), v1)
|
||||
ADWaitPool.releaseVehicle(v1)
|
||||
check("W3 frigivet", ADWaitPool.getOwner(11), nil)
|
||||
check("W3 nu ledig for v2", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 11)
|
||||
|
||||
-- W4: nåelighed
|
||||
unreachable[11] = true
|
||||
check("W4 uopnåelig nærmeste -> næste", ADWaitPool.pickFree(adEnv, v2, "cow wait", function(c) return ADWaitPool.hasRouteTo(adEnv, v2, c.marker.id) end).marker.id, 10)
|
||||
check("W4 hasRouteTo false", ADWaitPool.hasRouteTo(adEnv, v2, 11), false)
|
||||
check("W4 hasRouteBothWays true", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
|
||||
check("W4 hasRouteBothWays hjem uopnåelig", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 11), false)
|
||||
unreachable[11] = nil
|
||||
check("W4 intet punkt -> nil", ADWaitPool.pickFree(adEnv, v2, "cow wait", function() return false end), nil)
|
||||
|
||||
-- W5: getClaims sorteret på mappe, dernæst id
|
||||
ADWaitPool.claim(20, v2)
|
||||
ADWaitPool.claim(10, v1)
|
||||
local claims = ADWaitPool.getClaims(adEnv)
|
||||
check("W5 to reservationer", #claims, 2)
|
||||
check("W5 første = General Wait", claims[1].marker.group, "General Wait")
|
||||
check("W5 anden = cow wait 1", claims[2].marker.id, 10)
|
||||
ADWaitPool.releaseVehicle(v1)
|
||||
ADWaitPool.releaseVehicle(v2)
|
||||
check("W5 alt frigivet", #ADWaitPool.getClaims(adEnv), 0)
|
||||
|
||||
-- W6: redirect stopper trigger-aktivitet og sætter sti
|
||||
local stopped = {}
|
||||
local task = {
|
||||
destinationID = 99, state = 1,
|
||||
vehicle = {ad = {
|
||||
trailerModule = {isActiveAtTrigger = function() return true end, stopUnloading = function() stopped.unload = true end, stopLoading = function() stopped.load = true end},
|
||||
drivePathModule = {setPathTo = function(_, id) stopped.pathTo = id end},
|
||||
}},
|
||||
}
|
||||
ADWaitPool.STATE_DRIVING = 2
|
||||
ADWaitPool.redirect(task, 10)
|
||||
check("W6 destination", task.destinationID, 10)
|
||||
check("W6 state driving", task.state, 2)
|
||||
check("W6 setPathTo", stopped.pathTo, 10)
|
||||
check("W6 stopUnloading", stopped.unload, true)
|
||||
|
||||
-- WR: AD's rutesøgning siger nej, men vejnettet har en vej fra traktorens nærmeste waypoint -> godtages
|
||||
-- (in-game 24/9: 'Æg storage' ved Chicken Wait 1, "ingen vej til 'Chicken Storage Unload'" trods vej)
|
||||
do
|
||||
wayPoints[901] = {id = 901, x = 0, z = 0, out = {902}}
|
||||
wayPoints[902] = {id = 902, x = 5, z = 0, out = {903}}
|
||||
wayPoints[903] = {id = 903, x = 10, z = 0, out = {}}
|
||||
wayPoints[904] = {id = 904, x = 20, z = 0, out = {}} -- ingen indgang
|
||||
unreachable[903], unreachable[904] = true, true
|
||||
local vehicle = {getClosestWayPoint = function() return 901, 1 end}
|
||||
check("WR AD nej, vejnettet ja -> nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 903), true)
|
||||
check("WR hverken AD eller vejnet -> ikke nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 904), false)
|
||||
check("WR uden nærmeste waypoint -> AD's svar", ADWaitPool.hasRouteTo(adEnv, {}, 903), false)
|
||||
unreachable[903], unreachable[904] = nil, nil
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
Reference in New Issue
Block a user