Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb287a8154 | |||
| e229a27624 | |||
| ba98cf83d3 | |||
| a6662d6da1 | |||
| bf3b99aca4 | |||
| cbc9b2c8e3 | |||
| 1bda82a270 | |||
| 21b48d7664 | |||
| 88ddb3d6cd | |||
| 37cf84c101 | |||
| 991252848f | |||
| 6b533e62eb | |||
| c92c870297 | |||
| 18b6c82871 | |||
| 47afdebf64 | |||
| 2290bdde95 | |||
| 0c21ba49a0 | |||
| e1f7b19fef | |||
| 74537f7ec2 | |||
| d2970a1bda | |||
| bd106a2176 | |||
| acf94c8e88 | |||
| a3d234a3a2 | |||
| 68eb26d71d | |||
| 1c60529dfd | |||
| b33f2a3b01 | |||
| d2a4cf0d23 | |||
| 928299c8ee | |||
| e9f1251471 | |||
| 9fde03f950 | |||
| 979b7f72d5 | |||
| ba2a3e94f1 | |||
| 131b1ad10c | |||
| 19a10ab67c | |||
| 1e7b2ec699 | |||
| 817fc388a2 | |||
| 83394aabfd | |||
| 9e34a16dfa | |||
| d2453f78fb | |||
| 605c4e8d45 | |||
| 9c9c2badeb | |||
| e394a77833 | |||
| b77bda5334 | |||
| f0b8df8ae3 | |||
| b389fa3564 | |||
| 32f2886a55 | |||
| 2a7769d090 | |||
| 49eed51202 | |||
| 6dd1b5cac0 | |||
| 217498aefa | |||
| 2e7a7105c1 | |||
| 27017c0f62 | |||
| f813d4b3d8 | |||
| 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 | |||
| 944e61d1a8 | |||
| b1a65929a7 | |||
| ae1362d164 | |||
| 65986b1793 | |||
| 969db0a699 | |||
| 7b0a9e452b | |||
| 02e355e700 | |||
| 793352c4c4 | |||
| 9be60d4b1c | |||
| 9cab4b937d | |||
| 03c05b7608 | |||
| 7f83cc4d28 | |||
| cf7a2a51e5 | |||
| ba88eb5f99 | |||
| 4c88118e56 | |||
| 048f5e6547 | |||
| 52250dc35e | |||
| 6f387c051f | |||
| b1f1aaa148 | |||
| 716045a36b | |||
| bade84129b | |||
| 61a32720ea |
@@ -1 +1,5 @@
|
||||
*.zip
|
||||
docs/superpowers/
|
||||
bench/
|
||||
.superpowers/
|
||||
tests/
|
||||
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
# Æ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.62 — Kæder slås selv til
|
||||
|
||||
- **1.62.7** Nyt: konsolkommandoen `spuSellBlock` slår salgssteder fra og til (pr. savegame). Et salgssted, der er slået fra, bruges hverken til udkørsel eller i priserne. Til stationer, hvor aflæsningen går i stå, fx "Market Price Liquid Fill Station".
|
||||
- **1.62.6** Pålæsningsdiagnosen logger kun, når en trigger med varen er inden for 20 m (køretøjer der holder ved Wait-punkter eller marker gav falske meldinger).
|
||||
- Manualerne understreger nu, at parkering og hentning af redskaber kun virker ved at bakke: ruten til `P <navn>` og `Connect P <navn>` skal slutte som en bakrute.
|
||||
- **1.62.5** Diagnose: står AutoDrive 30 s ved et LOAD-mål uden at fylde, skriver loggen én linje med markør, vare og for hver vogn den nærmeste AD-læssetrigger (afstand, har varen, står vognen i triggerens boks). Ingen indgreb.
|
||||
- **1.62.4** Rettet: påfyldning af et langt sæt fik AutoDrive til at oprette ekstra markører med stationens navn (fx "Buying Station Multifruit"). Det gør AutoDrives `getMarkerAtStation`, når ingen markør passer til togets længde. Moden kalder den ikke længere; markøren findes ved stationens læssetriggere eller ved bygningen.
|
||||
- **1.62.3** Rettet: påfyldning ved købestation valgte en anden stations markør for lange sæt. AutoDrive vil have markøren togets længde + 3–13 m forbi triggeren, så Puma + Kinze 4900 ventede på frø ved "Market Price Liquid Fill Station" i stedet for "Buying Station Multifruit". Nu vælges den nærmeste markør ved stationens egne læssetriggere; P-, Connect- og Wait-markører springes over. Loggen skriver, hvor et tomt redskab fyldes.
|
||||
- **1.62.2** Kæderne læser fabrikkernes udgange (Sælg / Fordel / Behold). **Fordel:** spillet leverer selv til gårdens fabrikker, så kæden opretter intet flow, og fanen Kæder viser *fordeles af spillet*. **Behold** (spawn eller lager): et flow henter varen ved fabrikken som hidtil. **Sælg på en mellemvare** betyder, at næste led i kæden intet får, så kæden sætter den til Behold og henter den.
|
||||
- **1.62.1** Rettet: udkørsel fra objektlager (fx Sell Egg fra Æglageret) stoppede halvfuld med "spawner ikke … (30 s uden fremdrift)". Lageret deler samme vare i flere grupper (paller med forskellig fyldning), og moden talte kun den første gruppe — når en lille gruppe blev tom, så lageret ud til at vokse. Nu tælles alle grupper. Advarslen viser lager- og vogntal, hvis den alligevel opstår.
|
||||
- **1.62.0** **En kæde er noget du selv slår til:** tændte produktioner er ikke længere automatisk kæder (FS25 tænder nye produktioner som standard — 7 ensilagetårne gav 56 kæder og fik AIO-fabrikkens høproduktion tændt). Fanen **Kæder** har nederst *N tændte produktioner uden kæde* (foldet sammen); **[Kæde til]** gør en produktion til en kæde. En pauset kæde har **[Fjern]**. Ikke-valgte produktioner bruges stadig som kilde. Når en kæde fjernes, slettes dens tomme kædeflows (uden traktor), og mellemled kæden selv tændte slukkes igen. Rettet: kædens flowoversigt manglede traktorerne, så sammenlægning/oprydning kunne ramme et kædeflow med traktor. Udkørsel fra palleskur holder AutoDrive, mens moden tager paller ud.
|
||||
|
||||
## 1.61 — Produktionskæder
|
||||
|
||||
- **1.61.13** Udkørsel fra palleskur: mens moden tager paller ud (én ad gangen), holdes AutoDrives egen pålæsning, så vognen ikke kører halvfuld, når triggeren et øjeblik er tom. Pålæsningen slutter, når vognen er fuld, lageret er tomt, eller skuret ikke har udleveret noget i 30 s.
|
||||
- **1.61.12** Oversigt-tavlen folder også kædeflows uden traktor sammen og viser navnene uden "Kæde: "; et kædeflow uden traktor har status *mangler traktor/vogn* (tavle og Flows-liste).
|
||||
- **1.61.11** Markørvalg ved bygninger: når to markører står inden for 20 m af hinanden, afgør navnet — pålæsning foretrækker *Pickup/Hent/Læs/Load*, aflæsning *Unload/Drop/Aflæs*. Før vandt den nærmeste (Sell Egg kørte til "Chicken Storage Unload" i stedet for "Egg Storage Pickup").
|
||||
- **1.61.10** Markflådens puljeliste viser korte maskinnavne (uden " Unrealistic" og parenteser som "(+ Custom Crops)"), så lange linjer ikke ombrydes med traktorens nummer alene på næste linje.
|
||||
- **1.61.9** *Sælg fra* i udkørselsflows har valget **Altid (uanset pris)** øverst: flowet sælger, så snart der er noget at køre med, uden at vente på en god pris. Resumé og status viser *sælg altid*.
|
||||
- **1.61.8** Kæderne regner **læs i vogne og høstertanke** med som lager (afgrøder og markvarer), og der kommer ingen forslag eller automatiske markskift for en afgrøde, mens den høstes. Klik på en varelinje i fanen **Kæder** åbner flowet, der kører varen, i redigeringsvinduet (så en stoppet kæde kan få traktor og vogn).
|
||||
- **1.61.7** Kædelinjen viser *Gård* (uden klik) ved slutprodukter, gården selv bruger (fx ensilage, TMR, roesnitter), og *Sælg*/*Behold* kun ved varer, der faktisk kan sælges straks.
|
||||
- **1.61.6** Slutprodukter sælges kun straks, hvis gården ikke selv bruger varen (stald, tændt produktion, forbrug eller flow). Varer kæden allerede havde sat til salg, sættes automatisk tilbage til *Behold*, når de ikke længere må sælges (in-game: TMR og roesnitter blev sat til salg).
|
||||
- **1.61.5** Redskaber bredere end Courseplays 50 m (Unrealistic kan sættes til op til 120 m) afvises ikke længere: de bruges, og kursusbredden sættes til 50 m, før Courseplay laver kurset. Markflåden viser *kursus 50 m* ved dem.
|
||||
- **1.61.4** Kæder mere realistiske: **ét kædeflow pr. bygning** med alle bygningens manglende varer (før ét pr. vare og bygning); nye varer lægges i bygningens eksisterende kædeflow, og tidligere tomme kædeflows (uden traktor) til samme bygning lægges sammen. **Slutprodukter sælges straks** som standard (fabrikkens udgang sættes til *Sælg direkte*); feltet *Sælg*/*Behold* på kædelinjen skifter, og *Behold* sætter varer, kæden selv satte til salg, tilbage. I Flows-listen foldes kædeflows uden traktor sammen i én linje. En såmaskine med gødningstank (fx Kinze 4905) bruges kun til såning, ikke som gødningsspreder.
|
||||
- **1.61.3** Fanen **Kæder** sorteret: forslag til ny afgrøde står samlet øverst, én gang pr. mark og efter marknummer (før gentaget under hver kæde, der bruger afgrøden); kæderne står efter status (mangel/fejl, venter, dækket), så bygning og produkt. Kædelinjen viser stadig "· N forslag".
|
||||
- **1.61.2** Flows-listen: dine egne flows står øverst; kædernes auto-oprettede flows samles nederst under *Kædernes egne flows* og vises uden "Kæde: ", så bygningen kan ses.
|
||||
- **1.61.1** Fanen **Kæder** står nu efter **Marker**, og navne i kædelisten afkortes ikke længere (fx "+ Fermented Chaff", "+ Whole Crop Silage").
|
||||
- **1.61.0** Produktionskæder: slå et produkt til i en fabrik, og moden holder det kørende helt tilbage til marken. Kæden regner baglæns gennem dine egne fabrikker og stalde til afgrøderne og ser 12 måneder frem. Ny fane **Kæder** med træ pr. rod-produktion, farvet status, fold ud/ind og **[Pause]**/**[Start]**. Marker markeret **Fri til kæden** skifter afgrøde automatisk (højst 2 i minuttet); andre marker får forslag med **[Godkend]**/**[Afvis]** (højst 3 pr. afgrøde) i fanen og som ét samlet punkt i **Kræver handling**. En afgrøde, du selv har valgt, ændres aldrig. Mellemled tændes og slukkes kun igen, hvis kæden tændte dem. Flows **Kæde: <vare> → <bygning>** oprettes med en ledig pool-traktor og tipvogn (ellers stoppet: *mangler traktor/vogn*; paller altid stoppet) og stoppes, ikke slettes, når kæden slås fra. Dine egne tændte produktioner bruges som kilde først; slukker du en produktion, tænder kæden den ikke igen, før du tænder en ny. Stoppede kædeflows slippes af kæden. Ingen handlinger de første 2 minutter, ingen markskift før alle marker er scannet, og intet slukkes ved delvise læsefejl. Begrænsninger: intet køb, ingen dyrekøb, flerårige afgrøder og græsmarker skiftes aldrig, grundudbytte uden gødningsbonus. **Vigtigt:** hver produktion, du tænder i en fabrik, prøver moden at forsyne — tænd kun det, du vil have produceret; sluk produktionen eller tryk [Pause] for at stoppe kæden. Manglende led forklares i klart sprog med råd (*Ingen af dine fabrikker laver Mel — byg en eller køb*, *Mølle kan kun lave 800 af 1.0k/md*, *Dine stalde leverer ikke nok Mælk …*, *… er slukket af dig*, *Kredsløb omkring …*): som rød tekst under varen i fanen Kæder, som én samlet rød linje i **Kræver handling** (*Kæder: 3 manglende led*, under lagerets røde linjer; klik åbner fanen Kæder med kæderne foldet ud) og som engangsbesked med kædens navn foran, når et nyt led mangler — højst 3 ad gangen, resten samlet i *+N manglende led — se fanen Kæder* (ikke i opstartsfristen eller mens markerne scannes). Markvarer, der ikke er en afgrødes hovedvare (hø, tørret lucerne/kløver, hakkelse, helsæd, halm …), står med kilden *fra marker* uden afgrødeplan eller advarsel, så fermenteringssiloer ikke giver falske *byg en eller køb*. Nyt **statusvindue** på skærmen (i stil med Simple Inspector), som slås til/fra med **numpad \*** (Insert bruges af Universal Autoload; kan ændres under Styring; virker til fods og i køretøjer): øverst til højre under info-boksen vises flow-traktorernes handling og fyld, markarbejde i gang, kæder med manglende led og advarsler (slukkede nødvendige fabrikker, køretøjer der har stået stille over 2 min — solgte vises ikke —, ventende kædeforslag); traktorlinjernes farve følger flowets niveau som Flows-listen — højst 18 linjer, opdateres hvert sekund, skjules ved menu/dialog, skjult HUD og søvn; tændt/slukket gemmes pr. bruger i `modSettings/FS25_ADSmartPickup/status.xml`. **Advarsel når en nødvendig fabrik slukkes** (også uden kæder): slukker du en produktion, hvis vare et tændt flow kører (valgt vare, eller målet tager den i auto og gården forbruger den), en stald æder eller en aktiv kæde bruger sammen med et flow/en stald, kommer én besked pr. slukning (*Advarsel: du slukkede Fermenteringssilo 3 (Ensilage) — Cow Food, Goat Food +1 bruger det. Lageret rækker ca. 12 dage.*) og en rød linje *Slukket men nødvendig: …* i **Kræver handling** (højst én, flere tælles som *+k*; klik åbner fanen Fabrikker) og i statusvinduet, så længe den er slukket og nødvendig. Ingen advarsel når en anden opskrift i samme fabrik laver varen og er tændt, en anden tændt produktion dækker med nok kapacitet, kun kæder bruger varen (meldes som manglende led), lageret rækker over 60 dage, kæden selv slukkede den, eller den allerede var slukket ved indlæsning; produktioner ingen bruger, advarer aldrig. Manual og ordliste opdateret.
|
||||
|
||||
## 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,113 @@ 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
|
||||
|
||||
-- Markørnavnets rolle: "unload" (unload/drop/aflæs/aflever/indkast/tip) eller "load" (pickup/hent/læs/load) —
|
||||
-- aflæsning tjekkes først ("unload" og "aflæs" indeholder "load"/"læs"). nil = neutralt navn.
|
||||
local UNLOAD_WORDS = {"unload", "drop", "aflæs", "aflev", "indkast", "tip"}
|
||||
local LOAD_WORDS = {"pickup", "pick up", "hent", "læs", "load"}
|
||||
function ADBuildings.nameRole(name)
|
||||
local lower = tostring(name or ""):lower()
|
||||
for _, word in ipairs(UNLOAD_WORDS) do if lower:find(word, 1, true) then return "unload" end end
|
||||
for _, word in ipairs(LOAD_WORDS) do if lower:find(word, 1, true) then return "load" end end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Når to markører står tæt (inden for NAME_WINDOW_M af den nærmeste), afgør navnet: den første med den ønskede rolle
|
||||
-- flyttes forrest. Ellers urørt (nærmeste først). (in-game 30/9: Sell Egg tog "Chicken Storage Unload" 17 m frem for
|
||||
-- "Egg Storage Pickup" 27 m ved Æglageret)
|
||||
ADBuildings.NAME_WINDOW_M = 20
|
||||
function ADBuildings.preferByName(candidates, wantRole)
|
||||
local list = candidates or {}
|
||||
if #list < 2 then return list end
|
||||
local limit = (list[1].distance or 0) + ADBuildings.NAME_WINDOW_M
|
||||
if ADBuildings.nameRole(list[1].name) == wantRole then return list end
|
||||
for index, candidate in ipairs(list) do
|
||||
if (candidate.distance or math.huge) > limit then break end
|
||||
if ADBuildings.nameRole(candidate.name) == wantRole then
|
||||
local reordered = {candidate}
|
||||
for other, item in ipairs(list) do if other ~= index then table.insert(reordered, item) end end
|
||||
return reordered
|
||||
end
|
||||
end
|
||||
return list
|
||||
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(ADBuildings.preferByName(candidates, "load")) 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
|
||||
@@ -229,7 +345,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
local best, bestOther = nil, nil
|
||||
local best, bestOther, nearbyAll = nil, nil, {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if not ADSupplyPlanner.isWaitMarker(marker) then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
@@ -238,6 +354,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z
|
||||
local distance = math.sqrt(dx * dx + dz * dz)
|
||||
if distance < ADBuildings.MAX_MARKER_DISTANCE then
|
||||
table.insert(nearbyAll, {marker = marker, distance = distance})
|
||||
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
|
||||
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
|
||||
bestOther = {marker = marker, distance = distance}
|
||||
@@ -253,6 +370,16 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
|
||||
chosen = bestOther
|
||||
end
|
||||
-- navnet afgør mellem tætte markører: en "Unload/Drop"-markør frem for en "Pickup"-markør (in-game 30/9)
|
||||
table.sort(nearbyAll, function(a, b) return a.distance < b.distance end)
|
||||
if chosen ~= nil and ADBuildings.nameRole(chosen.marker.name) == "load" then
|
||||
for _, entry in ipairs(nearbyAll) do
|
||||
if entry.distance <= chosen.distance + ADBuildings.NAME_WINDOW_M and ADBuildings.nameRole(entry.marker.name) == "unload" then
|
||||
chosen = entry
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if chosen == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -332,8 +459,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 +471,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 +492,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 +502,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,643 @@
|
||||
-- AD Chain (kæde-kørsel)
|
||||
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
|
||||
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
|
||||
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
|
||||
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
|
||||
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
|
||||
-- Spiladgang kun inde i funktioner (filen dofile's i test).
|
||||
|
||||
ADChain = {}
|
||||
ADChain.FILE_NAME = "ADSmartPickup_chain.xml"
|
||||
ADChain.ROOT = "ADSmartPickupChain"
|
||||
ADChain.TICK_MS = 60000
|
||||
ADChain.STARTUP_GRACE_MS = 120000
|
||||
ADChain.MAX_FIELD_SWITCHES = 2
|
||||
ADChain.MAX_NEW_FLOWS = 2
|
||||
ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
|
||||
ADChain.DISMISS_CHANGE = 0.25
|
||||
|
||||
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
ADChain.last = nil
|
||||
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste tick — til at se spillerens slukninger
|
||||
ADChain.prevEnabled = nil
|
||||
-- true fra indlæsning til første tick: kædens produktioner der er slukket, glemmes (spillet blev ikke gemt)
|
||||
ADChain.afterLoad = false
|
||||
-- run-id -> "noVehicle"|"pallet" (true = "noVehicle"): flows kæden oprettede stoppet, fordi der ikke var
|
||||
-- traktor/vogn eller varen er på paller (kun i hukommelsen)
|
||||
ADChain.noVehicle = {}
|
||||
ADChain.switchCount = 0
|
||||
ADChain.now = 0
|
||||
ADChain.timer = 0
|
||||
ADChain.collectFailed = false
|
||||
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
|
||||
ADChain.problemKeys = {}
|
||||
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
|
||||
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
|
||||
ADChain.depPrevEnabled = nil
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
local function sortedKeys(map)
|
||||
local keys = {}
|
||||
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return keys
|
||||
end
|
||||
|
||||
-- ---- rene hjælpere ----
|
||||
|
||||
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
|
||||
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
|
||||
function ADChain.newState()
|
||||
-- finalKeep = {[rodnøgle] = true}: slutproduktet beholdes (standard: sælg straks); soldByChain = {["rod|vare"] = true}
|
||||
-- optIn = {[opskriftsnøgle] = true}: produktioner spilleren selv har slået til som kæde (tændte produktioner er
|
||||
-- ellers ikke kæder — FS25 tænder dem som standard)
|
||||
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
end
|
||||
|
||||
function ADChain.stateToRecords(state)
|
||||
local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {},
|
||||
roots = state.roots ~= nil and sortedKeys(state.roots) or nil,
|
||||
finalKeep = sortedKeys(state.finalKeep or {}), soldByChain = sortedKeys(state.soldByChain or {}), optIn = sortedKeys(state.optIn or {})}
|
||||
local ids = {}
|
||||
for id in pairs(state.dismissed or {}) do table.insert(ids, id) end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
local entry = state.dismissed[id]
|
||||
table.insert(records.dismissed, {id = id, fruit = entry.fruit, missingHa = entry.missingHa})
|
||||
end
|
||||
return records
|
||||
end
|
||||
|
||||
function ADChain.stateFromRecords(records)
|
||||
local state = ADChain.newState()
|
||||
if records == nil then return state end
|
||||
for _, key in ipairs(records.byChain or {}) do state.byChain[key] = true end
|
||||
for _, key in ipairs(records.paused or {}) do state.paused[key] = true end
|
||||
for _, key in ipairs(records.userOff or {}) do state.userOff[key] = true end
|
||||
for _, key in ipairs(records.finalKeep or {}) do state.finalKeep[key] = true end
|
||||
for _, key in ipairs(records.soldByChain or {}) do state.soldByChain[key] = true end
|
||||
for _, key in ipairs(records.optIn or {}) do state.optIn[key] = true end
|
||||
if records.roots ~= nil then
|
||||
state.roots = {}
|
||||
for _, key in ipairs(records.roots) do state.roots[key] = true end
|
||||
end
|
||||
for _, entry in ipairs(records.dismissed or {}) do
|
||||
local id = tonumber(entry.id)
|
||||
if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then
|
||||
state.dismissed[id] = {fruit = entry.fruit, missingHa = tonumber(entry.missingHa) or 0}
|
||||
end
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
-- Afviste forslag beholdes kun mens manglen for afgrøden findes og har ændret sig højst 25 %.
|
||||
function ADChain.pruneDismissed(dismissed, shortages)
|
||||
local byFruit = {}
|
||||
for _, shortage in ipairs(shortages or {}) do byFruit[shortage.fruit] = shortage.missingHa end
|
||||
local kept = {}
|
||||
for id, entry in pairs(dismissed or {}) do
|
||||
local now = byFruit[entry.fruit]
|
||||
local before = entry.missingHa or 0
|
||||
if now ~= nil and math.abs(now - before) <= math.abs(before) * ADChain.DISMISS_CHANGE then
|
||||
kept[id] = {fruit = entry.fruit, missingHa = before}
|
||||
end
|
||||
end
|
||||
return kept
|
||||
end
|
||||
|
||||
-- {[mark] = {fruit, missingHa}} -> {[mark] = afgrøde} (ADCropPlan.choose's dismissed)
|
||||
function ADChain.dismissedFruits(dismissed)
|
||||
local result = {}
|
||||
for id, entry in pairs(dismissed or {}) do result[id] = entry.fruit end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADChain.unionFruits(usedFruits, fieldDemand)
|
||||
local result = {}
|
||||
for fruit in pairs(usedFruits or {}) do result[fruit] = true end
|
||||
for fruit in pairs(fieldDemand or {}) do result[fruit] = true end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
|
||||
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
|
||||
-- optIn = {[nøgle] = true}: kun de produktioner er rødder (andre tændte markeres notChosen: kører for sig selv som
|
||||
-- kilde, men kæden skaber intet for dem). optIn = nil = alle tændte er rødder (som før).
|
||||
function ADChain.modelRecipes(recipes, paused, userOff, optIn)
|
||||
local result = {}
|
||||
for index, recipe in ipairs(recipes or {}) do
|
||||
local copy = {}
|
||||
for key, value in pairs(recipe) do copy[key] = value end
|
||||
if (paused or {})[recipe.key] then copy.paused = true end
|
||||
if optIn ~= nil and recipe.enabled and not recipe.byChain and not optIn[recipe.key] then copy.notChosen = true end
|
||||
if (userOff or {})[recipe.key] and not recipe.enabled then copy.cyclesPerMonth = 0 end
|
||||
result[index] = copy
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function copySet(map)
|
||||
local result = {}
|
||||
for key, value in pairs(map or {}) do result[key] = value end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er kædens (= rødderne, inkl. pausede).
|
||||
function ADChain.enabledKeys(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Spillerens valg og forældede nøgler, før kæden regner (rent: state røres ikke).
|
||||
-- opts = {prevEnabled, isPartial, firstEval}. -> {userOff, byChain, paused, roots, added, forgotten, droppedPaused, cleared}
|
||||
-- * kædens produktion er slukket: af spilleren -> userOff (added); første tick efter indlæsning -> glemt (forgotten)
|
||||
-- * en produktion (ikke kædens) der var tændt sidste tick, er slukket -> userOff (added)
|
||||
-- * userOff ryddes kun når der kommer en ny rod (spilleren tænder noget) — ikke når noget "ikke længere bruges"
|
||||
-- (det gav tænd/sluk-løkker); nye slukninger i samme tick beholdes
|
||||
-- * pausede nøgler for produktioner der er væk eller slukket, droppes (droppedPaused)
|
||||
-- Delvise data: userOff ryddes ikke, rødderne opdateres ikke og pausede droppes ikke.
|
||||
function ADChain.reconcile(state, recipes, opts)
|
||||
opts = opts or {}
|
||||
local roots = ADChain.enabledKeys(recipes)
|
||||
local rootAdded = false
|
||||
if state.roots ~= nil then
|
||||
for key in pairs(roots) do if not state.roots[key] then rootAdded = true end end
|
||||
end
|
||||
local result = {userOff = copySet(state.userOff), byChain = copySet(state.byChain), paused = copySet(state.paused),
|
||||
roots = opts.isPartial and state.roots or roots, added = {}, forgotten = {}, droppedPaused = {}, cleared = false}
|
||||
if rootAdded and not opts.isPartial and next(result.userOff) ~= nil then
|
||||
result.userOff, result.cleared = {}, true
|
||||
end
|
||||
local present = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
present[recipe.key] = recipe
|
||||
if not recipe.enabled and recipe.byChain then
|
||||
result.byChain[recipe.key] = nil
|
||||
if opts.firstEval then
|
||||
table.insert(result.forgotten, recipe.key)
|
||||
elseif not result.userOff[recipe.key] then
|
||||
result.userOff[recipe.key] = true
|
||||
table.insert(result.added, recipe.key)
|
||||
end
|
||||
elseif not recipe.enabled and (opts.prevEnabled or {})[recipe.key] and not result.userOff[recipe.key] then
|
||||
result.userOff[recipe.key] = true
|
||||
table.insert(result.added, recipe.key)
|
||||
end
|
||||
end
|
||||
if not opts.isPartial then
|
||||
for key in pairs(state.paused or {}) do
|
||||
if present[key] == nil or not present[key].enabled then
|
||||
result.paused[key] = nil
|
||||
table.insert(result.droppedPaused, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(result.added); table.sort(result.forgotten); table.sort(result.droppedPaused)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Rødder hvis flows ikke må stoppes: kædens aktuelle rødder + pausede rødder (pause rører intet).
|
||||
function ADChain.activeRoots(chain, paused)
|
||||
local result = {}
|
||||
for _, root in ipairs((chain or {}).roots or {}) do result[root.key] = true end
|
||||
for key in pairs(paused or {}) do result[key] = true end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Mest brugte Wait-pulje blandt forsyningsflows til bygningen, ellers "".
|
||||
function ADChain.waitPoolFor(runs, building)
|
||||
local counts, best, bestCount = {}, "", 0
|
||||
for _, run in ipairs(runs or {}) do
|
||||
local group = run.waitPoolGroup
|
||||
if run.kind == "supply" and group ~= nil and group ~= "" and contains(run.targetBuildingIds, building) then
|
||||
counts[group] = (counts[group] or 0) + 1
|
||||
end
|
||||
end
|
||||
for _, group in ipairs(sortedKeys(counts)) do
|
||||
if counts[group] > bestCount then best, bestCount = group, counts[group] end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
function ADChain.flowName(fillTitle, buildingName)
|
||||
return "Kæde: " .. tostring(fillTitle) .. " → " .. tostring(buildingName)
|
||||
end
|
||||
|
||||
local function covers(run, edge)
|
||||
return contains(run.targetBuildingIds, edge.consumer)
|
||||
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType))
|
||||
end
|
||||
|
||||
-- Bruger en af de aktuelle kanter stadig flowet? -> kantens rodnøgle (flowet flyttes dertil), ellers nil.
|
||||
function ADChain.stillNeeded(runView, edges)
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if covers(runView, edge) then return edge.rootKey end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- {[forbruger|vare] = "ok"|"auto"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
|
||||
function ADChain.flowStatus(edges, runs, noVehicle)
|
||||
local result = {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
-- udgangen står på Fordel: spillet leverer selv, intet flow
|
||||
local status = edge.delivery == "auto" and "auto" or "missing"
|
||||
for _, run in ipairs(edge.delivery == "auto" and {} or runs or {}) do
|
||||
if run.kind == "supply" and covers(run, edge) then
|
||||
if not run.stopped then status = "ok"; break end
|
||||
local reason = (noVehicle or {})[run.id]
|
||||
if reason == "pallet" then status = "pallet"
|
||||
elseif reason then status = "noVehicle"
|
||||
elseif status == "missing" then status = "stopped" end
|
||||
end
|
||||
end
|
||||
result[tostring(edge.consumer) .. "|" .. tostring(edge.fillType)] = status
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Flows i det format ADChainActions regner på.
|
||||
function ADChain.runsView(runs)
|
||||
local list = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(list, {id = run.id, name = run.name, kind = run.kind, targetBuildingIds = run.targetBuildingIds,
|
||||
ingredientMode = run.ingredientMode, ingredients = run.ingredients, stopped = run.stopped == true,
|
||||
chain = run.chain, waitPoolGroup = run.waitPoolGroup,
|
||||
-- traktorerne: uden dem ligner alle kædeflows tomme (sammenlægning/sletning må kun ramme tomme)
|
||||
vehicleIds = run.vehicleIds or {}})
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Manglende led der ikke var der sidst (nøgle = rodnøgle|vare|art). -> nye problemer (hver nøgle én gang), nøglerne nu.
|
||||
-- Et problem der forsvinder, glemmes; kommer det igen, er det nyt. prevKeys røres ikke.
|
||||
function ADChain.problemKey(problem)
|
||||
return tostring(problem.rootKey) .. "|" .. tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||
end
|
||||
|
||||
function ADChain.newProblems(prevKeys, problems)
|
||||
local fresh, keys = {}, {}
|
||||
for _, problem in ipairs(problems or {}) do
|
||||
local key = ADChain.problemKey(problem)
|
||||
if not keys[key] then
|
||||
keys[key] = true
|
||||
if not (prevKeys or {})[key] then table.insert(fresh, problem) end
|
||||
end
|
||||
end
|
||||
return fresh, keys
|
||||
end
|
||||
|
||||
-- ---- evaluering ----
|
||||
|
||||
local function farmId()
|
||||
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||||
end
|
||||
|
||||
local function plan(data, state)
|
||||
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff, state.optIn)
|
||||
local chain = ADChainModel.build(recipes, data.husbandry, data.crops, data.fieldGoods)
|
||||
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
|
||||
local protected = {}
|
||||
if next(state.paused) ~= nil then
|
||||
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff, state.optIn), data.husbandry, data.crops, data.fieldGoods).needRecipes
|
||||
end
|
||||
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
|
||||
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
|
||||
local shortages, actions = {}, {}
|
||||
if not data.fieldsIncomplete then
|
||||
shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields)
|
||||
state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages)
|
||||
actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.priceOf or data.prices, ADChain.dismissedFruits(state.dismissed), data.harvesting)
|
||||
end
|
||||
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
|
||||
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
|
||||
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain, data.recipes)}
|
||||
end
|
||||
|
||||
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
|
||||
-- Spillerens valg og forældede nøgler ind i tilstanden (kun ved tick, før der regnes).
|
||||
local function applyReconcile(state, data)
|
||||
local result = ADChain.reconcile(state, data.recipes, {prevEnabled = ADChain.prevEnabled, isPartial = data.partial == true,
|
||||
firstEval = ADChain.afterLoad})
|
||||
if result.cleared then logInfo("ny produktion tændt — slukkede produktioner må bruges igen") end
|
||||
for _, key in ipairs(result.added) do logInfo("spilleren slukkede %s — kæden tænder den ikke igen", key) end
|
||||
for _, key in ipairs(result.forgotten) do logInfo("kæden glemmer %s (ikke tændt efter indlæsning)", key) end
|
||||
for _, key in ipairs(result.droppedPaused) do logInfo("pause for %s fjernet: produktionen er væk eller slukket", key) end
|
||||
state.userOff, state.byChain, state.paused, state.roots = result.userOff, result.byChain, result.paused, result.roots
|
||||
ADChain.prevEnabled = ADChain.enabledKeys(data.recipes)
|
||||
ADChain.afterLoad = false
|
||||
end
|
||||
|
||||
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
|
||||
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
|
||||
-- mellemtiden meldes ved næste hele tjek).
|
||||
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
|
||||
local function dependencyCheck(data, plan, runs, act, previous)
|
||||
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
|
||||
if data.readFailed then
|
||||
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
|
||||
end
|
||||
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
|
||||
local enabling, recipes = {}, {}
|
||||
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
|
||||
for _, recipe in ipairs(data.recipes or {}) do
|
||||
if not enabling[recipe.key] then table.insert(recipes, recipe) end
|
||||
end
|
||||
local prev = act and ADChain.depPrevEnabled or nil
|
||||
local result = {
|
||||
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||
}
|
||||
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
|
||||
return result
|
||||
end
|
||||
|
||||
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
|
||||
function ADChain.evaluate(adEnv, act)
|
||||
local state = ADChain.state
|
||||
local ok, data = pcall(ADChainData.collect, adEnv, farmId(), state.byChain)
|
||||
if not ok then
|
||||
if not ADChain.collectFailed then Logging.warning(ADChain.LOG_PREFIX .. "gården kunne ikke læses: %s", tostring(data)) end
|
||||
ADChain.collectFailed = true
|
||||
return ADChain.last
|
||||
end
|
||||
ADChain.collectFailed = false
|
||||
if act then
|
||||
local okState, errState = pcall(applyReconcile, state, data)
|
||||
if not okState then Logging.warning(ADChain.LOG_PREFIX .. "tilstanden kunne ikke opdateres: %s", tostring(errState)) end
|
||||
end
|
||||
local okPlan, result = pcall(plan, data, state)
|
||||
if not okPlan then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "kæden kunne ikke beregnes: %s", tostring(result))
|
||||
return ADChain.last
|
||||
end
|
||||
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
|
||||
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
|
||||
if not okDep then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
|
||||
dependency = nil
|
||||
end
|
||||
local paused = {}
|
||||
for key in pairs(state.paused) do paused[key] = true end
|
||||
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
|
||||
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
|
||||
paused = paused, userOff = copySet(state.userOff), finalKeep = copySet(state.finalKeep or {}),
|
||||
sellable = ADChain.sellableRoots(data, result, runs), candidates = ADChain.candidates(data.recipes, state), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
|
||||
dependency = dependency}
|
||||
return ADChain.last
|
||||
end
|
||||
|
||||
-- Alder (ms, spillets ur) af det sidste resultat; math.huge når der intet er. Fanen genbruger et friskt resultat.
|
||||
function ADChain.lastAge()
|
||||
if ADChain.last == nil then return math.huge end
|
||||
return ADChain.now - (ADChain.last.at or 0)
|
||||
end
|
||||
|
||||
-- Engangsbesked pr. nyt manglende led (kun fra tick). Ikke i opstartsfristen og ikke mens gården er læst delvist
|
||||
-- eller markscanningen kører (ellers falske alarmer lige efter indlæsning) — så huskes intet, og det meldes senere.
|
||||
-- Højst MAX_PROBLEM_NOTES beskeder pr. tick; resten samles i én besked (alle logges stadig).
|
||||
ADChain.MAX_PROBLEM_NOTES = 3
|
||||
function ADChain.notifyProblems(last)
|
||||
if last == nil or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||
if last.fieldsIncomplete or (last.data ~= nil and last.data.partial) then return 0 end
|
||||
local fresh, keys = ADChain.newProblems(ADChain.problemKeys, last.chain ~= nil and last.chain.problems or {})
|
||||
ADChain.problemKeys = keys
|
||||
if #fresh == 0 or ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||
local fmt = SmartPickupFrame ~= nil and SmartPickupFrame.formatCompact or nil
|
||||
local rootNames = {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do
|
||||
rootNames[root.key] = tostring((last.titles or {})[root.name] or root.name)
|
||||
end
|
||||
for index, problem in ipairs(fresh) do
|
||||
-- kædens navn foran, så to kæder der mangler samme vare kan skelnes
|
||||
local text = tostring(rootNames[problem.rootKey] or problem.rootKey) .. ": " .. ADChainView.problemText(problem, last, textOf, nil, fmt)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "manglende led: %s", text)
|
||||
if index <= ADChain.MAX_PROBLEM_NOTES then ADFieldJobs.notify(text, true) end
|
||||
end
|
||||
-- resten samles i én besked (fx mange fermenteringssiloer efter indlæsning); alle står i fanen Kæder
|
||||
if #fresh > ADChain.MAX_PROBLEM_NOTES then
|
||||
local ok, text = pcall(string.format, textOf("spu_chain_notifyMore"), #fresh - ADChain.MAX_PROBLEM_NOTES)
|
||||
ADFieldJobs.notify(ok and text or ("+" .. tostring(#fresh - ADChain.MAX_PROBLEM_NOTES)), true)
|
||||
end
|
||||
return #fresh
|
||||
end
|
||||
|
||||
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
|
||||
function ADChain.notifyDependencies(last)
|
||||
local dependency = last ~= nil and last.dependency or nil
|
||||
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||
local warnings = dependency.warnings
|
||||
dependency.warnings = {}
|
||||
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
|
||||
for _, warning in ipairs(warnings) do
|
||||
local text = ADDependency.notifyText(warning, textOf, titleOf)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
|
||||
ADFieldJobs.notify(text, true)
|
||||
end
|
||||
return #warnings
|
||||
end
|
||||
|
||||
function ADChain.tick(adEnv, dt)
|
||||
if adEnv == nil then return end
|
||||
ADChain.now = ADChain.now + (dt or 0)
|
||||
ADChain.timer = ADChain.timer + (dt or 0)
|
||||
if ADChain.now < ADChain.STARTUP_GRACE_MS or ADChain.timer < ADChain.TICK_MS then return end
|
||||
ADChain.timer = 0
|
||||
local last = ADChain.evaluate(adEnv, true)
|
||||
local ok, err = pcall(ADChain.notifyProblems, last)
|
||||
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
|
||||
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
|
||||
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
|
||||
end
|
||||
|
||||
-- ---- GUI-handlinger ----
|
||||
|
||||
local function suggestionFor(fieldId)
|
||||
for _, action in ipairs(ADChain.last ~= nil and ADChain.last.actions or {}) do
|
||||
if action.fieldId == fieldId then return action end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Godkend forslag: marken får kædens afgrøde som mål (kilde "chain").
|
||||
function ADChain.approve(fieldId)
|
||||
local action = suggestionFor(fieldId)
|
||||
if action == nil then return false end
|
||||
local ok, err = pcall(function()
|
||||
-- et auto-skift må aldrig overskrive en afgrøde spilleren har sat siden; et forslag godkender spilleren selv
|
||||
if action.mode == "auto" then
|
||||
local plan = ADFieldJobs.getField(fieldId).plan or {}
|
||||
if plan.targetSource == "user" then error("spilleren har selv valgt afgrøde") end
|
||||
end
|
||||
ADFieldJobs.setTarget(fieldId, action.to, "chain")
|
||||
end)
|
||||
if not ok then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "F%s: forslag kunne ikke godkendes: %s", tostring(fieldId), tostring(err))
|
||||
return false
|
||||
end
|
||||
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag godkendt: %s -> %s", tostring(fieldId), tostring(action.from), tostring(action.to))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Afvis forslag: vises ikke igen, før manglen for afgrøden ændrer sig > 25 %.
|
||||
function ADChain.dismiss(fieldId)
|
||||
local action = suggestionFor(fieldId)
|
||||
if action == nil then return false end
|
||||
local missingHa = action.hectares or 0
|
||||
for _, shortage in ipairs(ADChain.last.shortages or {}) do
|
||||
if shortage.fruit == action.to then missingHa = shortage.missingHa end
|
||||
end
|
||||
ADChain.state.dismissed[fieldId] = {fruit = action.to, missingHa = missingHa}
|
||||
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag %s afvist", tostring(fieldId), tostring(action.to))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er valgt som kæde (og ikke pauset): {{key, name, buildingName}} til fanen Kæder.
|
||||
function ADChain.candidates(recipes, state)
|
||||
local list = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain and not (state.optIn or {})[recipe.key] and not (state.paused or {})[recipe.key] then
|
||||
table.insert(list, {key = recipe.key, name = recipe.name, buildingName = recipe.buildingName})
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if tostring(a.buildingName) ~= tostring(b.buildingName) then return tostring(a.buildingName) < tostring(b.buildingName) end
|
||||
return tostring(a.name) < tostring(b.name)
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Slå en produktion til/fra som kæde (fra = også pausen ophæves, kæden slipper den ved næste tick).
|
||||
function ADChain.setOptIn(recipeKey, on)
|
||||
if recipeKey == nil then return end
|
||||
ADChain.state.optIn[recipeKey] = on and true or nil
|
||||
if not on then ADChain.state.paused[recipeKey] = nil end
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(recipeKey), on and "slået til som kæde" or "fjernet fra kæderne")
|
||||
end
|
||||
|
||||
-- Rødder med et slutprodukt der kan sælges straks (gården bruger det ikke selv). -> {[rodnøgle] = true}
|
||||
function ADChain.sellableRoots(data, result, runs)
|
||||
local set = {}
|
||||
local ok, sales = pcall(ADChainActions.finalSales, result.chain, data.recipes, result.edges, {}, ADChainActions.farmUses(data, runs))
|
||||
for _, sale in ipairs(ok and sales or {}) do set[sale.key] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
-- Slutprodukt for en kæde: keep = true -> "Behold", ellers "Sælg straks" (standard). Anvendes ved næste tick.
|
||||
function ADChain.setFinalKeep(rootKey, keep)
|
||||
if rootKey == nil then return end
|
||||
ADChain.state.finalKeep[rootKey] = keep and true or nil
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s: slutprodukt %s", tostring(rootKey), keep and "beholdes" or "sælges straks")
|
||||
end
|
||||
|
||||
function ADChain.setPaused(rootKey, isPaused)
|
||||
if rootKey == nil then return end
|
||||
ADChain.state.paused[rootKey] = isPaused and true or nil
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(rootKey), isPaused and "på pause" or "genoptaget")
|
||||
end
|
||||
|
||||
-- ---- gem / indlæs (samme sti-opslag som runs; skrives kun fra ADRunsController.save) ----
|
||||
|
||||
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 .. "$", ADChain.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADChain.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupChain", path, ADChain.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local records = ADChain.stateToRecords(ADChain.state)
|
||||
for index, key in ipairs(records.byChain) do
|
||||
xmlFile:setString(string.format("%s.byChain.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.paused) do
|
||||
xmlFile:setString(string.format("%s.paused.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.userOff) do
|
||||
xmlFile:setString(string.format("%s.userOff.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.finalKeep) do
|
||||
xmlFile:setString(string.format("%s.finalKeep.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.soldByChain) do
|
||||
xmlFile:setString(string.format("%s.soldByChain.output(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.optIn) do
|
||||
xmlFile:setString(string.format("%s.optIn.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
if records.roots ~= nil then
|
||||
xmlFile:setBool(ADChain.ROOT .. ".roots#known", true)
|
||||
for index, key in ipairs(records.roots) do
|
||||
xmlFile:setString(string.format("%s.roots.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
end
|
||||
for index, entry in ipairs(records.dismissed) do
|
||||
local key = string.format("%s.dismissed.field(%d)", ADChain.ROOT, index - 1)
|
||||
xmlFile:setInt(key .. "#id", entry.id)
|
||||
xmlFile:setString(key .. "#fruit", entry.fruit)
|
||||
xmlFile:setString(key .. "#missingHa", string.format("%.2f", entry.missingHa or 0))
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADChain.load()
|
||||
ADChain.state = ADChain.newState()
|
||||
ADChain.noVehicle = {}
|
||||
ADChain.prevEnabled, ADChain.afterLoad = nil, true
|
||||
ADChain.problemKeys = {}
|
||||
ADChain.depPrevEnabled = nil
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
|
||||
if xmlFile == nil then return end
|
||||
local records = {byChain = {}, paused = {}, userOff = {}, dismissed = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
xmlFile:iterate(ADChain.ROOT .. ".optIn.recipe", function(_, key)
|
||||
table.insert(records.optIn, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".finalKeep.root", function(_, key)
|
||||
table.insert(records.finalKeep, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".soldByChain.output", function(_, key)
|
||||
table.insert(records.soldByChain, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".byChain.recipe", function(_, key)
|
||||
table.insert(records.byChain, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".paused.root", function(_, key)
|
||||
table.insert(records.paused, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".userOff.recipe", function(_, key)
|
||||
table.insert(records.userOff, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
if xmlFile:getBool(ADChain.ROOT .. ".roots#known", false) then
|
||||
records.roots = {}
|
||||
xmlFile:iterate(ADChain.ROOT .. ".roots.recipe", function(_, key)
|
||||
table.insert(records.roots, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
end
|
||||
xmlFile:iterate(ADChain.ROOT .. ".dismissed.field", function(_, key)
|
||||
table.insert(records.dismissed, {id = xmlFile:getInt(key .. "#id"), fruit = xmlFile:getString(key .. "#fruit"),
|
||||
missingHa = tonumber(xmlFile:getString(key .. "#missingHa"))})
|
||||
end)
|
||||
xmlFile:delete()
|
||||
ADChain.state = ADChain.stateFromRecords(records)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%d produktioner tændt af kæden, %d pausede kæder indlæst", #records.byChain, #records.paused)
|
||||
end
|
||||
@@ -0,0 +1,230 @@
|
||||
-- AD Chain Actions (ren logik): mellemled der skal tændes/slukkes, flows der mangler eller skal stoppes.
|
||||
|
||||
ADChainActions = {}
|
||||
|
||||
-- protected = {[nøgle] = cyklusser}: behov fra pausede rødder — slukkes ikke, men tændes heller ikke.
|
||||
function ADChainActions.productions(recipes, needRecipes, protected)
|
||||
local result = {enable = {}, disable = {}}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
local needed = ((needRecipes or {})[recipe.key] or 0) > 0
|
||||
local kept = ((protected or {})[recipe.key] or 0) > 0
|
||||
if needed and not recipe.enabled then table.insert(result.enable, recipe.key) end
|
||||
if not needed and not kept and recipe.enabled and recipe.byChain then table.insert(result.disable, recipe.key) end
|
||||
end
|
||||
table.sort(result.enable); table.sort(result.disable)
|
||||
return result
|
||||
end
|
||||
|
||||
local SUPPLIED = {production = true, husbandry = true, field = true}
|
||||
|
||||
-- Hvordan varen når frem, ud fra producenternes udgang: "auto" = alle fordeler (spillet leverer selv til gårdens
|
||||
-- fabrikker; kædens forbrugere er altid fabrikker) -> intet flow; "sold" = en producent sælger mellemvaren væk;
|
||||
-- ellers "haul" (Behold: spawn/lager ved fabrikken, eller stald/mark) -> et flow henter. recipesByKey = {[nøgle] = opskrift}.
|
||||
function ADChainActions.delivery(edge, recipesByKey)
|
||||
local producers = edge.producers or {}
|
||||
if #producers == 0 or recipesByKey == nil then return "haul" end
|
||||
local all, sold = true, false
|
||||
for _, key in ipairs(producers) do
|
||||
local mode = ADChainActions.producerMode(recipesByKey, key, edge.fillType)
|
||||
if mode == "sell" then sold = true end
|
||||
if mode ~= "distribute" then all = false end
|
||||
end
|
||||
if sold then return "sold" end
|
||||
return all and "auto" or "haul"
|
||||
end
|
||||
|
||||
function ADChainActions.producerMode(recipesByKey, key, fillType)
|
||||
local recipe = (recipesByKey or {})[key]
|
||||
return recipe ~= nil and (recipe.outputModes or {})[fillType] or nil
|
||||
end
|
||||
|
||||
local function producerKeys(node)
|
||||
local keys = {}
|
||||
for _, producer in ipairs(node.producers or {}) do table.insert(keys, producer.key) end
|
||||
return keys
|
||||
end
|
||||
|
||||
-- recipes (valgfri): gårdens opskrifter med outputModes -> kanten får delivery (se ADChainActions.delivery).
|
||||
function ADChainActions.edges(chain, recipes)
|
||||
local list, seen, byKey = {}, {}, nil
|
||||
if recipes ~= nil then
|
||||
byKey = {}
|
||||
for _, recipe in ipairs(recipes) do byKey[recipe.key] = recipe end
|
||||
end
|
||||
local function walk(nodes, rootKey)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
local id = tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||
if SUPPLIED[node.source] and node.consumer ~= nil and not seen[id] then
|
||||
seen[id] = true
|
||||
local edge = {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey,
|
||||
producers = producerKeys(node)}
|
||||
edge.delivery = ADChainActions.delivery(edge, byKey)
|
||||
edge.soldBy = {}
|
||||
for _, key in ipairs(edge.producers) do
|
||||
if ADChainActions.producerMode(byKey, key, edge.fillType) == "sell" then table.insert(edge.soldBy, key) end
|
||||
end
|
||||
table.insert(list, edge)
|
||||
end
|
||||
walk(node.children, rootKey)
|
||||
end
|
||||
end
|
||||
for _, root in ipairs((chain or {}).roots or {}) do walk(root.children, root.key) end
|
||||
return list
|
||||
end
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADChainActions.isCovered(edge, runs)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.kind == "supply" and contains(run.targetBuildingIds, edge.consumer)
|
||||
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType)) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADChainActions.missingFlows(edges, runs)
|
||||
local list = {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if edge.delivery ~= "auto" and not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Mellemvarer en producent sælger væk (kanten er "sold") -> {{key, fillType}} én gang hver; kæden sætter dem til Behold.
|
||||
function ADChainActions.soldIntermediates(edges)
|
||||
local list, seen = {}, {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if edge.delivery == "sold" then
|
||||
for _, key in ipairs(edge.soldBy or {}) do
|
||||
local id = key .. "|" .. edge.fillType
|
||||
if not seen[id] then
|
||||
seen[id] = true
|
||||
table.insert(list, {key = key, fillType = edge.fillType})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
function ADChainActions.flowsToStop(runs, activeRootKeys)
|
||||
local list = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
-- også stoppede kædeflows uden traktor (de ryddes op); stoppede med traktor er allerede sluppet/spillerens
|
||||
local idle = #(run.vehicleIds or {}) == 0
|
||||
if run.chain ~= nil and run.chain ~= "" and (not run.stopped or idle) and not (activeRootKeys or {})[run.chain] then
|
||||
table.insert(list, run)
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- ---- ét kædeflow pr. bygning (in-game 30/9: ét flow pr. vare og bygning gav mange stoppede flows) ----
|
||||
|
||||
-- Manglende kanter samlet pr. modtagerbygning: {consumer, consumerName, rootKey, fillTypes} i fundet rækkefølge.
|
||||
function ADChainActions.groupMissing(missing)
|
||||
local groups, byConsumer = {}, {}
|
||||
for _, edge in ipairs(missing or {}) do
|
||||
local group = byConsumer[edge.consumer]
|
||||
if group == nil then
|
||||
group = {consumer = edge.consumer, consumerName = edge.consumerName, rootKey = edge.rootKey, fillTypes = {}}
|
||||
byConsumer[edge.consumer] = group
|
||||
table.insert(groups, group)
|
||||
end
|
||||
if not contains(group.fillTypes, edge.fillType) then table.insert(group.fillTypes, edge.fillType) end
|
||||
end
|
||||
return groups
|
||||
end
|
||||
|
||||
local function isChainRun(run)
|
||||
return run.chain ~= nil and run.chain ~= "" and run.kind == "supply" and run.ingredientMode == "manual"
|
||||
end
|
||||
|
||||
-- Kædens eget flow (manuelle varer) til bygningen -> run-id, så nye varer lægges i det i stedet for et nyt flow.
|
||||
function ADChainActions.chainRunFor(runs, consumer)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if isChainRun(run) and contains(run.targetBuildingIds, consumer) then return run.id end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Flere tomme kædeflows (uden traktorer) til samme bygning: det første beholdes med alle varerne, resten slettes.
|
||||
-- -> {{keepId, add = {varer}, deleteIds = {id}}}
|
||||
function ADChainActions.mergeIdle(runs)
|
||||
local byConsumer, order = {}, {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
local target = (run.targetBuildingIds or {})[1]
|
||||
if isChainRun(run) and target ~= nil and #(run.targetBuildingIds or {}) == 1 and #(run.vehicleIds or {}) == 0 then
|
||||
if byConsumer[target] == nil then byConsumer[target] = {}; table.insert(order, target) end
|
||||
table.insert(byConsumer[target], run)
|
||||
end
|
||||
end
|
||||
local merges = {}
|
||||
for _, target in ipairs(order) do
|
||||
local list = byConsumer[target]
|
||||
if #list > 1 then
|
||||
local keep, add, deleteIds = list[1], {}, {}
|
||||
for index = 2, #list do
|
||||
for _, name in ipairs(list[index].ingredients or {}) do
|
||||
if not contains(keep.ingredients, name) and not contains(add, name) then table.insert(add, name) end
|
||||
end
|
||||
table.insert(deleteIds, list[index].id)
|
||||
end
|
||||
table.insert(merges, {keepId = keep.id, add = add, deleteIds = deleteIds})
|
||||
end
|
||||
end
|
||||
return merges
|
||||
end
|
||||
|
||||
-- Varer gården selv bruger: stalde æder dem, en tændt produktion bruger dem, gården har forbrug af dem, eller et
|
||||
-- flow (ikke udkørsel) kører dem. -> {[vare] = true}. Den slags sælges aldrig straks (in-game 30/9: TMR og roesnitter).
|
||||
function ADChainActions.farmUses(data, runs)
|
||||
local uses = {}
|
||||
for name in pairs((data or {}).barnFood or {}) do uses[name] = true end
|
||||
for name, rate in pairs((data or {}).rates or {}) do
|
||||
if (rate.consumption or 0) > 0 then uses[name] = true end
|
||||
end
|
||||
for _, recipe in ipairs((data or {}).recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name in pairs(recipe.inputs or {}) do uses[name] = true end
|
||||
end
|
||||
end
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.kind ~= "outbound" and run.ingredientMode == "manual" then
|
||||
for _, name in ipairs(run.ingredients or {}) do uses[name] = true end
|
||||
end
|
||||
end
|
||||
return uses
|
||||
end
|
||||
|
||||
-- Slutprodukter der skal sælges straks: aktive rødders udgange, som ikke selv er en kant i kæden (mellemvare) og
|
||||
-- som gården ikke selv bruger (farmUses). keep = {[rodnøgle] = true} for rødder sat til "Behold". -> {{key, fillType}}
|
||||
function ADChainActions.finalSales(chain, recipes, edges, keep, farmUses)
|
||||
local intermediate, byKey, list = {}, {}, {}
|
||||
for _, edge in ipairs(edges or {}) do intermediate[edge.fillType] = true end
|
||||
for _, recipe in ipairs(recipes or {}) do byKey[recipe.key] = recipe end
|
||||
for _, root in ipairs((chain or {}).roots or {}) do
|
||||
local recipe = byKey[root.key]
|
||||
if recipe ~= nil and not (keep or {})[root.key] then
|
||||
local names = {}
|
||||
for name in pairs(recipe.outputs or {}) do table.insert(names, name) end
|
||||
table.sort(names)
|
||||
for _, name in ipairs(names) do
|
||||
if not intermediate[name] and not (farmUses or {})[name] then table.insert(list, {key = root.key, fillType = name}) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Kædeflow for en kæde der er slået fra: uden traktor slettes det (auto-oprettet og tomt), ellers stoppes og slippes
|
||||
-- det (bliver spillerens eget). (in-game 30/9: ellers 12 tomme "Kæde: …"-flows blandt spillerens egne)
|
||||
function ADChainActions.releaseAction(run)
|
||||
if #(run.vehicleIds or {}) == 0 then return "delete" end
|
||||
return "release"
|
||||
end
|
||||
@@ -0,0 +1,511 @@
|
||||
-- AD Chain Data (læse-adapter)
|
||||
-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
|
||||
-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
|
||||
-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
|
||||
-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
|
||||
|
||||
ADChainData = {}
|
||||
ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||
ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
|
||||
ADChainData.warned = {}
|
||||
|
||||
-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
|
||||
function ADChainData.isPermanent(fruitName)
|
||||
if type(fruitName) ~= "string" or fruitName == "" then return false end
|
||||
return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
|
||||
end
|
||||
|
||||
function ADChainData.recipeKey(placeableUid, productionId)
|
||||
return tostring(placeableUid) .. ":" .. tostring(productionId)
|
||||
end
|
||||
|
||||
-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
|
||||
function ADChainData.cyclesPerMonth(production)
|
||||
return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
|
||||
end
|
||||
|
||||
function ADChainData.warnOnce(key, fmt, ...)
|
||||
if ADChainData.warned[key] then return end
|
||||
ADChainData.warned[key] = true
|
||||
if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
|
||||
end
|
||||
|
||||
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
|
||||
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
|
||||
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
|
||||
local function failed(data, key, fmt, ...)
|
||||
data.partial, data.readFailed = true, true
|
||||
ADChainData.warnOnce(key, fmt, ...)
|
||||
end
|
||||
|
||||
-- ---- hjælpere ----
|
||||
|
||||
-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
|
||||
local function nameResolver(data)
|
||||
return function(index)
|
||||
if index == nil or g_fillTypeManager == nil then return nil end
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(index)
|
||||
if name == nil then return nil end
|
||||
if data.fillIndex[name] == nil then
|
||||
data.fillIndex[name] = index
|
||||
local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
|
||||
data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
|
||||
end
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
|
||||
local function amountsByName(list, nameOf)
|
||||
local result = {}
|
||||
for _, entry in ipairs(list or {}) do
|
||||
local name = nameOf(entry.type)
|
||||
if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function placeableId(placeable)
|
||||
if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||
return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
|
||||
end
|
||||
|
||||
-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
|
||||
local function placeableName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then return tostring(name) end
|
||||
end
|
||||
return fallback
|
||||
end
|
||||
|
||||
local function isEnabled(point, production)
|
||||
if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
|
||||
for _, active in ipairs(point.activeProductions or {}) do
|
||||
if active == production then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ---- læsning pr. del ----
|
||||
|
||||
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
|
||||
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
|
||||
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
|
||||
function ADChainData.outputModeName(mode)
|
||||
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
|
||||
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
|
||||
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
|
||||
return "keep"
|
||||
end
|
||||
|
||||
local function outputModes(point, outputs, nameOf)
|
||||
local modes = {}
|
||||
if point.getOutputDistributionMode == nil then return modes end
|
||||
for _, output in ipairs(outputs or {}) do
|
||||
local name = output.type ~= nil and nameOf(output.type) or nil
|
||||
if name ~= nil then
|
||||
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
|
||||
modes[name] = ADChainData.outputModeName(ok and mode or nil)
|
||||
end
|
||||
end
|
||||
return modes
|
||||
end
|
||||
|
||||
local function readFactory(placeable, point, byChain, nameOf)
|
||||
local uid = placeableId(placeable)
|
||||
if uid == nil then return {}, {} end
|
||||
local buildingName = placeableName(placeable, uid)
|
||||
local recipes, points = {}, {}
|
||||
for _, production in ipairs(point.productions or {}) do
|
||||
local key = ADChainData.recipeKey(uid, production.id)
|
||||
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
|
||||
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
|
||||
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
|
||||
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
|
||||
outputModes = outputModes(point, production.outputs, nameOf)})
|
||||
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
|
||||
end
|
||||
return recipes, points
|
||||
end
|
||||
|
||||
local function readHerd(placeable, nameOf)
|
||||
local uid = placeableId(placeable)
|
||||
local outputs = {}
|
||||
for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
|
||||
end
|
||||
if uid == nil or next(outputs) == nil then return nil end
|
||||
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
|
||||
end
|
||||
|
||||
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
|
||||
local function readBarnFood(data, placeable, nameOf)
|
||||
if ADSources.getHusbandryConsumption == nil then return end
|
||||
local barnName
|
||||
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil and (perDay or 0) > 0 then
|
||||
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
|
||||
data.barnFood[name] = data.barnFood[name] or {}
|
||||
table.insert(data.barnFood[name], barnName)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function readPlaceables(data, farmId, byChain, nameOf)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for index, placeable in pairs(placeables) do
|
||||
local ok, err = pcall(function()
|
||||
if not ADPrices.isOwnedBy(placeable, farmId) then return end
|
||||
local uid = placeableId(placeable)
|
||||
if uid ~= nil then data.placeables[uid] = placeable end
|
||||
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
|
||||
if point ~= nil then
|
||||
local recipes, points = readFactory(placeable, point, byChain, nameOf)
|
||||
for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
|
||||
for key, entry in pairs(points) do data.points[key] = entry end
|
||||
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
|
||||
local herd = readHerd(placeable, nameOf)
|
||||
if herd ~= nil then table.insert(data.husbandry, herd) end
|
||||
readBarnFood(data, placeable, nameOf)
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
local key = tostring(placeable ~= nil and placeable.uniqueId or index)
|
||||
failed(data, "placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
|
||||
end
|
||||
end
|
||||
table.sort(data.recipes, function(a, b) return a.key < b.key end)
|
||||
end
|
||||
|
||||
-- Markedspris for en afgrøde, først når kæden spørger (kun kandidatmarkernes afgrøder, kun ved mangel);
|
||||
-- hver afgrøde slås højst op én gang pr. læsning. Fejl -> 0 (logget én gang).
|
||||
local function priceResolver(data)
|
||||
local cache = {}
|
||||
return function(fruitName)
|
||||
if cache[fruitName] ~= nil then return cache[fruitName] end
|
||||
local index = data.fruitFillIndex[fruitName]
|
||||
local price = 0
|
||||
if index ~= nil then
|
||||
local ok, quote = pcall(ADSources.getMarketQuote, index)
|
||||
if ok then
|
||||
price = quote ~= nil and quote.price or 0
|
||||
else
|
||||
ADChainData.warnOnce("price:" .. tostring(fruitName), "pris for %s kunne ikke læses: %s", tostring(fruitName), tostring(quote))
|
||||
end
|
||||
end
|
||||
cache[fruitName] = price
|
||||
return price
|
||||
end
|
||||
end
|
||||
|
||||
local function readCrops(data, nameOf)
|
||||
local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
|
||||
for _, fruit in ipairs(fruits) do
|
||||
if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
|
||||
local ok, err = pcall(function()
|
||||
local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
|
||||
if fillName == nil then return end
|
||||
if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
|
||||
data.crops[fillName] = fruit.name
|
||||
data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
|
||||
data.fruitFillIndex[fruit.name] = fruit.fillType.index
|
||||
end)
|
||||
if not ok then failed(data, "fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- markvarer (hø, hakkelse, halm …) ----
|
||||
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
|
||||
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
|
||||
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
|
||||
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
|
||||
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
|
||||
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
|
||||
|
||||
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
|
||||
local function windrowIndex(fruit)
|
||||
local windrow = fruit.windrowFillType
|
||||
if type(windrow) == "number" then return windrow end
|
||||
if type(windrow) == "table" then
|
||||
if windrow.index ~= nil then return windrow.index end
|
||||
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
|
||||
local function converterTargets(converter, add)
|
||||
for _, entry in pairs(converter or {}) do
|
||||
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||
end
|
||||
end
|
||||
|
||||
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
|
||||
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
|
||||
local function readFieldGoods(data, nameOf)
|
||||
local indices = {}
|
||||
local function add(index) if type(index) == "number" then indices[index] = true end end
|
||||
local function guarded(key, fn)
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
|
||||
end
|
||||
if g_fillTypeManager == nil then return end
|
||||
local fruitManager = g_fruitTypeManager
|
||||
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
|
||||
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
|
||||
end
|
||||
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
|
||||
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
|
||||
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
|
||||
end
|
||||
end
|
||||
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
|
||||
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
|
||||
end
|
||||
end
|
||||
-- tørrede varianter (vender: græs-vindue -> hø)
|
||||
if g_fillTypeManager.getConverterDataByName ~= nil then
|
||||
guarded("TEDDER", function()
|
||||
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
|
||||
local windrows = {}
|
||||
for index in pairs(indices) do table.insert(windrows, index) end
|
||||
for _, index in ipairs(windrows) do
|
||||
local entry = tedder[index]
|
||||
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||
end
|
||||
end)
|
||||
end
|
||||
for index in pairs(indices) do
|
||||
local ok, name = pcall(nameOf, index)
|
||||
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
|
||||
end
|
||||
end
|
||||
|
||||
-- Læs i gårdens køretøjer (vogne, høstertanke, marksilo-vogne) tæller som lager: kun afgrøder og markvarer, ikke
|
||||
-- diesel o.l. loads = {{name, liters}} (in-game 30/9: høst på vej hjem gav falsk mangel og forslag).
|
||||
function ADChainData.addVehicleLoads(data, loads)
|
||||
for _, load in ipairs(loads or {}) do
|
||||
local name, liters = load.name, load.liters or 0
|
||||
local fruit = name ~= nil and (data.crops or {})[name] or nil
|
||||
if liters > 0 and (fruit ~= nil or (data.fieldGoods or {})[name]) then
|
||||
data.stockByFill[name] = (data.stockByFill[name] or 0) + liters
|
||||
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function readVehicleLoads(data, farmId, nameOf)
|
||||
local ok, err = pcall(function()
|
||||
local loads = {}
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
|
||||
local spec = vehicle.spec_fillUnit
|
||||
if spec ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
|
||||
for index, unit in ipairs(spec.fillUnits or {}) do
|
||||
local level = vehicle:getFillUnitFillLevel(index) or 0
|
||||
if level > 0 then
|
||||
table.insert(loads, {name = nameOf(vehicle:getFillUnitFillType(index) or unit.fillType), liters = level})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ADChainData.addVehicleLoads(data, loads)
|
||||
end)
|
||||
if not ok then ADChainData.warnOnce("vehicleLoads", "læs i køretøjer kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
local function readStock(data, adEnv, farmId, nameOf)
|
||||
local ok, err = pcall(function()
|
||||
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
|
||||
local fruit = data.crops[name or ""]
|
||||
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
|
||||
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
|
||||
local function readRates(data, adEnv, farmId, nameOf)
|
||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
|
||||
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
|
||||
local ok, err = pcall(function()
|
||||
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
|
||||
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
|
||||
local function readUsedFruits(data, adEnv, farmId, nameOf)
|
||||
for _, recipe in ipairs(data.recipes) do
|
||||
if recipe.enabled then
|
||||
for name in pairs(recipe.inputs) do
|
||||
if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, true) or {}) do
|
||||
local fruit = data.crops[nameOf(index) or ""]
|
||||
if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Markscanningen har ikke nået alle marker endnu (efter indlæsning ~én mark pr. 2 s): marklisten er ufuldstændig,
|
||||
-- så udbud undervurderes -> fieldsIncomplete (kæden rører ingen marker) og partial (intet slukkes/stoppes).
|
||||
local function readFields(data)
|
||||
if ADFields.isScanComplete ~= nil and not ADFields.isScanComplete() then
|
||||
data.partial, data.fieldsIncomplete = true, true
|
||||
end
|
||||
for _, info in ipairs(ADFields.list() or {}) do
|
||||
local ok, entry = pcall(function()
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
local plan = field.plan or {}
|
||||
local fruit = info.growing and info.fruitName or field.lastFruit
|
||||
-- høst i gang: ingen forslag/skift for afgrøden, mens læssene er på vej hjem
|
||||
if field.stage == "harvesting" and info.fruitName ~= nil then data.harvesting[info.fruitName] = true end
|
||||
return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
|
||||
targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
|
||||
permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
|
||||
end)
|
||||
if ok then
|
||||
table.insert(data.fields, entry)
|
||||
else
|
||||
failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- afhængighedstjek (spec afsnit 8) ----
|
||||
|
||||
local function unloadStationsOf(placeable)
|
||||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||
local result = {}
|
||||
if storage == nil or storage.getUnloadingStations == nil then return result end
|
||||
for _, station in pairs(storage:getUnloadingStations() or {}) do
|
||||
if station.owningPlaceable == placeable then table.insert(result, station) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
|
||||
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
|
||||
function ADChainData.acceptsMap(data, flows, goods)
|
||||
local result = {}
|
||||
for _, flow in ipairs(flows or {}) do
|
||||
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
|
||||
for _, id in ipairs(flow.targetBuildingIds or {}) do
|
||||
local placeable = data.placeables[id]
|
||||
if result[id] == nil and placeable ~= nil then
|
||||
result[id] = {}
|
||||
local ok, err = pcall(function()
|
||||
local stations = unloadStationsOf(placeable)
|
||||
for _, name in ipairs(goods) do
|
||||
local index = data.fillIndex[name]
|
||||
for _, station in ipairs(stations) do
|
||||
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
|
||||
end
|
||||
end
|
||||
end)
|
||||
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
|
||||
function ADChainData.chainInputs(chain, titles)
|
||||
local result = {}
|
||||
local function walk(nodes, rootTitle)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
result[node.fillType] = result[node.fillType] or {}
|
||||
local list = result[node.fillType]
|
||||
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
|
||||
walk(node.children, rootTitle)
|
||||
end
|
||||
end
|
||||
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
|
||||
walk(root.children, tostring((titles or {})[root.name] or root.name))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
|
||||
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
|
||||
function ADChainData.dependencyInputs(data, chain, flows)
|
||||
local goods, seen = {}, {}
|
||||
for _, recipe in ipairs(data.recipes or {}) do
|
||||
if not recipe.enabled and not recipe.byChain then
|
||||
for name in pairs(recipe.outputs or {}) do
|
||||
if not seen[name] then seen[name] = true; table.insert(goods, name) end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(goods)
|
||||
-- auto-flowets mål tager mange varer (fx biogas: korn, ensilage, gylle): kun varer gården faktisk forbruger
|
||||
-- (stald eller fabrik) eller en kæde vil have, tæller
|
||||
local consumed = {}
|
||||
for _, name in ipairs(goods) do
|
||||
local rate = (data.rates or {})[name]
|
||||
if (rate ~= nil and (rate.consumption or 0) > 0) or ((chain ~= nil and chain.demand or {})[name] or 0) > 0 then
|
||||
table.insert(consumed, name)
|
||||
end
|
||||
end
|
||||
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
|
||||
#consumed > 0 and ADChainData.acceptsMap(data, flows, consumed) or {})
|
||||
local adjustment = data.timeAdjustment or 1
|
||||
local capacity, demand, stockDays = {}, {}, {}
|
||||
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
|
||||
for _, name in ipairs(goods) do
|
||||
local rate = (data.rates or {})[name] or {}
|
||||
local consumption, production = rate.consumption or 0, rate.production or 0
|
||||
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
|
||||
demand[name] = math.max(consumption, chainDemand)
|
||||
if consumption > production then
|
||||
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
|
||||
elseif consumption > 0 then
|
||||
stockDays[name] = math.huge
|
||||
end
|
||||
end
|
||||
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
|
||||
end
|
||||
|
||||
-- ---- samlet ----
|
||||
|
||||
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
|
||||
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
|
||||
-- ikke har nået alle marker.
|
||||
function ADChainData.collect(adEnv, farmId, byChain)
|
||||
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
|
||||
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}, harvesting = {}}
|
||||
local nameOf = nameResolver(data)
|
||||
data.priceOf = priceResolver(data)
|
||||
readPlaceables(data, farmId, byChain, nameOf)
|
||||
readCrops(data, nameOf)
|
||||
readFieldGoods(data, nameOf)
|
||||
readStock(data, adEnv, farmId, nameOf)
|
||||
readVehicleLoads(data, farmId, nameOf)
|
||||
readRates(data, adEnv, farmId, nameOf)
|
||||
readUsedFruits(data, adEnv, farmId, nameOf)
|
||||
readFields(data)
|
||||
return data
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
-- AD Chain Model (ren logik)
|
||||
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
|
||||
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
|
||||
|
||||
ADChainModel = {}
|
||||
ADChainModel.MAX_DEPTH = 8
|
||||
|
||||
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
|
||||
|
||||
-- producenter pr. vare: tændte først, så stabil rækkefølge
|
||||
local function indexProducers(recipes)
|
||||
local index = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
|
||||
index[name] = index[name] or {}
|
||||
table.insert(index[name], recipe)
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, list in pairs(index) do
|
||||
table.sort(list, function(a, b)
|
||||
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
|
||||
return byKey(a, b)
|
||||
end)
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function indexHerds(husbandry)
|
||||
local index = {}
|
||||
for _, herd in ipairs(husbandry or {}) do
|
||||
for name, perMonth in pairs(herd.outputs or {}) do
|
||||
if (perMonth or 0) > 0 then
|
||||
index[name] = index[name] or {}
|
||||
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
|
||||
end
|
||||
end
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function sortedNames(map)
|
||||
local names = {}
|
||||
for name in pairs(map or {}) do table.insert(names, name) end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
|
||||
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
|
||||
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
|
||||
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
|
||||
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
|
||||
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
|
||||
-- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion).
|
||||
-- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den.
|
||||
local capacity, roots, standalone = {}, {}, {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
capacity[recipe.key] = recipe.cyclesPerMonth or 0
|
||||
if recipe.enabled and not recipe.byChain then
|
||||
if not recipe.paused and not recipe.notChosen then table.insert(roots, recipe) end
|
||||
standalone[recipe.key] = true
|
||||
end
|
||||
end
|
||||
table.sort(roots, byKey)
|
||||
|
||||
-- detail = manglende liter/md (flaskehals/køb), need = varens behov pr. måned i den gren
|
||||
local function problem(kind, fillType, rootKey, detail, need)
|
||||
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail, need = need})
|
||||
end
|
||||
|
||||
local need
|
||||
local function inputsOf(recipe, cycles, depth, path, rootKey)
|
||||
local children = {}
|
||||
for _, name in ipairs(sortedNames(recipe.inputs)) do
|
||||
local amount = recipe.inputs[name] or 0
|
||||
if amount > 0 and cycles > 0 then
|
||||
local child = need(name, amount * cycles, depth + 1, path, rootKey)
|
||||
child.consumer, child.consumerName = recipe.building, recipe.buildingName
|
||||
table.insert(children, child)
|
||||
end
|
||||
end
|
||||
return children
|
||||
end
|
||||
|
||||
need = function(name, perMonth, depth, path, rootKey)
|
||||
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
|
||||
chain.demand[name] = (chain.demand[name] or 0) + perMonth
|
||||
if depth > ADChainModel.MAX_DEPTH or path[name] then
|
||||
node.source = "cycle"
|
||||
problem("cycle", name, rootKey, nil, perMonth)
|
||||
return node
|
||||
end
|
||||
path[name] = true
|
||||
local rest = perMonth
|
||||
-- a) gårdens fabrikker
|
||||
local makers = producers[name]
|
||||
if makers ~= nil then
|
||||
node.source = "production"
|
||||
for _, recipe in ipairs(makers) do
|
||||
local perCycle = recipe.outputs[name]
|
||||
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
|
||||
if cycles > 0 then
|
||||
capacity[recipe.key] = capacity[recipe.key] - cycles
|
||||
rest = rest - cycles * perCycle
|
||||
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
|
||||
if not standalone[recipe.key] then
|
||||
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
|
||||
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
|
||||
end
|
||||
end
|
||||
if rest <= 1e-9 then break end
|
||||
end
|
||||
if rest > 1e-9 and #node.producers > 0 then
|
||||
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||
node.shortPerMonth = rest
|
||||
end
|
||||
if rest > 1e-9 and #node.producers == 0 then
|
||||
node.source = nil
|
||||
end
|
||||
end
|
||||
-- b) stalde
|
||||
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
|
||||
node.source = "husbandry"
|
||||
for _, herd in ipairs(herds[name]) do
|
||||
local take = math.min(rest, herd.left)
|
||||
if take > 0 then
|
||||
herd.left = herd.left - take
|
||||
rest = rest - take
|
||||
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
|
||||
end
|
||||
end
|
||||
end
|
||||
-- c) mark
|
||||
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
|
||||
node.source, node.fruit = "field", crops[name]
|
||||
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
|
||||
rest = 0
|
||||
end
|
||||
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
|
||||
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
|
||||
node.source, node.fruit = "field", nil
|
||||
rest = 0
|
||||
end
|
||||
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
|
||||
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
|
||||
node.source = "production"
|
||||
node.shortPerMonth = rest
|
||||
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
|
||||
if node.source == nil then node.source = "buy" end
|
||||
node.shortPerMonth = rest
|
||||
problem("buy", name, rootKey, rest, perMonth)
|
||||
end
|
||||
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
|
||||
path[name] = nil
|
||||
return node
|
||||
end
|
||||
|
||||
for _, root in ipairs(roots) do
|
||||
local cycles = root.cyclesPerMonth or 0
|
||||
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
|
||||
children = inputsOf(root, cycles, 0, {}, root.key)})
|
||||
end
|
||||
return chain
|
||||
end
|
||||
@@ -0,0 +1,362 @@
|
||||
-- AD Chain Run (kædens handlinger i spillet)
|
||||
-- Udfører ADChain's plan: skifter frie marker (kun "auto", højst MAX_FIELD_SWITCHES pr. tick), tænder/slukker
|
||||
-- mellemled, opretter manglende forsyningsflows (højst MAX_NEW_FLOWS pr. tick) og stopper flows for rødder der
|
||||
-- er slået fra. Hver del kører i pcall, så en fejl ikke stopper resten. Spilkald kun inde i funktioner.
|
||||
|
||||
ADChainRun = {}
|
||||
|
||||
local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
|
||||
local UNLOADER = "unloader"
|
||||
|
||||
-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool).
|
||||
local function isUnloader(implement)
|
||||
return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true)
|
||||
end
|
||||
|
||||
-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle
|
||||
-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig.
|
||||
local function belongsToFlow(tractor)
|
||||
return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil
|
||||
end
|
||||
|
||||
-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og
|
||||
-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu).
|
||||
function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked)
|
||||
if fillTypeIndex == nil then return nil end
|
||||
used, blocked = used or {}, blocked or {}
|
||||
local free = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then
|
||||
table.insert(free, tractor)
|
||||
end
|
||||
end
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then
|
||||
local tractor = ADPool.bestFit(free, implement, "unloader")
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ---- marker ----
|
||||
|
||||
local function fieldById(fields, id)
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if field.id == id then return field end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADChainRun.switchFields(data, actions)
|
||||
local switched = 0
|
||||
for _, action in ipairs(actions or {}) do
|
||||
if switched >= ADChain.MAX_FIELD_SWITCHES then break end
|
||||
local field = fieldById(data.fields, action.fieldId)
|
||||
-- brugerens egen målafgrøde røres aldrig (dobbelttjek oven i ADCropPlan)
|
||||
if action.mode == "auto" and field ~= nil and field.targetSource ~= "user" then
|
||||
local ok, err = pcall(ADFieldJobs.setTarget, action.fieldId, action.to, "chain")
|
||||
if ok then
|
||||
switched = switched + 1
|
||||
ADChain.switchCount = ADChain.switchCount + 1
|
||||
local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from),
|
||||
tostring(action.to), action.hectares or 0, tostring(action.reason))
|
||||
log("%s", text)
|
||||
pcall(ADFieldJobs.notify, text, false)
|
||||
else
|
||||
warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- mellemled ----
|
||||
|
||||
local function setProduction(data, key, isOn)
|
||||
local entry = data.points[key]
|
||||
if entry == nil or entry.point == nil or entry.point.setProductionState == nil then
|
||||
warn("produktion %s findes ikke (længere)", tostring(key))
|
||||
return false
|
||||
end
|
||||
local ok, err = pcall(entry.point.setProductionState, entry.point, entry.productionId, isOn)
|
||||
if not ok then warn("produktion %s kunne ikke %s: %s", tostring(key), isOn and "tændes" or "slukkes", tostring(err)) end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen.
|
||||
-- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra).
|
||||
function ADChainRun.toggleProductions(data, prod)
|
||||
local state = ADChain.state
|
||||
local byChain = state.byChain
|
||||
for _, key in ipairs(prod.enable or {}) do
|
||||
if not state.userOff[key] and setProduction(data, key, true) then
|
||||
byChain[key] = true
|
||||
log("tænder %s (mellemled i kæden)", key)
|
||||
end
|
||||
end
|
||||
if data.partial then return end
|
||||
for _, key in ipairs(prod.disable or {}) do
|
||||
if setProduction(data, key, false) then
|
||||
byChain[key] = nil
|
||||
log("slukker %s (kæden bruger den ikke længere)", key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- flows ----
|
||||
|
||||
local function collectPool(adEnv)
|
||||
local ok, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
||||
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
||||
end)
|
||||
if not ok then
|
||||
warn("maskinpuljen kunne ikke læses: %s", tostring(implements))
|
||||
return {}, {}
|
||||
end
|
||||
return implements or {}, tractors or {}
|
||||
end
|
||||
|
||||
-- kind = "noVehicle"|"pallet": vises som status på kanten i fanen Kæder
|
||||
local function createStopped(run, kind, reason, ...)
|
||||
run.stopped = true
|
||||
ADChain.noVehicle[run.id] = kind
|
||||
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
|
||||
end
|
||||
|
||||
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
|
||||
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
|
||||
local function flowTitle(data, fillTypes)
|
||||
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
|
||||
if #fillTypes == 1 then return first end
|
||||
return string.format("%s +%d", tostring(first), #fillTypes - 1)
|
||||
end
|
||||
|
||||
local function startWithPair(adEnv, run, runId, pair, used)
|
||||
used[pair.tractor.id], used[pair.implement.id] = true, true
|
||||
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
|
||||
run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local wagons = ADRuns.copyWagons(run.wagons)
|
||||
wagons[pair.tractor.id] = pair.implement.id
|
||||
run.wagons = wagons
|
||||
ADRunsController.resolveMarkers(adEnv, run)
|
||||
local results = ADRunsController.startRun(adEnv, runId)
|
||||
local started = 0
|
||||
for _, result in ipairs(results or {}) do if result.ok then started = started + 1 end end
|
||||
log("flow '%s' oprettet med '%s' + vogn '%s' (%d startet)", tostring(run.name), tostring(pair.tractor.name),
|
||||
tostring(pair.implement.name), started)
|
||||
end
|
||||
|
||||
local function createGroupFlow(adEnv, data, group, fillTypes, pool, used)
|
||||
local title = flowTitle(data, fillTypes)
|
||||
local created = ADRunsController.createRun(ADChain.flowName(title, group.consumerName or group.consumer), "supply")
|
||||
local runId = created.id
|
||||
ADRunsController.updateRun(runId, {targetBuildingIds = {group.consumer}, ingredientMode = "manual", ingredients = fillTypes,
|
||||
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, group.consumer), chain = group.rootKey})
|
||||
-- updateRun erstatter store'en: slå altid run'et op igen
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local fillIndex = data.fillIndex[fillTypes[1]]
|
||||
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
|
||||
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
|
||||
return
|
||||
end
|
||||
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
|
||||
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
|
||||
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
|
||||
if pair == nil then
|
||||
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
|
||||
return
|
||||
end
|
||||
startWithPair(adEnv, run, runId, pair, used)
|
||||
end
|
||||
|
||||
-- Nye varer til en bygning, der allerede har et kædeflow: lægges i det flow.
|
||||
local function extendFlow(runId, fillTypes)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if run == nil then return false end
|
||||
local ingredients = {}
|
||||
for _, name in ipairs(run.ingredients or {}) do table.insert(ingredients, name) end
|
||||
local added = {}
|
||||
for _, name in ipairs(fillTypes) do
|
||||
local present = false
|
||||
for _, have in ipairs(ingredients) do if have == name then present = true end end
|
||||
if not present then table.insert(ingredients, name); table.insert(added, name) end
|
||||
end
|
||||
if #added == 0 then return false end
|
||||
ADRunsController.updateRun(runId, {ingredients = ingredients})
|
||||
log("flow '%s' får også %s", tostring(run.name), table.concat(added, ", "))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tidligere kædeflows (ét pr. vare) til samme bygning uden traktorer lægges sammen i ét.
|
||||
function ADChainRun.mergeIdleFlows(adEnv)
|
||||
local merged = 0
|
||||
for _, merge in ipairs(ADChainActions.mergeIdle(ADChain.runsView(ADRunsController.store.runs))) do
|
||||
if #merge.add > 0 then extendFlow(merge.keepId, merge.add) end
|
||||
for _, id in ipairs(merge.deleteIds) do
|
||||
local ok, err = pcall(ADRunsController.deleteRun, id, adEnv)
|
||||
if ok then
|
||||
ADChain.noVehicle[id] = nil
|
||||
merged = merged + 1
|
||||
else
|
||||
warn("kædeflow %s kunne ikke lægges sammen: %s", tostring(id), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
if merged > 0 then log("%d tomme kædeflows lagt sammen (ét flow pr. bygning)", merged) end
|
||||
return merged
|
||||
end
|
||||
|
||||
function ADChainRun.createFlows(adEnv, data, edges)
|
||||
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||
local missing = ADChainActions.missingFlows(edges, runs)
|
||||
local pool, used, count = {}, {}, 0
|
||||
for _, group in ipairs(ADChainActions.groupMissing(missing)) do
|
||||
if count >= ADChain.MAX_NEW_FLOWS then break end
|
||||
local bulk, pallets = {}, {}
|
||||
for _, name in ipairs(group.fillTypes) do
|
||||
local index = data.fillIndex[name]
|
||||
table.insert((index ~= nil and ADSources.isPalletFillType(index)) and pallets or bulk, name)
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
if #bulk > 0 then
|
||||
local existing = ADChainActions.chainRunFor(runs, group.consumer)
|
||||
if existing ~= nil then
|
||||
if extendFlow(existing, bulk) then count = count + 1 end
|
||||
else
|
||||
createGroupFlow(adEnv, data, group, bulk, pool, used)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(pallets) do
|
||||
createGroupFlow(adEnv, data, group, {name}, pool, used)
|
||||
count = count + 1
|
||||
end
|
||||
end)
|
||||
if not ok then warn("flow til %s kunne ikke oprettes: %s", tostring(group.consumerName), tostring(err)) end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Slutprodukter: sælg straks (fabrikkens udgang -> "Sælg direkte"), medmindre kæden står på "Behold". Varer kæden
|
||||
-- selv satte til salg, sættes tilbage til "Behold", når spilleren vælger det.
|
||||
local function outputMode(point, fillIndex)
|
||||
if point.getOutputDistributionMode == nil then return nil end
|
||||
local ok, mode = pcall(point.getOutputDistributionMode, point, fillIndex)
|
||||
return ok and mode or nil
|
||||
end
|
||||
|
||||
function ADChainRun.applyFinalSales(data, chain, edges)
|
||||
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||
local state, changed = ADChain.state, 0
|
||||
local wanted = {}
|
||||
local uses = ADChainActions.farmUses(data, ADChain.runsView(ADRunsController.store.runs))
|
||||
for _, sale in ipairs(ADChainActions.finalSales(chain, data.recipes, edges, state.finalKeep, uses)) do
|
||||
local id = sale.key .. "|" .. sale.fillType
|
||||
wanted[id] = true
|
||||
local entry, fillIndex = (data.points or {})[sale.key], data.fillIndex[sale.fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||
and outputMode(entry.point, fillIndex) ~= ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.DIRECT_SELL)
|
||||
state.soldByChain[id] = true
|
||||
changed = changed + 1
|
||||
log("slutprodukt %s i %s sælges straks", tostring(sale.fillType), tostring(entry.buildingName or sale.key))
|
||||
end
|
||||
end
|
||||
for id in pairs(state.soldByChain) do
|
||||
local key, fillType = id:match("^(.*)|([^|]+)$")
|
||||
-- sat til salg af kæden, men ikke længere ønsket (Behold, gården bruger varen, kæden væk): tilbage til Behold
|
||||
if key ~= nil and not wanted[id] then
|
||||
local entry, fillIndex = (data.points or {})[key], data.fillIndex[fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil
|
||||
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||
changed = changed + 1
|
||||
log("slutprodukt %s beholdes igen", tostring(fillType))
|
||||
end
|
||||
state.soldByChain[id] = nil
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
|
||||
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
|
||||
function ADChainRun.keepIntermediates(data, edges)
|
||||
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||
local changed = 0
|
||||
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
|
||||
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
|
||||
changed = changed + 1
|
||||
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
|
||||
tostring(entry.buildingName or item.key))
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
|
||||
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
|
||||
function ADChainRun.stopFlows(adEnv, chain, edges)
|
||||
local changed = 0
|
||||
local active = ADChain.activeRoots(chain, ADChain.state.paused)
|
||||
for _, run in ipairs(ADChainActions.flowsToStop(ADChain.runsView(ADRunsController.store.runs), active)) do
|
||||
local rootKey = ADChain.stillNeeded(run, edges)
|
||||
if rootKey ~= nil then
|
||||
ADRunsController.updateRun(run.id, {chain = rootKey})
|
||||
log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey))
|
||||
changed = changed + 1
|
||||
elseif ADChainActions.releaseAction(run) == "delete" then
|
||||
local ok, err = pcall(ADRunsController.deleteRun, run.id, adEnv)
|
||||
if ok then
|
||||
ADChain.noVehicle[run.id] = nil
|
||||
log("tomt kædeflow '%s' slettet: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||
changed = changed + 1
|
||||
else
|
||||
warn("flow '%s' kunne ikke slettes: %s", tostring(run.name), tostring(err))
|
||||
end
|
||||
else
|
||||
local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id)
|
||||
if ok then
|
||||
ADRunsController.updateRun(run.id, {chain = ""})
|
||||
log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||
changed = changed + 1
|
||||
else
|
||||
warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Hele planen; hver del for sig i pcall.
|
||||
function ADChainRun.apply(adEnv, data, result)
|
||||
local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions)
|
||||
if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end
|
||||
local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod)
|
||||
if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end
|
||||
local okMerge, mergedCount = pcall(ADChainRun.mergeIdleFlows, adEnv)
|
||||
if not okMerge then warn("sammenlægning af kædeflows fejlede: %s", tostring(mergedCount)) end
|
||||
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
|
||||
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
|
||||
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
|
||||
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
|
||||
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
|
||||
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
|
||||
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
|
||||
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
|
||||
local okStop, stopped = true, 0
|
||||
if not data.partial then
|
||||
okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges)
|
||||
if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end
|
||||
end
|
||||
if (okNew and created > 0) or (okStop and stopped > 0) then
|
||||
local okSave, errSave = pcall(ADRunsController.save)
|
||||
if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,453 @@
|
||||
-- AD Chain View (ren logik)
|
||||
-- Rækkerne til fanen Kæder ud fra ADChain.last. Hver rod er foldet sammen som standard (én linje med samlet
|
||||
-- status + antal forslag); foldet ud vises varerne rekursivt (kilde, behov pr. måned, status, lager). Forslag
|
||||
-- vises altid (under roden eller under afgrøden). Pausede rødder (som modellen ikke længere ser som rødder)
|
||||
-- får en grå linje med [Genoptag]. Nederst frie marker + antal automatiske skift (+ "marker scannes endnu"). Ingen spiladgang:
|
||||
-- titleOf(navn) -> titel, fmt(liter) -> kompakt tal, textOf(l10n-nøgle) -> tekst gives udefra.
|
||||
-- Manglende led (Task 10): foldet ud står problemteksten (problemText) som en rød tekstrække ("problem", ingen celler)
|
||||
-- lige under varen — teksten er for lang til en celle og ombrydes af fanen.
|
||||
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty"|"problem", indent, level, cells={c1..c5}, text,
|
||||
-- actions={[celle]={kind="approve"|"dismiss"|"pause"|"resume", fieldId|rootKey}}, toggle=rootKey, fillType}
|
||||
|
||||
ADChainView = {}
|
||||
ADChainView.INDENT = " "
|
||||
-- Tegnbudget pr. celle i flowListPrefab (gui/SmartPickupFrame.xml: textMaxWidth 192/162/62/244/92 px).
|
||||
-- Antaget ~11,5 px pr. tegn (spuInfoText: 800 px ~ 70 tegn), fed ~12,5 px; c3 har kun tal (~10 px pr. tegn).
|
||||
ADChainView.CELL_CHARS = {22, 20, 7, 28, 10} -- ca. tegn pr. celle (c1 192 px fed, c2 162, c3 62, c4 244, c5 92 fed)
|
||||
ADChainView.MAX_SOURCE_NAMES = 2
|
||||
-- Tegnbudget for en tekstlinje (addTextRow/spuInfoText: 800 px ~ 70 tegn), fx linjen med frie marker.
|
||||
ADChainView.TEXT_ROW_CHARS = 70
|
||||
-- "stramt": lager for en mark-afgrøde under så mange måneders behov -> gul
|
||||
ADChainView.TIGHT_MONTHS = 3
|
||||
-- nøgler rækkerne bruger (til l10n-testen)
|
||||
ADChainView.TEXT_KEYS = {"spu_chain_optIn", "spu_chain_optOut", "spu_chain_notChosen", "spu_chain_candidatesShow", "spu_chain_candidatesHide", "spu_chain_finalFarm", "spu_chain_finalSell", "spu_chain_finalKeep", "spu_chain_suggestHeader", "spu_chain_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
|
||||
"spu_chain_flowOk", "spu_chain_flowMissing", "spu_chain_flowPallet", "spu_chain_flowNone", "spu_chain_flowStopped", "spu_chain_bottleneck",
|
||||
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource", "spu_chain_fromFields",
|
||||
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
|
||||
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
|
||||
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
|
||||
"spu_chain_flowAuto", "spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
|
||||
|
||||
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
|
||||
pallet = "spu_chain_flowPallet", auto = "spu_chain_flowAuto"}
|
||||
local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError", waiting = "spu_chain_rootWaiting", stopped = "spu_chain_rootPaused"}
|
||||
|
||||
local function identity(value) return tostring(value) end
|
||||
|
||||
-- samme som SmartPickupFrame.formatCompact (standard-fmt når ingen gives, fx i beskeden fra kædens tick)
|
||||
function ADChainView.compact(liters)
|
||||
liters = math.floor((liters or 0) + 0.5)
|
||||
if liters >= 1000000 then return string.format("%.1fM", liters / 1000000)
|
||||
elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end
|
||||
return string.format("%d", liters)
|
||||
end
|
||||
|
||||
-- ---- UTF-8-sikker længde/afkortning (æøå og → er flere bytes) ----
|
||||
|
||||
function ADChainView.textLength(text)
|
||||
local count = 0
|
||||
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
local function utf8Prefix(text, chars)
|
||||
local result, count = {}, 0
|
||||
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||
if count >= chars then break end
|
||||
table.insert(result, char)
|
||||
count = count + 1
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
-- Afkort til celle-budgettet med ".." til sidst.
|
||||
function ADChainView.fit(text, chars)
|
||||
text = tostring(text or "")
|
||||
if ADChainView.textLength(text) <= chars then return text end
|
||||
return utf8Prefix(text, math.max(0, chars - 2)) .. ".."
|
||||
end
|
||||
|
||||
local function fitCells(cells)
|
||||
local result = {}
|
||||
for index = 1, 5 do result[index] = ADChainView.fit(cells[index] or "", ADChainView.CELL_CHARS[index]) end
|
||||
return result
|
||||
end
|
||||
|
||||
local function haText(ha)
|
||||
return string.format("%.0f", ha or 0)
|
||||
end
|
||||
|
||||
local function plannedFruit(field)
|
||||
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||
return field.fruit
|
||||
end
|
||||
|
||||
local function edgeKey(node)
|
||||
return tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||
end
|
||||
|
||||
local function shortageByFruit(last)
|
||||
local result = {}
|
||||
for _, shortage in ipairs(last.shortages or {}) do result[shortage.fruit] = shortage end
|
||||
return result
|
||||
end
|
||||
|
||||
local function stockOf(last, fruit)
|
||||
local stock = last.data ~= nil and last.data.stock or nil
|
||||
return stock ~= nil and fruit ~= nil and stock[fruit] or nil
|
||||
end
|
||||
|
||||
-- Lager for en mark-node: afgrødens lager, eller varens eget lager for en markvare uden afgrøde (hø, hakkelse …).
|
||||
local function fieldStockOf(last, node)
|
||||
if node.fruit ~= nil then return stockOf(last, node.fruit) end
|
||||
local byFill = last.data ~= nil and last.data.stockByFill or nil
|
||||
return byFill ~= nil and byFill[node.fillType] or nil
|
||||
end
|
||||
|
||||
-- Mark-afgrøde uden mangel men med lager under TIGHT_MONTHS måneders behov.
|
||||
local function isTight(node, last, short)
|
||||
if node.source ~= "field" or node.fruit == nil or short[node.fruit] ~= nil then return false end
|
||||
return (stockOf(last, node.fruit) or 0) < ADChainView.TIGHT_MONTHS * (node.perMonth or 0)
|
||||
end
|
||||
|
||||
local function eachNode(nodes, fn, depth)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
fn(node, depth or 1)
|
||||
eachNode(node.children, fn, (depth or 1) + 1)
|
||||
end
|
||||
end
|
||||
|
||||
function ADChainView.rootLevel(root, last)
|
||||
last = last or {}
|
||||
if (last.paused or {})[root.key] then return "stopped" end
|
||||
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||
if problem.rootKey == root.key then return "error" end
|
||||
end
|
||||
local short, flowStatus = shortageByFruit(last), last.flowStatus or {}
|
||||
local hasShortage, hasWaiting = false, false
|
||||
eachNode(root.children, function(node)
|
||||
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
|
||||
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
|
||||
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
|
||||
if (status ~= nil and status ~= "ok" and status ~= "auto") or isTight(node, last, short) then hasWaiting = true end
|
||||
end)
|
||||
if hasShortage then return "error" end
|
||||
if hasWaiting then return "waiting" end
|
||||
return "running"
|
||||
end
|
||||
|
||||
-- Højst MAX_SOURCE_NAMES navne + " +k".
|
||||
local function joinCapped(names)
|
||||
local shown = {}
|
||||
for index = 1, math.min(#names, ADChainView.MAX_SOURCE_NAMES) do shown[index] = names[index] end
|
||||
local text = table.concat(shown, ", ")
|
||||
if #names > #shown then text = text .. " +" .. tostring(#names - #shown) end
|
||||
return text
|
||||
end
|
||||
|
||||
-- Opskrifter (alle, også slukkede) der laver varen, i datarækkefølge.
|
||||
local function producersOf(last, fillType)
|
||||
local list = {}
|
||||
for _, recipe in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||
if ((recipe.outputs or {})[fillType] or 0) > 0 then table.insert(list, recipe) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Årsag i klart sprog med råd for et problem fra kædemodellen ({kind="buy"|"bottleneck"|"cycle", fillType, rootKey,
|
||||
-- detail = manglende l/md, need = behov l/md}). Køb af en rest efter gårdens stalde -> stald-tekst. En producent spilleren selv har slukket (last.userOff, stadig
|
||||
-- slukket) slår igennem som "slukket af dig" — det er den reelle årsag, uanset om modellen så køb eller flaskehals.
|
||||
function ADChainView.problemText(problem, last, textOf, titleOf, fmt)
|
||||
last, textOf, fmt = last or {}, textOf or identity, fmt or ADChainView.compact
|
||||
titleOf = titleOf or function(name) return (last.titles or {})[name] or tostring(name) end
|
||||
local item = titleOf(problem.fillType)
|
||||
if problem.kind == "cycle" then return string.format(textOf("spu_chain_problem_cycle"), item) end
|
||||
local userOff, names, seen = last.userOff or {}, {}, {}
|
||||
for _, recipe in ipairs(producersOf(last, problem.fillType)) do
|
||||
if userOff[recipe.key] and not recipe.enabled then
|
||||
return string.format(textOf("spu_chain_problem_off"), tostring(recipe.buildingName), titleOf(recipe.name))
|
||||
end
|
||||
local name = tostring(recipe.buildingName)
|
||||
if (recipe.cyclesPerMonth or 0) > 0 and not seen[name] then
|
||||
seen[name] = true
|
||||
table.insert(names, name)
|
||||
end
|
||||
end
|
||||
if problem.kind == "bottleneck" and #names > 0 then
|
||||
local need = problem.need or problem.detail or 0
|
||||
local made = math.max(0, need - (problem.detail or 0))
|
||||
return string.format(textOf("spu_chain_problem_bottleneck"), joinCapped(names), fmt(math.floor(made + 0.5)), fmt(math.floor(need + 0.5)))
|
||||
end
|
||||
-- modellen melder også "køb" for resten, når gårdens stalde leverer for lidt: så er rådet ikke "byg en fabrik"
|
||||
for _, herd in ipairs(last.data ~= nil and last.data.husbandry or {}) do
|
||||
if ((herd.outputs or {})[problem.fillType] or 0) > 0 then
|
||||
return string.format(textOf("spu_chain_problem_barn"), item)
|
||||
end
|
||||
end
|
||||
return string.format(textOf("spu_chain_problem_buy"), item)
|
||||
end
|
||||
|
||||
local function sourceText(node, last, textOf)
|
||||
local names = {}
|
||||
if node.source == "production" then
|
||||
for _, producer in ipairs(node.producers or {}) do table.insert(names, tostring(producer.buildingName)) end
|
||||
elseif node.source == "husbandry" then
|
||||
for _, herd in ipairs(node.herds or {}) do table.insert(names, tostring(herd.buildingName)) end
|
||||
elseif node.source == "field" then
|
||||
-- markvare uden egen afgrøde (hø, hakkelse …): ingen afgrødeplan, bare "fra marker"
|
||||
if node.fruit == nil then return textOf("spu_chain_fromFields") end
|
||||
for _, field in ipairs(last.fields or {}) do
|
||||
if plannedFruit(field) == node.fruit then table.insert(names, "F" .. tostring(field.id)) end
|
||||
end
|
||||
if #names == 0 then return textOf("spu_chain_noField") end
|
||||
elseif node.source == "buy" then
|
||||
return textOf("spu_chain_buySource")
|
||||
else
|
||||
return "-"
|
||||
end
|
||||
return joinCapped(names)
|
||||
end
|
||||
|
||||
-- Nodens status (tekst, niveau): kredsløb/flaskehals/køb/mangel (rød) > flow ikke ok (gul) > stramt lager (gul) > flow ok.
|
||||
local function nodeStatus(node, last, fmt, textOf, short)
|
||||
if node.source == "cycle" then return textOf("spu_chain_cycle"), "error" end
|
||||
if (node.shortPerMonth or 0) > 0 then
|
||||
local key = node.source == "production" and "spu_chain_bottleneck" or "spu_chain_buy"
|
||||
return string.format(textOf(key), fmt(node.shortPerMonth)), "error"
|
||||
end
|
||||
local shortage = node.fruit ~= nil and short[node.fruit] or nil
|
||||
if shortage ~= nil then return string.format(textOf("spu_chain_fieldShort"), haText(shortage.missingHa)), "error" end
|
||||
local status = node.consumer ~= nil and (last.flowStatus or {})[edgeKey(node)] or nil
|
||||
-- udgangen står på Fordel: spillet leverer selv
|
||||
if status == "auto" then return textOf("spu_chain_flowAuto"), "running" end
|
||||
if status ~= nil and status ~= "ok" then return textOf(FLOW_KEYS[status] or "spu_chain_flowNone"), "waiting" end
|
||||
if isTight(node, last, short) then return textOf("spu_chain_tight"), "waiting" end
|
||||
if status == "ok" then return textOf("spu_chain_flowOk"), "running" end
|
||||
return "", "running"
|
||||
end
|
||||
|
||||
local function joinText(cells)
|
||||
local parts = {}
|
||||
for _, cell in ipairs(cells) do
|
||||
if cell ~= nil and cell ~= "" then table.insert(parts, cell) end
|
||||
end
|
||||
return table.concat(parts, " · ")
|
||||
end
|
||||
|
||||
local function makeRow(kind, indent, level, cells, actions, toggle)
|
||||
local fitted = fitCells(cells)
|
||||
return {kind = kind, indent = indent, level = level, cells = fitted, actions = actions or {}, toggle = toggle, text = joinText(fitted)}
|
||||
end
|
||||
|
||||
local function suggestRow(action, indent, titleOf, textOf)
|
||||
local pad = string.rep(ADChainView.INDENT, indent)
|
||||
local from = (action.from ~= nil and action.from ~= "") and titleOf(action.from) or "-"
|
||||
local cells = {pad .. "F" .. tostring(action.fieldId) .. " " .. from, "→ " .. titleOf(action.to), haText(action.hectares) .. " ha"}
|
||||
if action.mode == "suggest" then
|
||||
cells[4], cells[5] = textOf("spu_chain_approve"), textOf("spu_chain_dismiss")
|
||||
return makeRow("suggest", indent, "waiting", cells,
|
||||
{[4] = {kind = "approve", fieldId = action.fieldId}, [5] = {kind = "dismiss", fieldId = action.fieldId}})
|
||||
end
|
||||
cells[4] = textOf("spu_chain_auto")
|
||||
return makeRow("auto", indent, "running", cells)
|
||||
end
|
||||
|
||||
-- Handlinger for de afgrøder (i rækkefølge) der hentes fra mark under roden.
|
||||
local function rootFruits(root)
|
||||
local fruits, seen = {}, {}
|
||||
eachNode(root.children, function(node)
|
||||
if node.source == "field" and node.fruit ~= nil and not seen[node.fruit] then
|
||||
seen[node.fruit] = true
|
||||
table.insert(fruits, node.fruit)
|
||||
end
|
||||
end)
|
||||
return fruits
|
||||
end
|
||||
|
||||
local function actionsFor(last, fruit, modes)
|
||||
local list = {}
|
||||
for _, action in ipairs(last.actions or {}) do
|
||||
if action.to == fruit and modes[action.mode] then table.insert(list, action) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Alle forslag (mode "suggest") én gang pr. mark, efter marknummer (in-game 30/9: samme forslag stod under
|
||||
-- hver kæde der bruger afgrøden, spredt mellem kæderne).
|
||||
function ADChainView.suggestionList(last)
|
||||
local byField, list = {}, {}
|
||||
for _, action in ipairs((last or {}).actions or {}) do
|
||||
if action.mode == "suggest" and action.fieldId ~= nil and not byField[action.fieldId] then
|
||||
byField[action.fieldId] = true
|
||||
table.insert(list, action)
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return (tonumber(a.fieldId) or 0) < (tonumber(b.fieldId) or 0) end)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Kæder efter status (fejl/mangel, venter, dækket, pause), så bygning, så produkt.
|
||||
local LEVEL_RANK = {error = 1, waiting = 2, running = 3, stopped = 4}
|
||||
function ADChainView.sortRoots(roots, last)
|
||||
local keyed = {}
|
||||
for index, root in ipairs(roots or {}) do
|
||||
table.insert(keyed, {root = root, rank = LEVEL_RANK[ADChainView.rootLevel(root, last)] or 5,
|
||||
building = tostring(root.buildingName or ""), name = tostring(root.name or ""), index = index})
|
||||
end
|
||||
table.sort(keyed, function(a, b)
|
||||
if a.rank ~= b.rank then return a.rank < b.rank end
|
||||
if a.building ~= b.building then return a.building < b.building end
|
||||
if a.name ~= b.name then return a.name < b.name end
|
||||
return a.index < b.index
|
||||
end)
|
||||
local sorted = {}
|
||||
for _, item in ipairs(keyed) do table.insert(sorted, item.root) end
|
||||
return sorted
|
||||
end
|
||||
|
||||
local function rootRows(rows, root, last, ctx, isExpanded)
|
||||
local level = ADChainView.rootLevel(root, last)
|
||||
local paused = (last.paused or {})[root.key] == true
|
||||
local suggestions = {}
|
||||
for _, fruit in ipairs(rootFruits(root)) do
|
||||
for _, action in ipairs(actionsFor(last, fruit, {suggest = true})) do table.insert(suggestions, action) end
|
||||
end
|
||||
local status = ctx.textOf(ROOT_STATUS[level])
|
||||
if #suggestions > 0 then status = string.format(ctx.textOf("spu_chain_rootSummary"), status, #suggestions) end
|
||||
local marker = (#(root.children or {}) > 0) and (isExpanded and "– " or "+ ") or ""
|
||||
-- slutproduktet: "Sælg straks" (standard) eller "Behold" — klik på feltet skifter
|
||||
-- varer gården selv bruger (last.sellable uden roden) sælges aldrig: "Gård" uden klik
|
||||
local keep = (last.finalKeep or {})[root.key] == true
|
||||
local sellable = last.sellable == nil or last.sellable[root.key] == true
|
||||
local finalText = not sellable and "spu_chain_finalFarm" or (keep and "spu_chain_finalKeep" or "spu_chain_finalSell")
|
||||
local actions = {[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}
|
||||
if sellable then actions[3] = {kind = "finalMode", rootKey = root.key, keep = not keep} end
|
||||
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""),
|
||||
ctx.textOf(finalText), status, ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")}, actions, root.key))
|
||||
if not isExpanded then return end
|
||||
local shown, problems, told = {}, {}, {}
|
||||
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||
if problem.rootKey == root.key then table.insert(problems, problem) end
|
||||
end
|
||||
local function problemRows(fillType, indent)
|
||||
for _, problem in ipairs(problems) do
|
||||
local key = tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||
if (fillType == nil or problem.fillType == fillType) and not told[key] then
|
||||
told[key] = true
|
||||
table.insert(rows, {kind = "problem", indent = indent, level = "error",
|
||||
text = string.rep(ADChainView.INDENT, indent) .. ADChainView.problemText(problem, last, ctx.textOf, ctx.titleOf, ctx.fmt)})
|
||||
end
|
||||
end
|
||||
end
|
||||
eachNode(root.children, function(node, depth)
|
||||
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
|
||||
local stock = node.source == "field" and fieldStockOf(last, node) or nil
|
||||
local row = makeRow("node", depth, nodeLevel, {string.rep(ADChainView.INDENT, depth) .. ctx.titleOf(node.fillType),
|
||||
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
|
||||
row.fillType = node.fillType
|
||||
-- klik åbner flowet der kører varen til modtageren (in-game 30/9: "flow stopped" kunne ikke redigeres)
|
||||
if node.consumer ~= nil then row.openConsumer, row.openFillType = node.consumer, node.fillType end
|
||||
table.insert(rows, row)
|
||||
problemRows(node.fillType, depth + 1)
|
||||
if node.source == "field" and node.fruit ~= nil and not shown[node.fruit] then
|
||||
shown[node.fruit] = true
|
||||
for _, action in ipairs(actionsFor(last, node.fruit, {auto = true})) do
|
||||
table.insert(rows, suggestRow(action, depth + 1, ctx.titleOf, ctx.textOf))
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- problemer uden en vist vare (bør ikke ske) står sidst under roden
|
||||
problemRows(nil, 1)
|
||||
end
|
||||
|
||||
-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag].
|
||||
local function pausedRows(rows, last, ctx)
|
||||
local present, keys = {}, {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do present[root.key] = true end
|
||||
for key in pairs(last.paused or {}) do
|
||||
if not present[key] then table.insert(keys, key) end
|
||||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
for _, key in ipairs(keys) do
|
||||
local recipe
|
||||
for _, candidate in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||
if candidate.key == key then recipe = candidate end
|
||||
end
|
||||
local name = recipe ~= nil and recipe.name ~= nil and ctx.titleOf(recipe.name) or tostring(key)
|
||||
-- pauset kæde: [Start] genoptager, [Fjern] tager den ud af kæderne
|
||||
table.insert(rows, makeRow("root", 0, "stopped", {name, recipe ~= nil and tostring(recipe.buildingName or "") or "", "",
|
||||
ctx.textOf("spu_chain_optOut"), ctx.textOf("spu_chain_resume")},
|
||||
{[4] = {kind = "optOut", rootKey = key}, [5] = {kind = "resume", rootKey = key}}))
|
||||
end
|
||||
return #keys
|
||||
end
|
||||
|
||||
-- Tændte produktioner uden kæde: foldet sammen under én linje; udfoldet med [Kæde til] pr. produktion.
|
||||
local function candidateRows(rows, last, ctx, isExpanded)
|
||||
local list = last.candidates or {}
|
||||
if #list == 0 then return end
|
||||
local key = isExpanded and "spu_chain_candidatesHide" or "spu_chain_candidatesShow"
|
||||
table.insert(rows, makeRow("root", 0, nil, {(isExpanded and "– " or "+ ") .. string.format(ctx.textOf(key), #list), "", "", "", ""}, {}, "__candidates"))
|
||||
if not isExpanded then return end
|
||||
for _, candidate in ipairs(list) do
|
||||
table.insert(rows, makeRow("root", 1, nil, {ADChainView.INDENT .. ctx.titleOf(candidate.name), tostring(candidate.buildingName or ""), "",
|
||||
ctx.textOf("spu_chain_notChosen"), ctx.textOf("spu_chain_optIn")}, {[5] = {kind = "optIn", rootKey = candidate.key}}))
|
||||
end
|
||||
end
|
||||
|
||||
-- "Marker fri til kæden: F1, F2 +k" — så mange navne som tekstlinjen kan rumme, resten talt.
|
||||
function ADChainView.freeText(names, textOf)
|
||||
local format = textOf("spu_chain_free")
|
||||
if #names == 0 then return string.format(format, "-") end
|
||||
for count = #names, 1, -1 do
|
||||
local shown = {}
|
||||
for index = 1, count do shown[index] = names[index] end
|
||||
local list = table.concat(shown, ", ")
|
||||
if count < #names then list = list .. " +" .. tostring(#names - count) end
|
||||
local text = string.format(format, list)
|
||||
if ADChainView.textLength(text) <= ADChainView.TEXT_ROW_CHARS then return text end
|
||||
end
|
||||
return ADChainView.fit(string.format(format, "+" .. tostring(#names)), ADChainView.TEXT_ROW_CHARS)
|
||||
end
|
||||
|
||||
local function footerRows(rows, last, textOf)
|
||||
local free = {}
|
||||
for _, field in ipairs(last.fields or {}) do
|
||||
if field.chainFree then table.insert(free, field.id) end
|
||||
end
|
||||
table.sort(free)
|
||||
local names = {}
|
||||
for _, id in ipairs(free) do table.insert(names, "F" .. tostring(id)) end
|
||||
table.insert(rows, {kind = "info", indent = 0, text = ADChainView.freeText(names, textOf)})
|
||||
table.insert(rows, {kind = "info", indent = 0, text = string.format(textOf("spu_chain_switches"), last.switchCount or 0)})
|
||||
if last.fieldsIncomplete then table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_scanning")}) end
|
||||
end
|
||||
|
||||
-- expanded = {[rodnøgle] = true} for udfoldede rødder (alle andre er foldet sammen).
|
||||
function ADChainView.rows(last, titleOf, fmt, textOf, expanded)
|
||||
textOf, fmt, expanded = textOf or identity, fmt or identity, expanded or {}
|
||||
local titles = last ~= nil and last.titles or {}
|
||||
titleOf = titleOf or function(name) return (titles or {})[name] or tostring(name) end
|
||||
local rows = {}
|
||||
local roots = last ~= nil and last.chain ~= nil and last.chain.roots or {}
|
||||
local hasPaused = last ~= nil and next(last.paused or {}) ~= nil
|
||||
if #roots == 0 and not hasPaused then
|
||||
table.insert(rows, {kind = "empty", indent = 0, text = textOf("spu_chain_empty")})
|
||||
end
|
||||
if last == nil then return rows end
|
||||
if #roots > 0 or hasPaused then
|
||||
table.insert(rows, makeRow("header", 0, nil, {textOf("spu_chain_col_item"), textOf("spu_chain_col_source"),
|
||||
textOf("spu_chain_col_perMonth"), textOf("spu_chain_col_status"), textOf("spu_chain_col_stock")}))
|
||||
end
|
||||
local ctx = {titleOf = titleOf, fmt = fmt, textOf = textOf, short = shortageByFruit(last)}
|
||||
local suggestions = ADChainView.suggestionList(last)
|
||||
if #suggestions > 0 then
|
||||
table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_suggestHeader")})
|
||||
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 0, titleOf, textOf)) end
|
||||
end
|
||||
for _, root in ipairs(ADChainView.sortRoots(roots, last)) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
|
||||
pausedRows(rows, last, ctx)
|
||||
candidateRows(rows, last, ctx, expanded["__candidates"] == true)
|
||||
footerRows(rows, last, textOf)
|
||||
return rows
|
||||
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,47 @@
|
||||
|
||||
ADCpBridge = {}
|
||||
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon"}
|
||||
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).
|
||||
ADCpBridge.SPRAY_ROLES = {
|
||||
FERTILIZER = "fertilizer", LIQUIDFERTILIZER = "fertilizer", MANURE = "fertilizer",
|
||||
LIQUIDMANURE = "fertilizer", DIGESTATE = "fertilizer", LIME = "limer", HERBICIDE = "weeder",
|
||||
}
|
||||
function ADCpBridge.sprayRoleForFillType(name)
|
||||
return ADCpBridge.SPRAY_ROLES[name]
|
||||
end
|
||||
|
||||
-- Afgrøder et skærebord/optager kan høste (spec_cutter). Finsnittere (fruitTypeConverters) tager ingen:
|
||||
-- de laver hakkelse, ikke korn/roer.
|
||||
function ADCpBridge.harvestFruitSet(cutterSpec)
|
||||
local fruits = {}
|
||||
if cutterSpec == nil then return fruits end
|
||||
if type(cutterSpec.fruitTypeConverters) == "table" and next(cutterSpec.fruitTypeConverters) ~= nil then
|
||||
return fruits
|
||||
end
|
||||
-- (ikke ipairs over {a, b}: den stopper ved første nil)
|
||||
for _, key in ipairs({"fruitTypes", "fruitTypeIndices"}) do
|
||||
local source = cutterSpec[key]
|
||||
if type(source) == "table" then
|
||||
for key, value in pairs(source) do
|
||||
if type(value) == "number" then fruits[value] = true
|
||||
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
|
||||
-- Må afgrøden sås i perioden? (growthDataSeasonal.periods[p].plantingAllowed, bevist i AI-Bonden).
|
||||
-- Ukendte data = tilladt (kort uden sæsondata).
|
||||
function ADCpBridge.isPlantableIn(fruit, period)
|
||||
local periods = fruit ~= nil and fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
|
||||
local data = periods ~= nil and periods[period] or nil
|
||||
if type(data) ~= "table" then return true end
|
||||
return data.plantingAllowed == true
|
||||
end
|
||||
|
||||
function ADCpBridge.isAvailable()
|
||||
return AIJobType ~= nil and AIJobType.FIELDWORK_CP ~= nil
|
||||
@@ -22,34 +62,532 @@ local function collect(vehicle, list)
|
||||
return list
|
||||
end
|
||||
|
||||
-- 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, 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)
|
||||
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)
|
||||
local specs = {}
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if object[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-- Mekanisk strigle (kun småt ukrudt) frem for ukrudtssprøjte.
|
||||
function ADCpBridge.isMechanicalWeeder(vehicle)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_weeder ~= nil then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function seederOf(vehicle)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sowingMachine ~= nil then return object end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Har såmaskinen frø på? (fyldenheder med SEEDS)
|
||||
function ADCpBridge.hasSeeds(vehicle)
|
||||
local seeder = seederOf(vehicle)
|
||||
if seeder == nil then return false end
|
||||
local ok, has = pcall(function()
|
||||
local seedsIndex = g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||
for index, unit in ipairs(seeder.spec_fillUnit ~= nil and seeder.spec_fillUnit.fillUnits or {}) do
|
||||
if unit.fillType == seedsIndex and (unit.fillLevel or 0) > 0 then return true end
|
||||
if unit.supportedFillTypes ~= nil and unit.supportedFillTypes[seedsIndex] and seeder:getFillUnitFillLevel(index) > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end)
|
||||
return ok and has == true
|
||||
end
|
||||
|
||||
-- Afgrøder såmaskinen kan så (spec_sowingMachine.seeds).
|
||||
function ADCpBridge.getSeedFruits(vehicle)
|
||||
local seeder = seederOf(vehicle)
|
||||
local fruits = {}
|
||||
local seeds = seeder ~= nil and seeder.spec_sowingMachine.seeds or nil
|
||||
for _, fruit in pairs(seeds or {}) do fruits[fruit] = true end
|
||||
return fruits
|
||||
end
|
||||
|
||||
-- Sæt såmaskinens afgrøde (setSeedIndex(slot), bevist i AI-Bonden).
|
||||
function ADCpBridge.setSeedFruit(vehicle, fruitIndex)
|
||||
local seeder = seederOf(vehicle)
|
||||
if seeder == nil then return false, "ingen såmaskine" end
|
||||
for slot, fruit in pairs(seeder.spec_sowingMachine.seeds or {}) do
|
||||
if fruit == fruitIndex then
|
||||
if seeder.setSeedIndex ~= nil then
|
||||
local ok, err = pcall(seeder.setSeedIndex, seeder, slot)
|
||||
return ok, not ok and tostring(err) or nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return false, "såmaskinen kan ikke så afgrøden"
|
||||
end
|
||||
|
||||
function ADCpBridge.isPlantableNow(fruitIndex)
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
|
||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||
return ADCpBridge.isPlantableIn(fruit, environment ~= nil and environment.currentPeriod or 0)
|
||||
end
|
||||
|
||||
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||
-- 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")
|
||||
end
|
||||
end
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
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
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvor meget af påfyldningsvaren sættet har tilbage (liter, alle fyldenheder med varen).
|
||||
function ADCpBridge.getRefillLevel(vehicle, fillType)
|
||||
local total = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
pcall(function()
|
||||
for index, unit in ipairs(object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits or {}) do
|
||||
if unit.fillType == fillType then total = total + (object:getFillUnitFillLevel(index) or 0) end
|
||||
end
|
||||
end)
|
||||
end
|
||||
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()
|
||||
if vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() and vehicle.stopCurrentAIJob ~= nil then
|
||||
vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new())
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Variabel bredde (Unrealistic Pack m.fl.) først, som Courseplays WorkWidthUtil: AI-markørerne står på
|
||||
-- grundbredden (læssevogn 2,4 m selv om den er sat til 40,8 m).
|
||||
local function variableWidth(object)
|
||||
if object.getVariableWorkWidth == nil then return nil end
|
||||
local okLeft, left, _, leftValid = pcall(object.getVariableWorkWidth, object, true)
|
||||
local okRight, right, _, rightValid = pcall(object.getVariableWorkWidth, object)
|
||||
if okLeft and okRight and leftValid and rightValid and type(left) == "number" and type(right) == "number" then
|
||||
return math.abs(left) + math.abs(right)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Courseplays egen bredde-indstilling (den i HUD'en) er den marken faktisk køres med.
|
||||
local function courseplayWidth(vehicle)
|
||||
if vehicle.getCourseGeneratorSettings == nil then return nil end
|
||||
local ok, width = pcall(function() return vehicle:getCourseGeneratorSettings().workWidth:getValue() end)
|
||||
if ok and type(width) == "number" and width > 0 then return width end
|
||||
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 ADCpBridge.widthOrVine(cpWidth, hasVine) end
|
||||
local best = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.getAIMarkers ~= nil then
|
||||
local variable = variableWidth(object)
|
||||
if variable ~= nil then
|
||||
best = math.max(best, variable)
|
||||
elseif object.getAIMarkers ~= nil then
|
||||
pcall(function() if object.updateAIMarkerWidth ~= nil then object:updateAIMarkerWidth() end end)
|
||||
local ok, _, _, _, _, width = pcall(object.getAIMarkers, object)
|
||||
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
|
||||
|
||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
-- Courseplays værktøjsforskydning (venstre positiv) på køretøjet; den der flytter kursuslinjen.
|
||||
function ADCpBridge.getToolOffset(vehicle)
|
||||
if vehicle == nil or vehicle.getCpSettings == nil then return 0 end
|
||||
local ok, value = pcall(function() return vehicle:getCpSettings().toolOffsetX:getValue() end)
|
||||
return ok and type(value) == "number" and value or 0
|
||||
end
|
||||
|
||||
function ADCpBridge.setToolOffset(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpSettings == nil then return false end
|
||||
return (pcall(function() vehicle:getCpSettings().toolOffsetX:setFloatValue(value) end))
|
||||
end
|
||||
|
||||
-- Riverens XML: workArea-nr (1-baseret) -> dropWindrowWorkAreaIndex. Bruges når spillet ikke har feltet
|
||||
-- på workArea-tabellen. Cachet pr. fil.
|
||||
ADCpBridge.dropIndexCache = {}
|
||||
function ADCpBridge.readDropIndicesFromXml(fileName)
|
||||
if ADCpBridge.dropIndexCache[fileName] ~= nil then return ADCpBridge.dropIndexCache[fileName] end
|
||||
local indices = {}
|
||||
local ok = pcall(function()
|
||||
local xmlFile = XMLFile.load("adspuRake", fileName)
|
||||
if xmlFile == nil then return end
|
||||
local position = 0
|
||||
xmlFile:iterate("vehicle.workAreas.workArea", function(_, key)
|
||||
position = position + 1
|
||||
local dropIndex = xmlFile:getInt(key .. ".windrower#dropWindrowWorkAreaIndex")
|
||||
if dropIndex ~= nil then indices[position] = dropIndex end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
end)
|
||||
if not ok then indices = {} end
|
||||
ADCpBridge.dropIndexCache[fileName] = indices
|
||||
return indices
|
||||
end
|
||||
|
||||
-- Hvor langt til venstre (positiv) for rivens kursuslinje skåret lægges: midten af nedlægningsområdet i
|
||||
-- traktorens ramme minus rivens egen CP-forskydning. Midterrive ≈ 0, siderive fx 3 m. nil = ingen river.
|
||||
function ADCpBridge.getWindrowOffset(vehicle)
|
||||
local root = vehicle.rootNode
|
||||
local sum, count = 0, 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
local areas = object.spec_windrower ~= nil and object.spec_workArea ~= nil and object.spec_workArea.workAreas or nil
|
||||
if areas ~= nil then
|
||||
local fromXml = nil
|
||||
for position, workArea in ipairs(areas) do
|
||||
local dropIndex = workArea.dropWindrowWorkAreaIndex
|
||||
if dropIndex == nil and object.configFileName ~= nil then
|
||||
fromXml = fromXml or ADCpBridge.readDropIndicesFromXml(object.configFileName)
|
||||
dropIndex = fromXml[position]
|
||||
end
|
||||
local drop = dropIndex ~= nil and areas[dropIndex] or nil
|
||||
if drop ~= nil and drop.width ~= nil and drop.height ~= nil then
|
||||
local widthX = localToLocal(drop.width, root, 0, 0, 0)
|
||||
local heightX = localToLocal(drop.height, root, 0, 0, 0)
|
||||
sum, count = sum + (widthX + heightX) / 2, count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if count == 0 then return nil end
|
||||
return sum / count - ADCpBridge.getToolOffset(vehicle)
|
||||
end
|
||||
|
||||
-- Køretøjets nuværende Courseplay-kursus (slåmaskinens kursus genbruges af de næste trin).
|
||||
function ADCpBridge.getCourse(vehicle)
|
||||
if vehicle == nil or vehicle.getFieldWorkCourse == nil then return nil end
|
||||
local ok, course = pcall(vehicle.getFieldWorkCourse, 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
|
||||
|
||||
-- Kursusbredde til Courseplay: højst 50 m (CP's grænse; Unrealistic-redskaber kan sættes bredere). nil = urørt.
|
||||
ADCpBridge.CP_MAX_WIDTH = 50
|
||||
function ADCpBridge.cpCourseWidth(width)
|
||||
if type(width) == "number" and width > ADCpBridge.CP_MAX_WIDTH then return ADCpBridge.CP_MAX_WIDTH end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function clampCourseWidth(vehicle)
|
||||
pcall(function()
|
||||
local setting = vehicle:getCourseGeneratorSettings().workWidth
|
||||
local capped = ADCpBridge.cpCourseWidth(setting:getValue())
|
||||
if capped ~= nil then
|
||||
setting:setValue(capped)
|
||||
Logging.info("ADSmartPickup: kursusbredde for '%s' begrænset til %d m (Courseplays maksimum)", tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), capped)
|
||||
end
|
||||
end)
|
||||
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)
|
||||
end
|
||||
|
||||
-- Sæt køretøjets bane på et multi-tool-kursus (samme som HUD'ens "Baneforskydning").
|
||||
local function setLane(vehicle, params, lane)
|
||||
if lane == nil then return end
|
||||
pcall(function()
|
||||
local course = vehicle:getFieldWorkCourse()
|
||||
if course ~= nil and course.getMultiTools ~= nil and course:getMultiTools() > 1 then course:setPosition(lane) end
|
||||
end)
|
||||
if params ~= nil and params.laneOffset ~= nil then pcall(function() params.laneOffset:setValue(lane) end) end
|
||||
end
|
||||
|
||||
-- Start Courseplay-markarbejde. opts (valgfri):
|
||||
-- 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)
|
||||
-- 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
|
||||
return nil, "Courseplay (FIELDWORK_CP) findes ikke"
|
||||
end
|
||||
@@ -61,6 +599,12 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
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)
|
||||
@@ -68,17 +612,48 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
handle.finished = true
|
||||
if not handle.cancelled then onDone(ok, reason) end
|
||||
end
|
||||
local function startWithCourse()
|
||||
setLane(vehicle, params, opts.lane)
|
||||
job:setValues()
|
||||
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 (handle.reusedCourse and "reused" or nil))
|
||||
end
|
||||
local function generateAndStart()
|
||||
if handle.cancelled then return end
|
||||
job:onClickGenerateFieldWorkCourse(function(course)
|
||||
if handle.cancelled then return end
|
||||
if not course then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
job:setValues()
|
||||
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)
|
||||
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
|
||||
handle.copiedCourse = true
|
||||
return startWithCourse()
|
||||
end
|
||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||
end
|
||||
setMultiTools(vehicle, opts.multiTools or 1)
|
||||
clampCourseWidth(vehicle)
|
||||
-- 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
|
||||
@@ -96,3 +671,90 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
end
|
||||
return handle
|
||||
end
|
||||
|
||||
-- Aflæsningsvogn under høst: Courseplays combine-unloader (bevist i AI-Bonden). Vognen venter ved
|
||||
-- (waitX, waitZ) — markens F-markør — følger høsteren på marken og kører fuld til station. station =
|
||||
-- den station Smart Pickups silovalg fandt; findes den ikke i Courseplays liste, bruges nærmeste.
|
||||
function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station, onDone)
|
||||
if AIJobType == nil or AIJobType.COMBINE_UNLOADER_CP == nil then
|
||||
return nil, "Courseplay (COMBINE_UNLOADER_CP) findes ikke"
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.COMBINE_UNLOADER_CP)
|
||||
if job == nil then return nil, "kunne ikke oprette aflæsningsjob" end
|
||||
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
|
||||
local params = job.cpJobParameters
|
||||
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)
|
||||
local chosen = nil
|
||||
local ok, stations = pcall(function() return job:getUnloadingStations() end)
|
||||
if ok and type(stations) == "table" then
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, candidate in ipairs(stations) do
|
||||
if station ~= nil and (candidate == station or candidate.owningPlaceable == station.owningPlaceable) then
|
||||
best, bestDistance = candidate, -1
|
||||
elseif bestDistance >= 0 then
|
||||
local okPos, sx, sz = pcall(function()
|
||||
local x, _, z = getWorldTranslation(candidate.owningPlaceable.rootNode)
|
||||
return x, z
|
||||
end)
|
||||
local distance = okPos and ((sx - fieldX) ^ 2 + (sz - fieldZ) ^ 2) or math.huge
|
||||
if distance < bestDistance then best, bestDistance = candidate, distance end
|
||||
end
|
||||
end
|
||||
chosen = best
|
||||
end
|
||||
if chosen == nil or params == nil or params.unloadingStation == nil then
|
||||
return nil, "ingen aflæsningsstation Courseplay kan bruge"
|
||||
end
|
||||
pcall(function() params.unloadingStation:setValue(NetworkUtil.getObjectId(chosen)) end)
|
||||
pcall(function() job:setValues() end)
|
||||
local handle = {cancelled = false, fired = false, finished = false, stationName = chosen.getName ~= nil and chosen:getName() or nil}
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,91 @@
|
||||
-- AD Crop Plan (ren logik)
|
||||
-- Mangel pr. afgrøde over 12 måneder (lager + forventet høst mod kædens behov) og valg af marker der skal skifte
|
||||
-- målafgrøde: "fri til kæden" -> auto, ellers forslag. Brugerens egen målafgrøde ændres aldrig automatisk.
|
||||
|
||||
ADCropPlan = {}
|
||||
ADCropPlan.HORIZON_MONTHS = 12
|
||||
ADCropPlan.MIN_SHARE = 0.10
|
||||
ADCropPlan.MIN_HA = 0.5
|
||||
-- højst så mange forslag (ikke auto-skift) pr. afgrøde, så listen ikke vokser til snesevis af rækker
|
||||
ADCropPlan.MAX_SUGGEST_PER_FRUIT = 3
|
||||
|
||||
function ADCropPlan.plannedFruit(field)
|
||||
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||
return field.fruit
|
||||
end
|
||||
|
||||
function ADCropPlan.shortages(demand, stock, fields, yields)
|
||||
local list = {}
|
||||
for fruit, perMonth in pairs(demand or {}) do
|
||||
local perHa = (yields or {})[fruit] or 0
|
||||
if perHa > 0 and (perMonth or 0) > 0 then
|
||||
local need = perMonth * ADCropPlan.HORIZON_MONTHS
|
||||
local supply = (stock or {})[fruit] or 0
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if ADCropPlan.plannedFruit(field) == fruit then supply = supply + (field.areaHa or 0) * perHa end
|
||||
end
|
||||
local missing = need - supply
|
||||
local missingHa = missing / perHa
|
||||
if missing > need * ADCropPlan.MIN_SHARE and missingHa > ADCropPlan.MIN_HA then
|
||||
table.insert(list, {fruit = fruit, needLiters = need, supplyLiters = supply, missingLiters = missing, missingHa = missingHa})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.missingHa ~= b.missingHa then return a.missingHa > b.missingHa end
|
||||
return a.fruit < b.fruit
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
local function isCandidate(field, used, dismissedFruit, fruit)
|
||||
if field.permanent or field.excluded then return false end
|
||||
local planned = ADCropPlan.plannedFruit(field)
|
||||
if planned ~= nil and planned ~= "" and used[planned] then return false end
|
||||
return dismissedFruit ~= fruit
|
||||
end
|
||||
|
||||
-- prices: {[afgrøde] = pris} eller en funktion afgrøde -> pris (slås kun op for kandidaternes afgrøder).
|
||||
-- harvesting = {[afgrøde] = true}: afgrøder der høstes lige nu — ingen forslag/skift for dem (læssene er på vej).
|
||||
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed, harvesting)
|
||||
local actions, taken = {}, {}
|
||||
local used = usedFruits or {}
|
||||
local priceOf = type(prices) == "function" and prices or function(fruit) return (prices or {})[fruit] or 0 end
|
||||
local priceCache = {}
|
||||
local function valueOf(fruit)
|
||||
if fruit == "" then return 0 end
|
||||
if priceCache[fruit] == nil then priceCache[fruit] = ((yields or {})[fruit] or 0) * (priceOf(fruit) or 0) end
|
||||
return priceCache[fruit]
|
||||
end
|
||||
for _, shortage in ipairs(shortages or {}) do
|
||||
local remaining = (harvesting or {})[shortage.fruit] and 0 or shortage.missingHa
|
||||
local candidates = {}
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if not taken[field.id] and isCandidate(field, used, (dismissed or {})[field.id], shortage.fruit) then
|
||||
local planned = ADCropPlan.plannedFruit(field) or ""
|
||||
local value = valueOf(planned)
|
||||
table.insert(candidates, {field = field, value = value})
|
||||
end
|
||||
end
|
||||
table.sort(candidates, function(a, b)
|
||||
if a.value ~= b.value then return a.value < b.value end
|
||||
local da, db = math.abs((a.field.areaHa or 0) - remaining), math.abs((b.field.areaHa or 0) - remaining)
|
||||
if da ~= db then return da < db end
|
||||
return a.field.id < b.field.id
|
||||
end)
|
||||
local suggested = 0
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if remaining <= 0 then break end
|
||||
local field = candidate.field
|
||||
local auto = field.chainFree == true and field.targetSource ~= "user"
|
||||
if auto or suggested < ADCropPlan.MAX_SUGGEST_PER_FRUIT then
|
||||
table.insert(actions, {fieldId = field.id, from = ADCropPlan.plannedFruit(field) or "", to = shortage.fruit,
|
||||
hectares = field.areaHa or 0, reason = shortage.fruit, mode = auto and "auto" or "suggest"})
|
||||
taken[field.id] = true
|
||||
remaining = remaining - (field.areaHa or 0)
|
||||
if not auto then suggested = suggested + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
return actions
|
||||
end
|
||||
@@ -0,0 +1,190 @@
|
||||
-- AD Dependency (ren logik)
|
||||
-- Advarsel når spilleren slukker en produktion gården er afhængig af (spec 2026-09-30-produktionskaeder afsnit 8).
|
||||
-- En vare er "nødvendig" når et tændt forsyningsflow kører den (manuel vare, eller flowets mål tager den i
|
||||
-- auto-tilstand), en stald æder den, eller en aktiv kæde har den som input. Varer er NAVNE; kapacitet og forbrug
|
||||
-- i samme enhed (adapteren i adChain.lua vælger), lagerdage i spillets døgn. Ingen spiladgang.
|
||||
|
||||
ADDependency = {}
|
||||
ADDependency.STOCK_DAYS_OK = 60 -- lager til mere end så mange døgn: ingen advarsel
|
||||
ADDependency.MAX_USERS = 2 -- brugere nævnt i beskeden; resten "+k"
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
local function sortedKeys(map)
|
||||
local keys = {}
|
||||
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||
table.sort(keys)
|
||||
return keys
|
||||
end
|
||||
|
||||
-- flows = {{name, kind, stopped, targetBuildingIds, ingredientMode, ingredients, chain}} (chain = kædens rodnøgle for kædens flows)
|
||||
-- barnFood = {[vare] = {staldnavn...}}, chainInputs = {[vare] = {kædenavn...}}
|
||||
-- accepts = {[bygnings-id] = {[vare] = true}}: hvad auto-flowenes mål tager (adapteren slår op)
|
||||
-- -> {[vare] = {users = {navn...}, onlyChain, strong}} (brugere i rækkefølgen flows, stalde, kæder; hver kun én gang).
|
||||
-- strong = varen er valgt som manuel vare i et flow eller er kæde-input: behovet gælder også uden kendt forbrug.
|
||||
-- Stald-foder og auto-flowenes mål er "svage": uden kendt forbrug (lagerdage) er de ikke nødvendige.
|
||||
function ADDependency.neededOutputs(flows, barnFood, chainInputs, accepts)
|
||||
local result = {}
|
||||
local function add(name, user, isChain, isStrong)
|
||||
if name == nil or user == nil then return end
|
||||
local entry = result[name]
|
||||
if entry == nil then
|
||||
entry = {users = {}, onlyChain = true, strong = false}
|
||||
result[name] = entry
|
||||
end
|
||||
if not isChain then entry.onlyChain = false end
|
||||
if isStrong then entry.strong = true end
|
||||
if not contains(entry.users, user) then table.insert(entry.users, user) end
|
||||
end
|
||||
for _, flow in ipairs(flows or {}) do
|
||||
if flow.kind ~= "outbound" and not flow.stopped then
|
||||
local user = tostring(flow.name or "?")
|
||||
-- kædens egne flows (flow.chain) er kæde-behov: Task 10 melder kæderne, så de gør ikke onlyChain falsk
|
||||
local isChainFlow = flow.chain ~= nil and flow.chain ~= ""
|
||||
if flow.ingredientMode == "manual" then
|
||||
for _, name in ipairs(flow.ingredients or {}) do add(name, user, isChainFlow, true) end
|
||||
else
|
||||
for _, building in ipairs(flow.targetBuildingIds or {}) do
|
||||
for _, name in ipairs(sortedKeys((accepts or {})[building])) do add(name, user, isChainFlow, isChainFlow) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(sortedKeys(barnFood)) do
|
||||
for _, barn in ipairs(barnFood[name]) do add(name, tostring(barn), false) end
|
||||
end
|
||||
for _, name in ipairs(sortedKeys(chainInputs)) do
|
||||
for _, root in ipairs(chainInputs[name]) do add(name, tostring(root), true, true) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er kædens (samme regel som ADChain.enabledKeys) -> {[nøgle] = true}
|
||||
function ADDependency.enabledKeys(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioners kapacitet pr. vare (outputs × cyklusser pr. måned) -> {[vare] = mængde}
|
||||
function ADDependency.capacity(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
result[name] = (result[name] or 0) + (amount or 0) * (recipe.cyclesPerMonth or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Dækker andre tændte produktioner varen? Ukendt/intet forbrug: enhver kapacitet dækker.
|
||||
local function isCovered(name, otherCapacity, demand)
|
||||
local capacity = (otherCapacity or {})[name] or 0
|
||||
return capacity > 0 and capacity >= ((demand or {})[name] or 0)
|
||||
end
|
||||
|
||||
-- Rækker lageret længe nok? nil = ukendt (advar, uden dage).
|
||||
local function stockLasts(name, stockDays)
|
||||
local days = (stockDays or {})[name]
|
||||
return days ~= nil and days > ADDependency.STOCK_DAYS_OK
|
||||
end
|
||||
|
||||
-- {[bygning|vare] = true} for tændte opskrifter: spilleren har valgt hvilken opskrift i fabrikken der kører.
|
||||
local function enabledInBuilding(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
if (amount or 0) > 0 then result[tostring(recipe.building) .. "|" .. name] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Er behovet for varen reelt? Kun kæder: nej (Task 10 melder kæderne). Svagt behov (stald-foder, auto-flowets mål)
|
||||
-- kræver kendt forbrug (lagerdage); manuel flow-vare og kæde-input gælder også uden.
|
||||
local function isNeeded(need, name, stockDays)
|
||||
if need == nil or need.onlyChain then return false end
|
||||
return need.strong == true or (stockDays or {})[name] ~= nil
|
||||
end
|
||||
|
||||
-- Første nødvendige, udækkede vare (efter navn) for en slukket produktion (ikke kædens) -> advarsel eller nil.
|
||||
local function check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
if recipe.enabled or recipe.byChain then return nil end
|
||||
for _, name in ipairs(sortedKeys(recipe.outputs)) do
|
||||
local need = (needed or {})[name]
|
||||
if isNeeded(need, name, stockDays) and (recipe.outputs[name] or 0) > 0
|
||||
and not alternatives[tostring(recipe.building) .. "|" .. name]
|
||||
and not isCovered(name, otherCapacity, demand) and not stockLasts(name, stockDays) then
|
||||
local days = (stockDays or {})[name]
|
||||
return {recipeKey = recipe.key, building = recipe.building, buildingName = recipe.buildingName, fillType = name,
|
||||
users = need.users, onlyChain = need.onlyChain == true, days = days}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Alle slukkede, nødvendige produktioner (én pr. fabrik) — "Slukket men nødvendig" i Kræver handling.
|
||||
function ADDependency.offNeeded(recipes, needed, stockDays, otherCapacity, demand)
|
||||
local result, seen = {}, {}
|
||||
local alternatives = enabledInBuilding(recipes)
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
if warning ~= nil and not seen[tostring(warning.building)] then
|
||||
seen[tostring(warning.building)] = true
|
||||
table.insert(result, warning)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Fælles regler (warnings og offNeeded): ingen advarsel når en anden opskrift i SAMME fabrik laver varen og er
|
||||
-- tændt, når andre tændte produktioner dækker behovet, når lageret rækker > 60 dage, eller når kun kæder bruger varen.
|
||||
-- Nye slukninger siden sidste evaluering: tændt i prevEnabled, slukket nu, nødvendig og ikke dækket.
|
||||
-- prevEnabled = nil (første evaluering efter indlæsning) -> ingen. Én pr. fabrik og vare.
|
||||
function ADDependency.warnings(prevEnabled, recipes, needed, stockDays, otherCapacity, demand)
|
||||
local result, seen = {}, {}
|
||||
if prevEnabled == nil then return result end
|
||||
local alternatives = enabledInBuilding(recipes)
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if prevEnabled[recipe.key] then
|
||||
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
local id = warning ~= nil and (tostring(warning.building) .. "|" .. warning.fillType) or nil
|
||||
if warning ~= nil and not seen[id] then
|
||||
seen[id] = true
|
||||
table.insert(result, warning)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- "A, B +2"
|
||||
function ADDependency.usersText(users)
|
||||
local shown = {}
|
||||
for index = 1, math.min(#(users or {}), ADDependency.MAX_USERS) do shown[index] = tostring(users[index]) end
|
||||
if #shown == 0 then return "?" end
|
||||
local text = table.concat(shown, ", ")
|
||||
if #users > #shown then text = text .. " +" .. tostring(#users - #shown) end
|
||||
return text
|
||||
end
|
||||
|
||||
-- Beskedtekst (spu_dep_warn med dage, spu_dep_warnNoDays uden).
|
||||
function ADDependency.notifyText(warning, textOf, titleOf)
|
||||
local title = tostring(titleOf ~= nil and titleOf(warning.fillType) or warning.fillType)
|
||||
local users = ADDependency.usersText(warning.users)
|
||||
local building = tostring(warning.buildingName or warning.building or "?")
|
||||
local days = warning.days
|
||||
if days ~= nil and days < math.huge then
|
||||
return string.format(textOf("spu_dep_warn"), building, title, users, math.max(0, math.floor(days)))
|
||||
end
|
||||
return string.format(textOf("spu_dep_warnNoDays"), building, title, users)
|
||||
end
|
||||
@@ -1,48 +1,157 @@
|
||||
-- AD Field Fleet
|
||||
-- Markflåden som ren logik: hvilken rolle et sæt har ud fra redskabernes specs, og hvilket ledigt sæt
|
||||
-- der sendes til en mark. Kun sæt med flueben bruges; et sæt spilleren sidder i, røres aldrig.
|
||||
-- Markflåden som ren logik: hvilken rolle et sæt har ud fra redskabernes specs, og hvilke ledige sæt der
|
||||
-- sendes til en mark. Kun sæt med flueben bruges; et sæt spilleren sidder i, røres aldrig.
|
||||
|
||||
ADFieldFleet = {}
|
||||
|
||||
-- Rækkefølge = prioritet når et køretøj har flere (læssevogn med egen pickup m.m.)
|
||||
-- Rækkefølge = prioritet når et køretøj har flere (såmaskine med gødning er såmaskine; traktor med plov er
|
||||
-- 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"},
|
||||
{role = "mower", spec = "spec_mower"},
|
||||
{role = "seeder", spec = "spec_sowingMachine"},
|
||||
{role = "roller", spec = "spec_roller"},
|
||||
{role = "weeder", spec = "spec_weeder"},
|
||||
{role = "sprayRole"},
|
||||
{role = "mulcher", spec = "spec_mulcher"},
|
||||
-- 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"},
|
||||
}
|
||||
|
||||
function ADFieldFleet.roleFromSpecs(specs)
|
||||
for _, entry in ipairs(ADFieldFleet.ROLE_SPECS) do
|
||||
if specs[entry.spec] then return entry.role end
|
||||
if entry.role == "sprayRole" then
|
||||
if specs.sprayRole ~= nil then return specs.sprayRole end
|
||||
elseif specs[entry.spec] then
|
||||
return entry.role
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Nærmeste brugbare sæt til rollen. Grund når intet passer: noRig (ingen med flueben og rollen),
|
||||
-- allBusy (alle optaget/i brug af spilleren), tooWide/tooNarrow (bredden, med tal fra den første).
|
||||
function ADFieldFleet.pick(candidates, role, x, z, mowWidth)
|
||||
local best, bestDistance = nil, math.huge
|
||||
local sawRole, widthReason, widthArgs = false, nil, nil
|
||||
-- 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 not candidate.busy and not candidate.controlled then
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, mowWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
local distance = dx * dx + dz * dz
|
||||
if distance < bestDistance then best, bestDistance = candidate, distance end
|
||||
elseif widthReason == nil then
|
||||
widthReason, widthArgs = reason, args
|
||||
if accept == nil or accept(candidate) then
|
||||
sawAccepted = true
|
||||
if not candidate.busy and not candidate.controlled then
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
-- 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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if best ~= nil then return best end
|
||||
if not sawRole then return nil, "noRig" end
|
||||
if widthReason ~= nil then return nil, widthReason, widthArgs end
|
||||
return nil, "allBusy"
|
||||
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"
|
||||
elseif not sawAccepted then
|
||||
reason = "cannotDo"
|
||||
elseif widthReason ~= nil then
|
||||
reason, args = widthReason, widthArgs
|
||||
else
|
||||
reason = "allBusy"
|
||||
end
|
||||
return usable, reason, args
|
||||
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
|
||||
return nil, reason, args
|
||||
end
|
||||
|
||||
-- n sæt på én gang (multi-tool kræver alle baner): de n nærmeste, ellers ingen. notEnough har {ledige, n}.
|
||||
function ADFieldFleet.pickMany(candidates, role, n, x, z, requiredWidth, accept)
|
||||
local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
if #usable >= n then
|
||||
local picked = {}
|
||||
for index = 1, n do picked[index] = usable[index].candidate end
|
||||
return picked
|
||||
end
|
||||
if #usable > 0 then return nil, "notEnough", {#usable, n} end
|
||||
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)
|
||||
@@ -51,3 +160,27 @@ function ADFieldFleet.toggle(enabledIds, id)
|
||||
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,491 @@
|
||||
-- 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, chainFree = 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
|
||||
table.insert(rows, toggle("chainFree", L("spu_field_chainFree"), plan.chainFree))
|
||||
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)
|
||||
-- kilden ejes af setTarget; kladdens gamle værdi må ikke overskrive den
|
||||
plan.targetSource = nil
|
||||
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
|
||||
+419
-297
@@ -1,51 +1,234 @@
|
||||
-- AD Field Jobs
|
||||
-- Markarbejdets job-løkke (hvert sekund): opdater klar-andel, start marker, send sæt ud, følg dem
|
||||
-- gennem AutoDrive-kørsel -> Courseplay-arbejde -> hjem, og gå videre til næste trin. decide() er ren
|
||||
-- og testet; resten oversætter til AutoDrive/Courseplay og er pcall'et pr. mark og pr. sæt.
|
||||
-- Markarbejdets job-løkke (hvert sekund): opdater markerne, start kæder og pleje, beslut hvilke sæt der
|
||||
-- skal ud, og gå videre til næste trin. decide() er ren og testet; udsendelse og opfølgning af sættene
|
||||
-- (AutoDrive/Courseplay) ligger i adFieldWork. Alt spil-nært er pcall'et pr. mark og pr. sæt.
|
||||
|
||||
ADFieldJobs = {}
|
||||
|
||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADFieldJobs.TICK_MS = 1000
|
||||
ADFieldJobs.ARRIVE_DISTANCE = 30
|
||||
ADFieldJobs.IDLE_CONFIRM_MS = 5000
|
||||
ADFieldJobs.START_TIMEOUT_MS = 60000
|
||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige slå-ture går marken videre.
|
||||
ADFieldJobs.MAX_MOW_PASSES = 2
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
||||
ADFieldJobs.timer = 0
|
||||
-- é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
|
||||
-- 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 = {}
|
||||
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
|
||||
ADFieldJobs.timer = 500
|
||||
ADFieldJobs.now = 0
|
||||
ADFieldJobs.loaded = false
|
||||
|
||||
local ACTIVE_PHASES = {driving = true, starting = 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 ----
|
||||
|
||||
function ADFieldJobs.decide(field, info, assignments, now)
|
||||
-- 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.
|
||||
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care, reason}.
|
||||
function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
local hasMarker = info ~= nil and info.markerId ~= nil
|
||||
local raining = env ~= nil and env.raining == true
|
||||
local kind = field.kind or (info ~= nil and info.kind) or "mowable"
|
||||
if field.stage == "ready" then
|
||||
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
local first = ADFieldModel.workStages(field.plan, info.kind, info.needs)[1]
|
||||
if raining and first ~= nil and ADFieldModel.isWeatherSensitive(first, field.plan, info.kind) then
|
||||
return {action = "wait", reason = "rain"}
|
||||
end
|
||||
return {action = "start"}
|
||||
end
|
||||
if not ADFieldModel.isWorking(field) then return {action = "none"} end
|
||||
local active, finished = 0, 0
|
||||
-- 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
|
||||
if field.plan.mode ~= "auto" and not field.startRequested then return {action = "none", care = care} end
|
||||
if not hasMarker then return {action = "wait", care = care} end
|
||||
return {action = "care", stage = care}
|
||||
end
|
||||
local active, finished, helpers, awaiting = 0, 0, 0, 0
|
||||
for _, assignment in ipairs(assignments) do
|
||||
if ACTIVE_PHASES[assignment.phase] then active = active + 1 end
|
||||
if assignment.phase == "finished" or assignment.phase == "returning" then finished = finished + 1 end
|
||||
if assignment.stage == nil or assignment.stage == field.stage then
|
||||
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||
if assignment.helper then
|
||||
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
|
||||
end
|
||||
if active == 0 and finished > 0 then
|
||||
local stillStanding = field.stage == "mowing" and info ~= nil and (info.readyRatio or 0) >= ADFieldModel.CUT_RATIO
|
||||
if not stillStanding or finished >= ADFieldJobs.MAX_MOW_PASSES then return {action = "advance"} end
|
||||
local isCropStage = ADFieldModel.CROP_STAGES[field.stage] == true
|
||||
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
|
||||
return {action = "wait", reason = "rain"}
|
||||
end
|
||||
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
||||
end
|
||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||
-- 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
|
||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
||||
local role = ADFieldModel.wantsRig(field, active, now)
|
||||
if role ~= nil then return {action = "dispatch", role = role} end
|
||||
return {action = "none"}
|
||||
end
|
||||
|
||||
-- ---- tilstand ----
|
||||
|
||||
local function log(fieldId, fmt, ...)
|
||||
function ADFieldJobs.log(fieldId, fmt, ...)
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
|
||||
end
|
||||
|
||||
@@ -53,10 +236,15 @@ function ADFieldJobs.getField(id)
|
||||
return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
|
||||
end
|
||||
|
||||
local function setField(field)
|
||||
function ADFieldJobs.setField(field)
|
||||
ADFieldJobs.state.fields[field.id] = field
|
||||
end
|
||||
|
||||
-- Ændr felter på marken (kopi via modellen).
|
||||
function ADFieldJobs.note(fieldId, changes)
|
||||
ADFieldJobs.setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
||||
end
|
||||
|
||||
function ADFieldJobs.assignmentsFor(fieldId)
|
||||
local list = {}
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
@@ -74,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
|
||||
@@ -81,20 +273,64 @@ function ADFieldJobs.load()
|
||||
end
|
||||
|
||||
function ADFieldJobs.setPlan(id, changes)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {plan = changes}))
|
||||
ADFieldJobs.note(id, {plan = changes})
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.requestStart(id)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {startRequested = true}))
|
||||
-- "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
|
||||
|
||||
local function note(fieldId, changes)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
||||
-- 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
|
||||
|
||||
function ADFieldJobs.forceReady(id)
|
||||
setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
end
|
||||
|
||||
function ADFieldJobs.setTarget(id, fruitName, source)
|
||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName, source))
|
||||
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)
|
||||
@@ -102,308 +338,194 @@ function ADFieldJobs.toggleFleet(vehicleId)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- ---- AutoDrive ----
|
||||
-- GUI'et henter flåde og gæringssiloer herfra (ligger i adFieldWork).
|
||||
function ADFieldJobs.getRigs(adEnv) return ADFieldWork.getRigs(adEnv) end
|
||||
function ADFieldJobs.getFermenters(...) return ADFieldWork.getFermenters(...) end
|
||||
|
||||
local function stateModule(vehicle)
|
||||
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
end
|
||||
|
||||
local function isAdActive(vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
return module ~= nil and module:isActive()
|
||||
end
|
||||
|
||||
local function stopAd(vehicle)
|
||||
if isAdActive(vehicle) then vehicle:stopAutoDrive() end
|
||||
end
|
||||
|
||||
local function startAd(adEnv, vehicle, mode, firstId, secondId, withCp)
|
||||
local module = stateModule(vehicle)
|
||||
stopAd(vehicle)
|
||||
module:setMode(mode)
|
||||
module:setFirstMarkerByWayPointId(firstId)
|
||||
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
||||
module:setStartHelper(withCp == true)
|
||||
if withCp then
|
||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
||||
end
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
if module.setActualFarmId ~= nil then module:setActualFarmId(farmId) end
|
||||
module:raiseDirtyFlag()
|
||||
local current = module:getCurrentMode()
|
||||
if current ~= nil and current.start ~= nil then
|
||||
current:start(adEnv.AutoDrive.USER_PLAYER or 1)
|
||||
else
|
||||
vehicle:startAutoDrive()
|
||||
end
|
||||
end
|
||||
|
||||
local function parkMarkerId(adEnv, vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
||||
if index == nil or index < 1 then return nil end
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
local function restLoad(adEnv, vehicle)
|
||||
local ok, level = pcall(function()
|
||||
return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle)))
|
||||
end)
|
||||
return ok and tonumber(level) or 0
|
||||
end
|
||||
|
||||
local function vehiclePosition(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function isControlledByPlayer(vehicle)
|
||||
return (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or false
|
||||
end
|
||||
|
||||
-- ---- flåde ----
|
||||
|
||||
function ADFieldJobs.getRigs(adEnv)
|
||||
local rigs = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local role = ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle))
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if role ~= nil and id ~= nil then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
table.insert(rigs, {
|
||||
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
||||
enabled = ADFieldJobs.state.fleet[id] == true,
|
||||
busy = ADFieldJobs.state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil,
|
||||
controlled = isControlledByPlayer(vehicle),
|
||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||
})
|
||||
end
|
||||
end
|
||||
return rigs
|
||||
end
|
||||
|
||||
-- ---- silo ----
|
||||
|
||||
local function chooseStore(adEnv, field, info, vehicle)
|
||||
local fillType = field.plan.product == "hay" and info.hay or info.windrow
|
||||
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
|
||||
if #field.plan.storeBuildingIds > 0 then
|
||||
local markerIds = {}
|
||||
for _, buildingId in ipairs(field.plan.storeBuildingIds) do
|
||||
local building = ADBuildings.find(adEnv, vehicle:getOwnerFarmId(), buildingId)
|
||||
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers(building, "unload") or {}) do
|
||||
table.insert(markerIds, marker.id)
|
||||
end
|
||||
end
|
||||
return ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, markerIds, isReachable)
|
||||
end
|
||||
return ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, 1, info.markerId, isReachable, nil, nil, false)
|
||||
end
|
||||
|
||||
-- ---- udsendelse og opfølgning ----
|
||||
|
||||
-- Færdige sæt for en mark glemmes, når marken går videre eller stoppes. Listen er begrænset: en mark
|
||||
-- der sendes ud igen uden at gå videre (hjørner), går videre efter MAX_MOW_PASSES færdige ture.
|
||||
local function forgetFinished(fieldId)
|
||||
function ADFieldJobs.forgetFinished(fieldId)
|
||||
local remaining = {}
|
||||
for _, done in ipairs(ADFieldJobs.finished or {}) do
|
||||
for _, done in ipairs(ADFieldJobs.finished) do
|
||||
if done.fieldId ~= fieldId then table.insert(remaining, done) end
|
||||
end
|
||||
ADFieldJobs.finished = remaining
|
||||
end
|
||||
|
||||
local function release(assignment, phase)
|
||||
assignment.phase = phase or "released"
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
|
||||
local function dispatch(adEnv, field, info, role)
|
||||
local candidates = ADFieldJobs.getRigs(adEnv)
|
||||
local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, field.mowWidth)
|
||||
if rig == nil then
|
||||
note(field.id, {reason = {key = "spu_field_reason_" .. reason, role = role, args = args}})
|
||||
return
|
||||
end
|
||||
local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, phase = "driving", since = ADFieldJobs.now, width = rig.width}
|
||||
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil, false)
|
||||
log(field.id, "%s '%s' sendt til %s (%.1f m)", role, rig.label, tostring(info.markerName), rig.width)
|
||||
note(field.id, {reason = false})
|
||||
end
|
||||
|
||||
local function beginWork(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if assignment.role == "collector" then
|
||||
local store, why = chooseStore(adEnv, field, info, vehicle)
|
||||
if store == nil then
|
||||
release(assignment)
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(why)}})
|
||||
log(field.id, "intet lager: %s", tostring(why))
|
||||
return
|
||||
end
|
||||
assignment.storeId = store.id
|
||||
-- AD overtager ved "fuld": Hent og aflever F -> silo, hjælper = Courseplay
|
||||
local module = stateModule(vehicle)
|
||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||
module:setFirstMarkerByWayPointId(info.markerId)
|
||||
module:setSecondMarkerByWayPointId(store.id)
|
||||
module:setStartHelper(true)
|
||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
||||
end
|
||||
module:raiseDirtyFlag()
|
||||
log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||
end
|
||||
local point = info.points[1] or {x = info.x, z = info.z}
|
||||
assignment.phase = "starting"
|
||||
assignment.since = ADFieldJobs.now
|
||||
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
||||
if ok then
|
||||
assignment.phase = "working"
|
||||
assignment.since = ADFieldJobs.now
|
||||
if assignment.role == "mower" then
|
||||
note(field.id, {mowWidth = assignment.width})
|
||||
end
|
||||
log(field.id, "Courseplay startet (%s)", assignment.role)
|
||||
else
|
||||
release(assignment)
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(reason)}})
|
||||
log(field.id, "Courseplay fejlede: %s", tostring(reason))
|
||||
end
|
||||
end)
|
||||
if handle == nil then
|
||||
release(assignment)
|
||||
log(field.id, "Courseplay kunne ikke startes: %s", tostring(err))
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(err)}})
|
||||
return
|
||||
end
|
||||
assignment.handle = handle
|
||||
end
|
||||
|
||||
local function follow(adEnv, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if isControlledByPlayer(vehicle) then
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
||||
return release(assignment)
|
||||
end
|
||||
local field = ADFieldJobs.getField(assignment.fieldId)
|
||||
local info = ADFields.getInfo(assignment.fieldId)
|
||||
if assignment.phase == "driving" then
|
||||
if not isAdActive(vehicle) and ADFieldJobs.now - assignment.since > 2000 then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
||||
if distance <= ADFieldJobs.ARRIVE_DISTANCE then
|
||||
beginWork(adEnv, field, info, assignment)
|
||||
else
|
||||
log(assignment.fieldId, "'%s' nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info.markerName), distance)
|
||||
release(assignment)
|
||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS,
|
||||
reason = {text = string.format("%s nåede ikke %s", ADRuns.vehicleLabel(vehicle), tostring(info.markerName))}})
|
||||
end
|
||||
end
|
||||
elseif assignment.phase == "starting" then
|
||||
if ADFieldJobs.now - assignment.since > ADFieldJobs.START_TIMEOUT_MS then
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
release(assignment)
|
||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = "Courseplay svarede ikke (60 s)"}})
|
||||
log(assignment.fieldId, "Courseplay svarede ikke inden 60 s")
|
||||
end
|
||||
elseif assignment.phase == "working" then
|
||||
local busy = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle)
|
||||
if busy then
|
||||
assignment.idleSince = nil
|
||||
else
|
||||
assignment.idleSince = assignment.idleSince or ADFieldJobs.now
|
||||
local rest = assignment.role == "collector" and assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
|
||||
if rest > 0 and ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
||||
-- CP færdig med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til silo
|
||||
assignment.deliveredRest = true
|
||||
assignment.idleSince = nil
|
||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil, false)
|
||||
log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til silo", ADRuns.vehicleLabel(vehicle), rest)
|
||||
elseif ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||
local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil, false) end
|
||||
assignment.phase = "finished"
|
||||
ADFieldJobs.finished = ADFieldJobs.finished or {}
|
||||
table.insert(ADFieldJobs.finished, assignment)
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldJobs.stopField(adEnv, id)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(id)) do
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
pcall(stopAd, assignment.vehicle)
|
||||
pcall(function() if assignment.vehicle.stopCurrentAIJob ~= nil and ADCpBridge.isWorking(assignment.vehicle) then assignment.vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new()) end end)
|
||||
release(assignment)
|
||||
end
|
||||
forgetFinished(id)
|
||||
setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.save()
|
||||
log(id, "stoppet fra menuen")
|
||||
end
|
||||
|
||||
local function finishedFor(fieldId)
|
||||
local list = {}
|
||||
for _, assignment in ipairs(ADFieldJobs.finished or {}) do
|
||||
for _, assignment in ipairs(ADFieldJobs.finished) do
|
||||
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
local function stepField(adEnv, info)
|
||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
||||
setField(field)
|
||||
function ADFieldJobs.stopField(adEnv, id)
|
||||
ADFieldWork.stopAll(adEnv, id)
|
||||
ADFieldJobs.forgetFinished(id)
|
||||
ADFieldJobs.setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.save()
|
||||
ADFieldJobs.log(id, "stoppet fra menuen")
|
||||
end
|
||||
|
||||
-- Besked i spillet (højre side). critical = rød. Samme tekst vises kun én gang i træk.
|
||||
function ADFieldJobs.notify(text, critical)
|
||||
if ADFieldJobs.lastNotice == text then return end
|
||||
ADFieldJobs.lastNotice = text
|
||||
pcall(function()
|
||||
local kind = critical and FSBaseMission.INGAME_NOTIFICATION_CRITICAL or FSBaseMission.INGAME_NOTIFICATION_OK
|
||||
g_currentMission:addIngameNotification(kind, "Smart Pickup: " .. text)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Vejret lige nu (regn), læst en gang pr. tick.
|
||||
function ADFieldJobs.environment()
|
||||
local ok, raining = pcall(function()
|
||||
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,
|
||||
-- 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 = {}
|
||||
|
||||
-- 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)
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
|
||||
ADFieldJobs.lastDecision[info.id] = decision
|
||||
if decision.action == "start" then
|
||||
setField(ADFieldModel.start(field))
|
||||
log(info.id, "starter (%s)", field.plan.product)
|
||||
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
|
||||
local nextField = ADFieldModel.advance(field)
|
||||
setField(nextField)
|
||||
forgetFinished(info.id)
|
||||
log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||
-- 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)
|
||||
if nextField.stage == "done" or (ADFieldModel.isCare(field.stage) and nextField.stage == "idle") then
|
||||
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 ~= 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
|
||||
dispatch(adEnv, field, info, decision.role)
|
||||
-- é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(follow, adEnv, assignment)
|
||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||
if not ok then
|
||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
|
||||
release(assignment)
|
||||
ADFieldWork.release(assignment)
|
||||
end
|
||||
end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
if info.mowable or ADFieldModel.isWorking(ADFieldJobs.getField(info.id)) then
|
||||
local ok, err = pcall(stepField, adEnv, info)
|
||||
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
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
-- AD Field Model
|
||||
-- Markarbejdets regler som ren data (ingen spil-API): en marks plan og trin, hvornår den er klar,
|
||||
-- hvilket trin der følger, hvor mange sæt et trin må have, og bredde-reglerne. Alle funktioner
|
||||
-- returnerer nye tabeller; job-løkken (adFieldJobs) og GUI'et kalder kun herind.
|
||||
-- hvilket trin der følger, hvor mange sæt et trin må have, pleje-behov og bredde-reglerne. Alle
|
||||
-- funktioner returnerer nye tabeller; job-løkken (adFieldJobs/adFieldWork) og GUI'et kalder kun herind.
|
||||
--
|
||||
-- Markens art (kind): "mowable" (græs/kløver/lucerne), "harvest" (alt andet der høstes), "bare" (ingen
|
||||
-- afgrøde: stub, pløjet, harvet). Kæder:
|
||||
-- slåafgrøde: mowing -> [tedding] -> [raking] -> collecting
|
||||
-- høst: harvesting -> klargøring
|
||||
-- klargøring: [mulching] -> [liming] -> [plowing|cultivating] -> [sowing] -> [rolling]
|
||||
-- Pleje under vækst (enkelt trin, derefter tilbage til idle): fertilizing, weeding, liming.
|
||||
|
||||
ADFieldModel = {}
|
||||
|
||||
@@ -10,10 +17,40 @@ ADFieldModel.CUT_RATIO = 0.5
|
||||
ADFieldModel.CP_MAX_WIDTH = 50
|
||||
ADFieldModel.WIDTH_TOLERANCE = 0.05
|
||||
ADFieldModel.RETRY_MS = 300000
|
||||
ADFieldModel.MAX_COLLECTORS = 2
|
||||
ADFieldModel.CP_MAX_TOOL_OFFSET = 10
|
||||
ADFieldModel.MAX_RIGS = 5
|
||||
ADFieldModel.MAX_UNLOADERS = 3
|
||||
ADFieldModel.WEED_MIN = 2 -- weedState 1 = spredt, 2+ = tæt (AI-Bonde/Arbejdsseddel)
|
||||
ADFieldModel.WEED_MECHANICAL_MAX = 4 -- større ukrudt kræver sprøjte, strigle kan ikke
|
||||
ADFieldModel.MAX_CARE_RUNS = 2 -- samme pleje højst to gange pr. vækstcyklus (undgår løkker)
|
||||
|
||||
ADFieldModel.ROLES = {mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector"}
|
||||
local KNOWN_STAGES = {idle = true, ready = true, mowing = true, tedding = true, raking = true, collecting = true, done = true}
|
||||
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", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector",
|
||||
}
|
||||
ADFieldModel.HELPERS = {harvesting = "unloader"}
|
||||
ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true}
|
||||
-- trin med afgrøde der skal være væk bagefter (ellers sendes maskinen ud igen)
|
||||
ADFieldModel.CROP_STAGES = {mowing = true, harvesting = true}
|
||||
|
||||
local KNOWN_STAGES = {idle = true, ready = true, done = true}
|
||||
for stage in pairs(ADFieldModel.ROLES) do KNOWN_STAGES[stage] = true end
|
||||
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, targetFruit = "", prepAfterMow = false, straw = "chop",
|
||||
-- kæde: marken er frigivet til produktionskæden; hvem der valgte målafgrøden ("user"|"chain"|"")
|
||||
chainFree = false, targetSource = "",
|
||||
}
|
||||
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}
|
||||
|
||||
local function copyList(list)
|
||||
local copy = {}
|
||||
@@ -21,56 +58,203 @@ local function copyList(list)
|
||||
return copy
|
||||
end
|
||||
|
||||
local function copyPlan(plan)
|
||||
local copy = {}
|
||||
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
|
||||
|
||||
local function copyField(field)
|
||||
local copy = {}
|
||||
for key, value in pairs(field) do copy[key] = value end
|
||||
copy.plan = {
|
||||
mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake,
|
||||
storeBuildingIds = copyList(field.plan.storeBuildingIds),
|
||||
}
|
||||
copy.plan = copyPlan(field.plan)
|
||||
local care = {}
|
||||
for key, value in pairs(field.careRuns or {}) do care[key] = value end
|
||||
copy.careRuns = care
|
||||
return copy
|
||||
end
|
||||
|
||||
local function clamp(value, low, high)
|
||||
return math.max(low, math.min(high, math.floor(tonumber(value) or low)))
|
||||
end
|
||||
|
||||
function ADFieldModel.newField(id)
|
||||
return {
|
||||
id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false,
|
||||
plan = {mode = "manual", product = "silage", rake = false, storeBuildingIds = {}},
|
||||
careRuns = {}, plan = copyPlan(nil),
|
||||
}
|
||||
end
|
||||
|
||||
function ADFieldModel.workStages(plan)
|
||||
if plan.product == "hay" then
|
||||
if plan.rake then
|
||||
return {"mowing", "tedding", "raking", "collecting"}
|
||||
end
|
||||
return {"mowing", "tedding", "collecting"}
|
||||
-- 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
|
||||
|
||||
-- Klargøringskæden efter høst (eller på en bar mark). needs = {lime = bool, plow = bool} fra marken.
|
||||
function ADFieldModel.prepStages(plan, needs)
|
||||
needs = needs or {}
|
||||
local stages = {}
|
||||
if plan.mulch then table.insert(stages, "mulching") end
|
||||
if plan.lime and needs.lime then table.insert(stages, "liming") end
|
||||
local soil = plan.soil or "auto"
|
||||
if soil == "plow" or (soil == "auto" and needs.plow) then
|
||||
table.insert(stages, "plowing")
|
||||
elseif soil == "cultivate" or soil == "auto" then
|
||||
table.insert(stages, "cultivating")
|
||||
end
|
||||
return {"mowing", "collecting"}
|
||||
if plan.sow then
|
||||
table.insert(stages, "sowing")
|
||||
if plan.roll then table.insert(stages, "rolling") end
|
||||
end
|
||||
return stages
|
||||
end
|
||||
|
||||
-- Hele kæden for markens art. kind nil = slåafgrøde (bagudkompatibelt med etape 1).
|
||||
function ADFieldModel.workStages(plan, kind, needs)
|
||||
kind = kind or "mowable"
|
||||
if kind == "bare" then
|
||||
return ADFieldModel.prepStages(plan, needs)
|
||||
end
|
||||
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
|
||||
local stages = {"mowing"}
|
||||
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
|
||||
|
||||
-- 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, source)
|
||||
local copy = copyField(field)
|
||||
copy.plan.targetFruit = fruitName or ""
|
||||
-- kilden huskes kun mens der er et mål; standard er at spilleren valgte det
|
||||
copy.plan.targetSource = copy.plan.targetFruit ~= "" and (source or "user") 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 and field.stage ~= "strawCollecting" then
|
||||
return field.mowWidth
|
||||
end
|
||||
return nil
|
||||
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
|
||||
|
||||
function ADFieldModel.roleFor(stage)
|
||||
if stage == "careLiming" then return "limer" end
|
||||
return ADFieldModel.ROLES[stage]
|
||||
end
|
||||
|
||||
function ADFieldModel.helperFor(stage)
|
||||
return ADFieldModel.HELPERS[stage]
|
||||
end
|
||||
|
||||
function ADFieldModel.isWorking(field)
|
||||
return ADFieldModel.ROLES[field.stage] ~= nil
|
||||
return ADFieldModel.roleFor(field.stage) ~= nil
|
||||
end
|
||||
|
||||
function ADFieldModel.maxRigs(stage)
|
||||
return stage == "collecting" and ADFieldModel.MAX_COLLECTORS or 1
|
||||
function ADFieldModel.isCare(stage)
|
||||
return ADFieldModel.CARE_STAGES[stage] == true
|
||||
end
|
||||
|
||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis marken blev slået uden os.
|
||||
-- Arbejdende marker styres af job-løkken, ikke af vækst.
|
||||
-- Antal sæt pr. trin (multi-tool 1–5) og aflæsningsvogne under høst (1–3).
|
||||
function ADFieldModel.maxRigs(stage, plan)
|
||||
return clamp(plan ~= nil and plan.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||
end
|
||||
|
||||
function ADFieldModel.maxHelpers(stage, plan)
|
||||
if ADFieldModel.helperFor(stage) == nil then return 0 end
|
||||
return clamp(plan ~= nil and plan.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||
end
|
||||
|
||||
-- Courseplays multi-tool-baner (laneOffset) for n sæt: 2 -> -1,1 · 3 -> -1,0,1 · 4 -> -2,-1,1,2 · 5 -> -2..2
|
||||
function ADFieldModel.lanePositions(n)
|
||||
local lanes = {[1] = {0}, [2] = {-1, 1}, [3] = {-1, 0, 1}, [4] = {-2, -1, 1, 2}, [5] = {-2, -1, 0, 1, 2}}
|
||||
return copyList(lanes[clamp(n, 1, 5)])
|
||||
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"
|
||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO then
|
||||
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)
|
||||
@@ -85,58 +269,156 @@ function ADFieldModel.forceReady(field)
|
||||
return field
|
||||
end
|
||||
|
||||
function ADFieldModel.start(field)
|
||||
local copy = copyField(field)
|
||||
copy.stage = ADFieldModel.workStages(field.plan)[1]
|
||||
local function freshCycle(copy)
|
||||
copy.startRequested = false
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
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.
|
||||
-- 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)
|
||||
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)
|
||||
return copy
|
||||
end
|
||||
|
||||
function ADFieldModel.advance(field)
|
||||
-- Næste trin i kæden (behov genberegnes, så fx kalk springes over hvis jorden allerede er kalket).
|
||||
-- Pleje-trin går tilbage til idle.
|
||||
function ADFieldModel.advance(field, needs)
|
||||
local copy = copyField(field)
|
||||
local stages = ADFieldModel.workStages(field.plan)
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
if ADFieldModel.isCare(field.stage) then
|
||||
copy.stage = "idle"
|
||||
return copy
|
||||
end
|
||||
local stages = ADFieldModel.workStages(field.plan, field.kind, needs)
|
||||
copy.stage = "done"
|
||||
for index, stage in ipairs(stages) do
|
||||
if stage == field.stage and stages[index + 1] ~= nil then
|
||||
copy.stage = stages[index + 1]
|
||||
local passed = false
|
||||
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
|
||||
if not passed and (rank[stage] or 0) > (rank[field.stage] or 0) then
|
||||
copy.stage = stage
|
||||
passed = true
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Stop fra menuen: arbejdet afbrydes; marken står klar (hovedkæde) eller venter (pleje/klargøring).
|
||||
function ADFieldModel.stop(field)
|
||||
local copy = copyField(field)
|
||||
if ADFieldModel.CROP_STAGES[field.stage] or field.stage == "tedding" or field.stage == "raking" or field.stage == "collecting" then
|
||||
copy.stage = "ready"
|
||||
elseif ADFieldModel.isWorking(field) then
|
||||
copy.stage = "idle"
|
||||
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, 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"
|
||||
end
|
||||
|
||||
-- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields:
|
||||
-- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus.
|
||||
function ADFieldModel.careNeed(field, info)
|
||||
if not (field.stage == "idle" or field.stage == "done") or info == nil or not info.growing then
|
||||
return nil
|
||||
end
|
||||
local runs = field.careRuns or {}
|
||||
local function allowed(stage) return (runs[stage] or 0) < ADFieldModel.MAX_CARE_RUNS end
|
||||
if field.plan.weed and info.kind == "harvest" and (info.weed or 0) >= ADFieldModel.WEED_MIN and allowed("weeding") then
|
||||
return "weeding"
|
||||
end
|
||||
if field.plan.fertilize and info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax and allowed("fertilizing") then
|
||||
return "fertilizing"
|
||||
end
|
||||
if field.plan.lime and info.lime ~= nil and info.lime < 1 and allowed("careLiming") then
|
||||
return "careLiming"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Start et pleje-trin (tæller kørslen, så samme pleje ikke gentages i det uendelige).
|
||||
function ADFieldModel.startCare(field, stage)
|
||||
local copy = copyField(field)
|
||||
copy.careRuns[stage] = (copy.careRuns[stage] or 0) + 1
|
||||
copy.stage = stage
|
||||
copy.startRequested = false
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Stop fra menuen: arbejdet afbrydes; marken står klar igen (manuel start kræves).
|
||||
function ADFieldModel.stop(field)
|
||||
local copy = copyField(field)
|
||||
if ADFieldModel.isWorking(field) then
|
||||
copy.stage = "ready"
|
||||
end
|
||||
copy.startRequested = false
|
||||
copy.reason = nil
|
||||
return copy
|
||||
-- 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}
|
||||
|
||||
-- Bredden kurset laves med: højst CP's maksimum.
|
||||
function ADFieldModel.effectiveWidth(width)
|
||||
if width == nil then return nil end
|
||||
return math.min(width, ADFieldModel.CP_MAX_WIDTH)
|
||||
end
|
||||
|
||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst slåbredden.
|
||||
function ADFieldModel.checkWidth(role, width, mowWidth)
|
||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||
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 role ~= "mower" and mowWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < mowWidth then
|
||||
return false, "tooNarrow", {width, mowWidth}
|
||||
-- bredere end CP's 50 m (Unrealistic kan sættes til 120 m): bruges; kurset begrænses til 50 m (brugerønske 30/9)
|
||||
width = ADFieldModel.effectiveWidth(width)
|
||||
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
|
||||
return false, "tooNarrow", {width, requiredWidth}
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldModel.wantsRig(field, assignedCount, now)
|
||||
-- Hvilken rolle trinet mangler: {role, count, helper}. Hovedrollen sendes samlet (multi-tool kræver alle
|
||||
-- baner), hjælpere (aflæsningsvogne) en ad gangen. counts = {primary = aktive, helpers = aktive}.
|
||||
function ADFieldModel.wantsRig(field, counts, now)
|
||||
if type(counts) == "number" then counts = {primary = counts, helpers = 0} end
|
||||
local role = ADFieldModel.roleFor(field.stage)
|
||||
if role == nil or (field.retryAt or 0) > now or assignedCount >= ADFieldModel.maxRigs(field.stage) then
|
||||
return nil
|
||||
if role == nil or (field.retryAt or 0) > now then return nil end
|
||||
local wanted = ADFieldModel.maxRigs(field.stage, field.plan)
|
||||
if (counts.primary or 0) < wanted then
|
||||
return {role = role, count = wanted - (counts.primary or 0), helper = false}
|
||||
end
|
||||
return role
|
||||
local helper = ADFieldModel.helperFor(field.stage)
|
||||
if helper ~= nil and (counts.helpers or 0) < ADFieldModel.maxHelpers(field.stage, field.plan) then
|
||||
return {role = helper, count = 1, helper = true}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvilken afgrøde der skal sås: planens valg, ellers den afgrøde der blev høstet.
|
||||
function ADFieldModel.sowFruit(field)
|
||||
if field.plan.sowFruit ~= nil and field.plan.sowFruit ~= "" then return field.plan.sowFruit end
|
||||
return field.lastFruit
|
||||
end
|
||||
|
||||
function ADFieldModel.with(field, changes)
|
||||
@@ -144,6 +426,8 @@ function ADFieldModel.with(field, changes)
|
||||
for key, value in pairs(changes) do
|
||||
if key == "plan" then
|
||||
for planKey, planValue in pairs(value) do copy.plan[planKey] = planValue end
|
||||
elseif value == false and (key == "reason") then
|
||||
copy[key] = nil
|
||||
else
|
||||
copy[key] = value
|
||||
end
|
||||
@@ -151,21 +435,95 @@ function ADFieldModel.with(field, changes)
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Læssevognens Courseplay-forskydning når den kører rivens kursus (se etape 1-spec).
|
||||
function ADFieldModel.collectorOffset(ownOffset, windrowOffset)
|
||||
local offset = (ownOffset or 0) - (windrowOffset or 0)
|
||||
return math.max(-ADFieldModel.CP_MAX_TOOL_OFFSET, math.min(ADFieldModel.CP_MAX_TOOL_OFFSET, offset))
|
||||
end
|
||||
|
||||
-- Gæringssilo: en produktion der har markens skår som input og ensilage som output.
|
||||
function ADFieldModel.isFermenter(productions, windrowFillType, silageFillType)
|
||||
if silageFillType == nil then return false end
|
||||
for _, production in ipairs(productions or {}) do
|
||||
local takesWindrow, makesSilage = false, false
|
||||
for _, input in ipairs(production.inputs or {}) do
|
||||
if input.type == windrowFillType then takesWindrow = true end
|
||||
end
|
||||
for _, output in ipairs(production.outputs or {}) do
|
||||
if output.type == silageFillType then makesSilage = true end
|
||||
end
|
||||
if takesWindrow and makesSilage then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function flag(value) return value and 1 or 0 end
|
||||
|
||||
function ADFieldModel.toRecord(field)
|
||||
local plan = field.plan
|
||||
local care = {}
|
||||
for stage, runs in pairs(field.careRuns or {}) do table.insert(care, stage .. "=" .. tostring(runs)) end
|
||||
table.sort(care)
|
||||
return {
|
||||
id = field.id, stage = field.stage, mode = field.plan.mode, product = field.plan.product,
|
||||
rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "),
|
||||
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",
|
||||
chainFree = flag(plan.chainFree), targetSource = plan.targetSource or "",
|
||||
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
|
||||
|
||||
local function readFlag(value, default)
|
||||
if value == nil then return default end
|
||||
return value == 1 or value == true
|
||||
end
|
||||
|
||||
local function readText(value)
|
||||
return value ~= nil and value ~= "" and value or nil
|
||||
end
|
||||
|
||||
function ADFieldModel.fromRecord(record)
|
||||
local field = ADFieldModel.newField(record.id)
|
||||
local plan = field.plan
|
||||
field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle"
|
||||
field.plan.mode = record.mode == "auto" and "auto" or "manual"
|
||||
field.plan.product = record.product == "hay" and "hay" or "silage"
|
||||
field.plan.rake = record.rake == 1 or record.rake == true
|
||||
for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end
|
||||
plan.mode = record.mode == "auto" and "auto" or "manual"
|
||||
-- etape 1 gemte "silage"/"hay"; ukendt -> auto
|
||||
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)
|
||||
plan.lime = readFlag(record.lime, true)
|
||||
plan.soil = SOIL_VALUES[record.soil] and record.soil or "auto"
|
||||
plan.sow = readFlag(record.sow, false)
|
||||
plan.sowFruit = record.sowFruit or ""
|
||||
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.chainFree = readFlag(record.chainFree, false)
|
||||
-- gamle saves: et eksisterende mål er valgt af spilleren
|
||||
plan.targetSource = record.targetSource or (plan.targetFruit ~= "" and "user" or "")
|
||||
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
|
||||
@@ -1,17 +1,63 @@
|
||||
-- AD Field Storage
|
||||
-- Markernes trin/plan og markflåden gemmes i savegame-mappen ved siden af runs.
|
||||
-- Markernes trin/plan og markflåden gemmes i savegame-mappen ved siden af runs. Hvert felt modellen gemmer
|
||||
-- (ADFieldModel.toRecord) står i FIELD_TYPES med sin XML-type — en test sikrer at intet felt glemmes.
|
||||
|
||||
ADFieldStorage = {}
|
||||
|
||||
ADFieldStorage.FILE_NAME = "ADSmartPickup_fields.xml"
|
||||
ADFieldStorage.ROOT = "ADSmartPickupFields"
|
||||
|
||||
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", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
||||
chainFree = "Int", targetSource = "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()
|
||||
local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if runsPath == nil then return nil end
|
||||
return (string.gsub(runsPath, ADRunsStorage.FILE_NAME .. "$", ADFieldStorage.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADFieldStorage.writeRecord(xmlFile, key, record)
|
||||
for name, kind in pairs(ADFieldStorage.FIELD_TYPES) do
|
||||
local value = record[name]
|
||||
if value ~= nil then xmlFile["set" .. kind](xmlFile, key .. "#" .. name, value) end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldStorage.readRecord(xmlFile, key)
|
||||
local record = {}
|
||||
for name, kind in pairs(ADFieldStorage.FIELD_TYPES) do
|
||||
record[name] = xmlFile["get" .. kind](xmlFile, key .. "#" .. name)
|
||||
end
|
||||
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
|
||||
@@ -19,46 +65,55 @@ function ADFieldStorage.save(state)
|
||||
if xmlFile == nil then return false end
|
||||
local index = 0
|
||||
for _, field in pairs(state.fields) do
|
||||
local record = ADFieldModel.toRecord(field)
|
||||
local key = string.format("%s.field(%d)", ADFieldStorage.ROOT, index)
|
||||
xmlFile:setInt(key .. "#id", record.id)
|
||||
xmlFile:setString(key .. "#stage", record.stage)
|
||||
xmlFile:setString(key .. "#mode", record.mode)
|
||||
xmlFile:setString(key .. "#product", record.product)
|
||||
xmlFile:setInt(key .. "#rake", record.rake)
|
||||
xmlFile:setString(key .. "#store", record.store)
|
||||
xmlFile:setFloat(key .. "#mowWidth", record.mowWidth)
|
||||
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({
|
||||
id = xmlFile:getInt(key .. "#id"), stage = xmlFile:getString(key .. "#stage"),
|
||||
mode = xmlFile:getString(key .. "#mode"), product = xmlFile:getString(key .. "#product"),
|
||||
rake = xmlFile:getInt(key .. "#rake"), store = xmlFile:getString(key .. "#store"),
|
||||
mowWidth = xmlFile:getFloat(key .. "#mowWidth"),
|
||||
})
|
||||
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
||||
if field.id ~= nil then result.fields[field.id] = field end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".fleet.vehicle", function(_, key)
|
||||
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
|
||||
|
||||
@@ -1,32 +1,111 @@
|
||||
-- AD Field View
|
||||
-- Rene regler for fanen Marker: hvilke marker vises (slåafgrøder som standard), rækkefølge og
|
||||
-- statusord. GUI'et oversætter nøglerne.
|
||||
-- Rene regler for fanen Marker: rækkefølge, statusord, planens tekst og jordtilstand. GUI'et oversætter
|
||||
-- nøglerne.
|
||||
|
||||
ADFieldView = {}
|
||||
|
||||
function ADFieldView.rows(infos, fieldsById, showAll)
|
||||
-- Alle gårdens marker (slåafgrøder, høstafgrøder og bare), sorteret efter marknummer.
|
||||
function ADFieldView.rows(infos, fieldsById)
|
||||
local rows = {}
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if showAll or info.mowable then
|
||||
table.insert(rows, {info = info, field = fieldsById[info.id] or ADFieldModel.newField(info.id)})
|
||||
end
|
||||
table.insert(rows, {info = info, field = fieldsById[info.id] or ADFieldModel.newField(info.id)})
|
||||
end
|
||||
table.sort(rows, function(a, b) return a.info.id < b.info.id end)
|
||||
return rows
|
||||
end
|
||||
|
||||
function ADFieldView.hiddenCount(infos, showAll)
|
||||
if showAll then return 0 end
|
||||
local hidden = 0
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if not info.mowable then hidden = hidden + 1 end
|
||||
end
|
||||
return hidden
|
||||
end
|
||||
|
||||
function ADFieldView.statusKey(field, info)
|
||||
if info.markerId == nil then
|
||||
return "spu_field_noMarker"
|
||||
end
|
||||
return "spu_field_stage_" .. tostring(field.stage)
|
||||
end
|
||||
|
||||
-- Planens tekstnøgle ud fra markens art: ensilage/hø for slåafgrøder, høst, eller klargøring (bar mark).
|
||||
function ADFieldView.planKey(plan, kind)
|
||||
if kind == "bare" then return "spu_field_plan_prep" end
|
||||
return "spu_field_plan_" .. ADFieldModel.product(plan, kind)
|
||||
end
|
||||
|
||||
-- Hvad jorden trænger til, som korte nøgler i fast rækkefølge: kalk, pløj, gødning, ukrudt.
|
||||
function ADFieldView.soilParts(info)
|
||||
local parts = {}
|
||||
local needs = info.needs or {}
|
||||
if needs.lime then table.insert(parts, "lime") end
|
||||
if needs.plow then table.insert(parts, "plow") end
|
||||
if info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax then table.insert(parts, "fert") end
|
||||
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
+542
-78
@@ -10,7 +10,15 @@ ADFields.MARKER_MAX_DISTANCE = 400
|
||||
ADFields.cache = {}
|
||||
ADFields.order = {}
|
||||
ADFields.scanIndex = 0
|
||||
-- {[markId] = true}: marker hvis scanning er afsluttet (med resultat) eller fejlede — til isScanComplete
|
||||
ADFields.scanFailed = {}
|
||||
ADFields.ownedDone = false
|
||||
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
|
||||
@@ -33,6 +41,80 @@ function ADFields.buildMowable(mowerConverter, tedderConverter, fillTypeName)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Markens art: slåafgrøde, høstafgrøde eller bar (ingen afgrøde). Vokser = afgrøde der ikke er klar endnu.
|
||||
function ADFields.classify(fruitIndex, isMowable, readyRatio)
|
||||
if fruitIndex == nil then return "bare", false end
|
||||
local kind = isMowable and "mowable" or "harvest"
|
||||
return kind, (readyRatio or 0) < 0.9
|
||||
end
|
||||
|
||||
-- Vækststadie -> fase ud fra afgrødens grænser (FS25-felter hedder minHarvestingGrowthState eller
|
||||
-- minHarvestGrowthState afhængigt af version).
|
||||
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
|
||||
local maxReady = fruit.maxHarvestingGrowthState or fruit.maxHarvestGrowthState or minReady
|
||||
if minReady ~= nil and state >= minReady and state <= (maxReady or minReady) then return "ready" end
|
||||
if minReady ~= nil and state < minReady then return "growing" end
|
||||
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
|
||||
return (anyPixels or 0) > 0 and (readyRatio or 0) < 0.9
|
||||
end
|
||||
|
||||
-- Jordens behov (kalk/pløjning) ud fra gennemsnitsniveauer og spillets indstillinger (kalk/pløjning kan
|
||||
-- være slået fra i savegamet). Ukendt niveau = intet behov.
|
||||
function ADFields.soilNeeds(levels, settings)
|
||||
return {
|
||||
lime = settings.limeRequired and levels.lime ~= nil and levels.lime < 1 or false,
|
||||
plow = settings.plowingRequired and levels.plowLevel ~= nil and levels.plowLevel < 1 or false,
|
||||
}
|
||||
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()
|
||||
@@ -47,6 +129,55 @@ function ADFields.getMowable()
|
||||
return ADFields.mowable
|
||||
end
|
||||
|
||||
-- ---- density-maps (samme læsning som AI-Bonden/Arbejdsseddel, bevist i spillet) ----
|
||||
ADFields.densityCache = {}
|
||||
local function readDensity(channel, x, z)
|
||||
if channel == nil or g_currentMission == nil or g_currentMission.fieldGroundSystem == nil then return nil end
|
||||
local cached = ADFields.densityCache[channel]
|
||||
if cached == nil then
|
||||
local ok, mapId, first, num = pcall(function() return g_currentMission.fieldGroundSystem:getDensityMapData(channel) end)
|
||||
if not ok or mapId == nil then ADFields.densityCache[channel] = false; return nil end
|
||||
cached = {modifier = DensityMapModifier.new(mapId, first, num, g_currentMission.terrainRootNode)}
|
||||
ADFields.densityCache[channel] = cached
|
||||
elseif cached == false then
|
||||
return nil
|
||||
end
|
||||
local value
|
||||
pcall(function()
|
||||
cached.modifier:setParallelogramWorldCoords(x - 0.5, z - 0.5, 1, 0, 0, 1, DensityCoordType.POINT_VECTOR_VECTOR)
|
||||
local sum, area = cached.modifier:executeGet()
|
||||
if area ~= nil and area > 0 then value = sum / area end
|
||||
end)
|
||||
return value
|
||||
end
|
||||
|
||||
local function readWeed(x, z)
|
||||
local weedSystem = g_currentMission ~= nil and g_currentMission.weedSystem or nil
|
||||
if weedSystem == nil or weedSystem.densityMap == nil then return nil end
|
||||
if ADFields.weedModifier == nil then
|
||||
local ok, modifier = pcall(DensityMapModifier.new, weedSystem.densityMap, weedSystem.firstChannel or 0, weedSystem.numChannels or 4, g_currentMission.terrainRootNode)
|
||||
ADFields.weedModifier = ok and modifier or false
|
||||
end
|
||||
if ADFields.weedModifier == false then return nil end
|
||||
local value
|
||||
pcall(function()
|
||||
ADFields.weedModifier:setParallelogramWorldCoords(x - 0.5, z - 0.5, 1, 0, 0, 1, DensityCoordType.POINT_VECTOR_VECTOR)
|
||||
local sum, area = ADFields.weedModifier:executeGet()
|
||||
if area ~= nil and area > 0 then value = sum / area end
|
||||
end)
|
||||
return value
|
||||
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
|
||||
return {
|
||||
limeRequired = flag(info.limeRequired),
|
||||
plowingRequired = flag(info.plowingRequiredEnabled),
|
||||
weedsEnabled = flag(info.weedsEnabled),
|
||||
}
|
||||
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
|
||||
@@ -61,66 +192,152 @@ 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
|
||||
|
||||
-- Markens areal i hektar (shoelace på {x,z}-polygonen, /10000); 0 uden en flade (under 3 hjørner).
|
||||
function ADFields.polygonAreaHa(verts)
|
||||
if type(verts) ~= "table" or #verts < 3 then return 0 end
|
||||
local n, twiceArea = #verts, 0
|
||||
for index = 1, n do
|
||||
local a, b = verts[index], verts[index % n + 1]
|
||||
twiceArea = twiceArea + a.x * b.z - b.x * a.z
|
||||
end
|
||||
return math.abs(twiceArea) / 2 / 10000
|
||||
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
|
||||
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
|
||||
@@ -131,70 +348,317 @@ 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
|
||||
-- ---- 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
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||
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
|
||||
if mowable ~= nil then
|
||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||
info.readyRatio = readyRatio(info.fruitIndex, points)
|
||||
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
||||
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
|
||||
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
|
||||
ADFields.order = scan.result
|
||||
ADFields.ownedDone = true
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
-- Har scanningen nået alle ejede marker mindst én gang (resultat i cachen, eller scanningen fejlede)?
|
||||
-- Indtil da er marklisten ufuldstændig (efter indlæsning fyldes den ~én mark pr. SCAN_INTERVAL_MS).
|
||||
function ADFields.isScanComplete()
|
||||
if not ADFields.ownedDone then return false end
|
||||
for _, entry in ipairs(ADFields.order) do
|
||||
if ADFields.cache[entry.id] == nil and not ADFields.scanFailed[entry.id] then return false end
|
||||
end
|
||||
return true
|
||||
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))
|
||||
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||
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
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
|
||||
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))
|
||||
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||
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,572 @@
|
||||
-- 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", "chain",
|
||||
}
|
||||
|
||||
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
|
||||
-- "Altid" (0) øverst: sælg uanset pris (in-game 30/9)
|
||||
local options = {{label = L("spu_sellAt_always"), value = 0}}
|
||||
for _, option in ipairs(percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5)) do table.insert(options, option) end
|
||||
table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), options, 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, draft.sellAtPercent == 0 and L("spu_guide_flow_sumSellAlways") or 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
|
||||
@@ -52,20 +52,61 @@ end
|
||||
|
||||
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
|
||||
-- {{kind, runs}, ...}; tomme grupper udelades.
|
||||
-- Spillerens egne flows først (forsyning, udkørsel), så kædernes egne flows (run.chain) samlet til sidst
|
||||
-- (in-game 30/9: "Kæde: …"-flows blandede sig alfabetisk ind mellem de flows spilleren selv har sat op).
|
||||
function ADFlowView.boardGroups(runs)
|
||||
local byKind = {supply = {}, outbound = {}}
|
||||
local chainRuns = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
table.insert(chainRuns, run)
|
||||
else
|
||||
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
||||
end
|
||||
end
|
||||
local groups = {}
|
||||
for _, kind in ipairs({"supply", "outbound"}) do
|
||||
if #byKind[kind] > 0 then
|
||||
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")})
|
||||
table.insert(groups, {kind = kind, chain = false, runs = sortedBy(byKind[kind], "name")})
|
||||
end
|
||||
end
|
||||
if #chainRuns > 0 then table.insert(groups, {kind = "supply", chain = true, runs = sortedBy(chainRuns, "name")}) end
|
||||
return groups
|
||||
end
|
||||
|
||||
-- Kort maskinnavn til lister: " Unrealistic" og parenteser som "(+ Custom Crops)" fjernes (lange navne blev ombrudt
|
||||
-- med traktorens nummer alene på næste linje, in-game 30/9). Bliver intet tilbage, bruges det fulde navn.
|
||||
function ADFlowView.shortVehicleName(name)
|
||||
local full = tostring(name or "")
|
||||
local short = full:gsub("%s*%b()", ""):gsub("%s+[Uu]nrealistic", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if short == "" then return full end
|
||||
return short
|
||||
end
|
||||
|
||||
-- Kædeflow uden traktor (venter på en ledig traktor/vogn).
|
||||
function ADFlowView.isIdleChain(run)
|
||||
return run.chain ~= nil and run.chain ~= "" and #(run.vehicleIds or {}) == 0
|
||||
end
|
||||
|
||||
-- Kædeflows uden traktor foldes sammen i listen (de venter på en ledig traktor/vogn). -> active, idle
|
||||
function ADFlowView.splitIdle(runs)
|
||||
local active, idle = {}, {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(#(run.vehicleIds or {}) > 0 and active or idle, run)
|
||||
end
|
||||
return active, idle
|
||||
end
|
||||
|
||||
-- Navn i listen: kædeflows står under overskriften "Kædernes egne flows", så "Kæde: " fjernes (ellers skjuler
|
||||
-- afkortningen hvilken bygning det gælder).
|
||||
function ADFlowView.listName(run)
|
||||
local name = tostring(run.name or "")
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
name = name:gsub("^Kæde:%s*", ""):gsub("^Chain:%s*", "")
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
|
||||
function ADFlowView.orderedRuns(runs)
|
||||
local result = {}
|
||||
@@ -101,18 +142,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 +188,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,761 @@
|
||||
-- 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
|
||||
|
||||
-- Kun læsning (statusvinduet): hvad flowets vogn-logik har traktoren i gang med -> "fetching"|"waiting"|"emptying"|"homing"|nil
|
||||
function ADFlowWagons.statusOf(vehicle)
|
||||
if fetching[vehicle] ~= nil then return "fetching" end
|
||||
if waiting[vehicle] ~= nil then return "waiting" end
|
||||
if emptying[vehicle] ~= nil then return "emptying" end
|
||||
if homing[vehicle] ~= nil then return "homing" end
|
||||
return 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
|
||||
@@ -0,0 +1,132 @@
|
||||
-- AD Load Diag
|
||||
-- Diagnose når AutoDrive står ved et LOAD-mål uden at fylde (in-game 30/9: Fendt + MEGA-sprøjte ved "Market Price
|
||||
-- Liquid Fill Station"). AutoDrive fylder kun, når en fyldenhed står i triggerens boks (trigger.fillableObjects) og
|
||||
-- triggeren har varen. Efter REPORT_MS ved målet uden fremgang skrives én linje: markør, vare, og for hver vogn den
|
||||
-- nærmeste AD-læssetrigger (afstand, har varen?, står vognen i boksen?). Kun når en trigger med varen er inden for
|
||||
-- NEAR_M (holder ved Wait-punkter logges ikke). Ingen indgreb — kun log.
|
||||
|
||||
ADLoadDiag = {}
|
||||
ADLoadDiag.LOG_PREFIX = "ADSmartPickup: pålæsning: "
|
||||
ADLoadDiag.REPORT_MS = 30000
|
||||
ADLoadDiag.NEAR_M = 20 -- m: trigger med varen herinden for = køretøjet står ved stationen (ellers Wait/mark: ingen log)
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- state (pr. opgave), dt, reached (ved målet), level (liter på vognene) -> true når der skal rapporteres (én gang pr.
|
||||
-- ankomst). Fremgang eller kørsel nulstiller.
|
||||
function ADLoadDiag.tick(state, dt, reached, level)
|
||||
if not reached then
|
||||
state.ms, state.reported, state.level = 0, false, nil
|
||||
return false
|
||||
end
|
||||
if state.level == nil or level > state.level then
|
||||
state.level, state.ms = level, 0
|
||||
return false
|
||||
end
|
||||
state.ms = (state.ms or 0) + (dt or 0)
|
||||
if state.reported or state.ms < ADLoadDiag.REPORT_MS then return false end
|
||||
state.reported = true
|
||||
return true
|
||||
end
|
||||
|
||||
-- distances = afstande fra vognene til nærmeste AD-trigger med varen -> står ved stationen?
|
||||
function ADLoadDiag.atStation(distances)
|
||||
for _, d in ipairs(distances or {}) do
|
||||
if d <= ADLoadDiag.NEAR_M then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
return g_fillTypeManager ~= nil and fillType ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function inBox(trigger, trailer)
|
||||
for key, entry in pairs(trigger.fillableObjects or {}) do
|
||||
if key == trailer or (type(entry) == "table" and entry.object == trailer) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function describeTrailer(adEnv, trailer, fillType)
|
||||
local node = trailer.components ~= nil and trailer.components[1] ~= nil and trailer.components[1].node or trailer.rootNode
|
||||
if node == nil then return nil end
|
||||
local tx, _, tz = getWorldTranslation(node)
|
||||
local nearest, nearestOk = nil, nil
|
||||
for _, trigger in pairs(adEnv.ADTriggerManager.getLoadTriggers() or {}) do
|
||||
local x, _, z = adEnv.ADTriggerManager.getTriggerPos(trigger)
|
||||
if x ~= nil and (x ~= 0 or z ~= 0) then
|
||||
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
|
||||
local ok = adEnv.ADTriggerManager.triggerSupportsFillType(trigger, fillType)
|
||||
if nearest == nil or d < nearest.d then nearest = {d = d, ok = ok, trigger = trigger} end
|
||||
if ok and (nearestOk == nil or d < nearestOk.d) then nearestOk = {d = d, trigger = trigger} end
|
||||
end
|
||||
end
|
||||
local name = trailer.getName ~= nil and trailer:getName() or "?"
|
||||
local parts = {}
|
||||
if nearest ~= nil then
|
||||
table.insert(parts, string.format("nærmeste AD-trigger %.1f m (%s varen, %s)", nearest.d, nearest.ok and "har" or "UDEN",
|
||||
inBox(nearest.trigger, trailer) and "vognen i boksen" or "vognen IKKE i boksen"))
|
||||
end
|
||||
if nearestOk ~= nil and (nearest == nil or nearestOk.trigger ~= nearest.trigger) then
|
||||
table.insert(parts, string.format("nærmeste med varen %.1f m (%s)", nearestOk.d,
|
||||
inBox(nearestOk.trigger, trailer) and "i boksen" or "ikke i boksen"))
|
||||
end
|
||||
if nearestOk == nil then table.insert(parts, "INGEN AD-læssetrigger med varen") end
|
||||
return string.format("'%s': %s", tostring(name), table.concat(parts, ", ")), nearestOk ~= nil and nearestOk.d or nil
|
||||
end
|
||||
|
||||
local function report(adEnv, task)
|
||||
local vehicle = task.vehicle
|
||||
local fillType = vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
|
||||
local marker = task.destinationID ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(task.destinationID) or nil
|
||||
local lines, distances = {}, {}
|
||||
for _, trailer in ipairs(task.trailers or {}) do
|
||||
if trailer.getFillUnits ~= nil and #trailer:getFillUnits() > 0 then
|
||||
local ok, line, distance = pcall(describeTrailer, adEnv, trailer, fillType)
|
||||
if ok and line ~= nil then table.insert(lines, line) end
|
||||
if ok and distance ~= nil then table.insert(distances, distance) end
|
||||
end
|
||||
end
|
||||
-- holder ved Wait/mark (ingen trigger med varen tæt på): det er ikke en fejlet pålæsning
|
||||
if not ADLoadDiag.atStation(distances) then return end
|
||||
local maxDistance = adEnv.AutoDrive.getMaxTriggerDistance ~= nil and adEnv.AutoDrive.getMaxTriggerDistance(vehicle) or nil
|
||||
Logging.warning(ADLoadDiag.LOG_PREFIX .. "'%s' står ved '%s' uden at fylde %s i %d s (AD-triggerafstand %s m): %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker ~= nil and marker.name or task.destinationID), fillTypeName(fillType),
|
||||
math.floor(ADLoadDiag.REPORT_MS / 1000), tostring(maxDistance), #lines > 0 and table.concat(lines, "; ") or "ingen vogne med fyldenheder")
|
||||
end
|
||||
|
||||
local states = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADLoadDiag.after(adEnv, task, dt)
|
||||
local vehicle = task.vehicle
|
||||
if vehicle == nil or vehicle.ad == nil or vehicle.ad.drivePathModule == nil then return end
|
||||
local state = states[task]
|
||||
if state == nil then
|
||||
state = {}
|
||||
states[task] = state
|
||||
end
|
||||
local reached = vehicle.ad.drivePathModule:isTargetReached() and not vehicle.ad.trailerModule:isActiveAtTrigger()
|
||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||
if ADLoadDiag.tick(state, dt, reached, level) then report(adEnv, task) end
|
||||
end
|
||||
|
||||
local isInstalled = false
|
||||
|
||||
function ADLoadDiag.install(adEnv)
|
||||
local task = adEnv ~= nil and adEnv.LoadAtDestinationTask or nil
|
||||
if isInstalled or task == nil or task.update == nil or adEnv.ADTriggerManager == nil then return end
|
||||
isInstalled = true
|
||||
local original = task.update
|
||||
task.update = function(self, dt)
|
||||
local result = original(self, dt)
|
||||
local ok, err = pcall(ADLoadDiag.after, adEnv, self, dt)
|
||||
if not ok and not self.spuDiagFailed then
|
||||
self.spuDiagFailed = true
|
||||
Logging.warning(ADLoadDiag.LOG_PREFIX .. "diagnose fejlede: %s", tostring(err))
|
||||
end
|
||||
return result
|
||||
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)
|
||||
@@ -630,6 +660,13 @@ end
|
||||
|
||||
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
||||
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
||||
-- Palleskur: moden tager ét objekt ud ad gangen og afslutter selv pålæsningen (fuld / tomt lager / 30 s uden
|
||||
-- udlevering). Imens holdes AD's egen pålæsning, ellers kører AD, så snart triggeren et øjeblik er tom (in-game 30/9:
|
||||
-- Sell Egg kørte halvfuld). Først når AD har aktiveret UAL (det gør AD selv ved ankomst).
|
||||
function ADOutbound.holdForSpawn(sourceKind, done, ualActive)
|
||||
return sourceKind == "objectStorage" and not done and ualActive == true
|
||||
end
|
||||
|
||||
local function updateSpawn(adEnv, state, task, source, dt)
|
||||
local vehicle = task.vehicle
|
||||
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
||||
@@ -646,7 +683,7 @@ local function updateSpawn(adEnv, state, task, source, dt)
|
||||
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
|
||||
spawn.pending = nil
|
||||
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType)))
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift: lager %d -> %d, vogn %d -> %d objekter); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType), spawn.pending.stored, stored, spawn.pending.loaded, loaded))
|
||||
state.done = true
|
||||
return
|
||||
else
|
||||
@@ -733,6 +770,7 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||||
task:finished()
|
||||
return true
|
||||
end
|
||||
if ADOutbound.holdForSpawn(source.kind, state.done, task.activatedUALLoading) then return true end
|
||||
end
|
||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||
if state.lastFill == nil or level > state.lastFill then
|
||||
@@ -753,7 +791,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
|
||||
|
||||
@@ -67,7 +67,10 @@ function ADOutboundPlanner.evaluate(product, input)
|
||||
storagePercent = storagePercent,
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
-- sælg fra 0 % = "Altid": uanset pris (også uden prisdata)
|
||||
priceOk = input.sellAtPercent == 0 or (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 +126,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,492 @@
|
||||
-- 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
|
||||
|
||||
-- Ekstra roller for et redskab: sprøjtens roller (gødning/kalk/ukrudt) — men ikke for en såmaskine med
|
||||
-- gødningstank (Kinze 4905), den er kun såmaskine (in-game 30/9: hentet til ren gødning).
|
||||
function ADPool.rolesOf(role, sprayRoles)
|
||||
if role ~= "seeder" and sprayRoles ~= nil then return sprayRoles end
|
||||
return ADFieldFleet.rolesFor(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 = ADPool.rolesOf(role, specs.sprayRoles), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
local park = parkMarker(adEnv, vehicle)
|
||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
||||
and 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,123 @@
|
||||
-- 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"},
|
||||
{ADChain, "evaluate", "kæde"},
|
||||
{ADChainData, "collect", "kæde: læs gården"},
|
||||
}
|
||||
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,9 @@ local function copyRun(run)
|
||||
loops = run.loops,
|
||||
returnBelowPercent = run.returnBelowPercent,
|
||||
vehicleIds = copyList(run.vehicleIds),
|
||||
stopped = run.stopped == true,
|
||||
wagons = ADRuns.copyWagons(run.wagons),
|
||||
chain = run.chain or "",
|
||||
}
|
||||
end
|
||||
|
||||
@@ -98,6 +136,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 = {},
|
||||
@@ -107,6 +146,7 @@ function ADRuns.createRun(store, name, kind)
|
||||
loops = 0,
|
||||
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
||||
vehicleIds = {},
|
||||
chain = "",
|
||||
}
|
||||
new.nextId = new.nextId + 1
|
||||
table.insert(new.runs, run)
|
||||
@@ -162,9 +202,12 @@ function ADRuns.updateRun(store, runId, changes)
|
||||
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end
|
||||
if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end
|
||||
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
|
||||
-- 0 = "Altid" (sælg uanset pris); ellers 50-100 %
|
||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = tonumber(changes.sellAtPercent) == 0 and 0 or 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
|
||||
if changes.chain ~= nil then run.chain = tostring(changes.chain) end
|
||||
return new
|
||||
end
|
||||
|
||||
@@ -308,10 +351,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 +440,11 @@ 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),
|
||||
chain = run.chain or "",
|
||||
}
|
||||
end
|
||||
return records
|
||||
@@ -405,8 +485,9 @@ function ADRuns.fromRecords(records)
|
||||
targetBuildingIds = parseIds(record.targetBuildings),
|
||||
storeBuildingIds = parseIds(record.storeBuildings),
|
||||
products = ADRuns.parseProducts(record.products),
|
||||
sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||
sellAtPercent = tonumber(record.sellAt) == 0 and 0 or 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 +497,9 @@ 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),
|
||||
chain = record.chain or "",
|
||||
})
|
||||
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,15 @@ function ADRunsController.save()
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||
pcall(ADFieldJobs.save)
|
||||
end
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||
-- kædens tilstand (tændte mellemled, pausede kæder, afviste forslag) — samme gem-regel som navnefilen
|
||||
if ADChain ~= nil and ADChain.save ~= nil then pcall(ADChain.save) end
|
||||
if ADSellBlock ~= nil and ADSellBlock.save ~= nil then pcall(ADSellBlock.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 +1495,12 @@ 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 ADChain ~= nil and ADChain.load ~= nil then pcall(ADChain.load) end
|
||||
if ADSellBlock ~= nil and ADSellBlock.load ~= nil then pcall(ADSellBlock.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 +1511,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,12 @@ 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 "")
|
||||
xmlFile:setString(key .. "#chain", record.chain or "")
|
||||
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||
end
|
||||
@@ -74,7 +79,11 @@ 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"),
|
||||
chain = xmlFile:getString(key .. "#chain"),
|
||||
vehicleIds = {},
|
||||
}
|
||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
-- AD Sell Block
|
||||
-- Salgssteder spilleren har slået fra (in-game 30/9: "Market Price Liquid Fill Station" fra FarmFillStations gik i stå
|
||||
-- ved aflæsning af gylle). Konsol: spuSellBlock viser salgsstederne; spuSellBlock <del af navn> slår dem fra/til.
|
||||
-- Udkørsel (ADSources.findSellMarkers) og priser (ADSources.getMarketQuote) springer udelukkede over. Gemmes pr.
|
||||
-- savegame i ADSmartPickup_sellblock.xml (skrives fra ADRunsController.save, som de andre filer).
|
||||
|
||||
ADSellBlock = {}
|
||||
ADSellBlock.FILE_NAME = "ADSmartPickup_sellblock.xml"
|
||||
ADSellBlock.ROOT = "sellBlock"
|
||||
ADSellBlock.blocked = {} -- {[stationId] = visningsnavn}
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- stations = {{id, names = {...}}}, query = del af et navn (små/store bogstaver ligegyldige) -> matchende stationer
|
||||
function ADSellBlock.match(stations, query)
|
||||
local wanted = string.lower(tostring(query or ""))
|
||||
local hits = {}
|
||||
if wanted == "" then return hits end
|
||||
for _, station in ipairs(stations or {}) do
|
||||
for _, name in ipairs(station.names or {}) do
|
||||
if string.find(string.lower(tostring(name)), wanted, 1, true) ~= nil then
|
||||
table.insert(hits, station)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return hits
|
||||
end
|
||||
|
||||
-- Ny tabel: hver station skiftes (fra -> til, til -> fra). Første navn huskes til visning.
|
||||
function ADSellBlock.toggle(blocked, stations)
|
||||
local result = {}
|
||||
for id, name in pairs(blocked or {}) do result[id] = name end
|
||||
for _, station in ipairs(stations or {}) do
|
||||
if result[station.id] ~= nil then
|
||||
result[station.id] = nil
|
||||
else
|
||||
result[station.id] = tostring((station.names or {})[1] or station.id)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function stationId(station)
|
||||
local placeable = station ~= nil and station.owningPlaceable or nil
|
||||
if placeable ~= nil and placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADSellBlock.isBlocked(station)
|
||||
local id = stationId(station)
|
||||
return id ~= nil and ADSellBlock.blocked[id] ~= nil
|
||||
end
|
||||
|
||||
local function stationNames(station)
|
||||
local names = {}
|
||||
local placeable = station.owningPlaceable
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then table.insert(names, name) end
|
||||
end
|
||||
if station.getName ~= nil then
|
||||
local ok, name = pcall(station.getName, station)
|
||||
if ok and name ~= nil then table.insert(names, name) end
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
-- Alle salgssteder med AD-markør -> {{id, names}} (markørens navn først: det spilleren ser i AutoDrive).
|
||||
local function sellStations(adEnv)
|
||||
local list, byId = {}, {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||
local id = station ~= nil and ADPrices.isSellingStation(station) and stationId(station) or nil
|
||||
if id ~= nil then
|
||||
if byId[id] == nil then
|
||||
byId[id] = {id = id, names = {}}
|
||||
table.insert(list, byId[id])
|
||||
for _, name in ipairs(stationNames(station)) do table.insert(byId[id].names, name) end
|
||||
end
|
||||
table.insert(byId[id].names, 1, marker.name)
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return tostring(a.names[1]) < tostring(b.names[1]) end)
|
||||
return list
|
||||
end
|
||||
|
||||
function ADSellBlock:consoleSellBlock(...)
|
||||
local query = table.concat({...}, " ")
|
||||
local adEnv = FS25_AutoDrive
|
||||
if adEnv == nil or adEnv.ADGraphManager == nil then return "AutoDrive ikke fundet" end
|
||||
local stations = sellStations(adEnv)
|
||||
if query == "" then
|
||||
local lines = {"Salgssteder (spuSellBlock <del af navn> slår fra/til):"}
|
||||
for _, station in ipairs(stations) do
|
||||
table.insert(lines, string.format(" %s %s", ADSellBlock.blocked[station.id] ~= nil and "[FRA]" or "[til]", table.concat(station.names, " / ")))
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
local hits = ADSellBlock.match(stations, query)
|
||||
if #hits == 0 then return string.format("Intet salgssted matcher '%s' (spuSellBlock uden navn viser listen)", query) end
|
||||
ADSellBlock.blocked = ADSellBlock.toggle(ADSellBlock.blocked, hits)
|
||||
local lines = {}
|
||||
for _, station in ipairs(hits) do
|
||||
local text = string.format("%s: %s", station.names[1], ADSellBlock.blocked[station.id] ~= nil and "slået FRA som salgssted" or "bruges igen")
|
||||
table.insert(lines, text)
|
||||
Logging.info("ADSmartPickup: salgssted " .. text)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.save ~= nil then pcall(ADRunsController.save) end
|
||||
return table.concat(lines, "\n")
|
||||
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 .. "$", ADSellBlock.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADSellBlock.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupSellBlock", path, ADSellBlock.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local index = 0
|
||||
for id, name in pairs(ADSellBlock.blocked) do
|
||||
local key = string.format("%s.station(%d)", ADSellBlock.ROOT, index)
|
||||
xmlFile:setString(key .. "#id", id)
|
||||
xmlFile:setString(key .. "#name", name)
|
||||
index = index + 1
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADSellBlock.load()
|
||||
ADSellBlock.blocked = {}
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupSellBlock", path)
|
||||
if xmlFile == nil then return end
|
||||
xmlFile:iterate(ADSellBlock.ROOT .. ".station", function(_, key)
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil then ADSellBlock.blocked[id] = xmlFile:getString(key .. "#name") or id end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuSellBlock", "Salgssteder: uden navn = liste; spuSellBlock <del af navn> slår fra/til", "consoleSellBlock", ADSellBlock)
|
||||
end
|
||||
@@ -10,14 +10,30 @@
|
||||
|
||||
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/SmartPickupChainTab.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 +232,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 +266,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 +375,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 +423,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 +483,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 +670,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 +703,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 +730,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 +798,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 +1149,12 @@ 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
|
||||
if ADLoadDiag ~= nil and ADLoadDiag.install ~= nil then
|
||||
ADLoadDiag.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 +1206,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 +1267,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 +1295,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,11 +1307,47 @@ 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 ADChain ~= nil then
|
||||
local okChain, errChain = pcall(ADChain.tick, getAutoDriveEnv(), dt)
|
||||
if not okChain then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kæde fejlede: %s", tostring(errChain))
|
||||
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))
|
||||
supplyState = setmetatable({}, {__mode = "k"})
|
||||
end
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.update, dt, getAutoDriveEnv()) end
|
||||
end
|
||||
|
||||
-- Statusvinduet på skærmen (adStatusHud.lua): tegner kun den cachede tekst.
|
||||
function ADSmartPickup:draw()
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.draw) end
|
||||
end
|
||||
|
||||
function ADSmartPickup:deleteMap()
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.delete) end
|
||||
end
|
||||
|
||||
addModEventListener(ADSmartPickup)
|
||||
|
||||
@@ -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
|
||||
@@ -499,9 +585,17 @@ function ADSources.getObjectInfoIndex(source, fillType)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Alle grupper med varen: lageret deler samme vare i flere objectInfos (fx paller med forskellig fyldning). Kun den
|
||||
-- første talt -> tallet "voksede", når en lille gruppe blev tom, og spawn-ventningen gik i stå (Sell Egg 30/9).
|
||||
function ADSources.countStored(source, fillType)
|
||||
local _, count = ADSources.getObjectInfoIndex(source, fillType)
|
||||
return count or 0
|
||||
local spec = source.placeable ~= nil and source.placeable.spec_objectStorage or nil
|
||||
local count = 0
|
||||
for _, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
|
||||
if getObjectInfoContent(objectInfo) == fillType then
|
||||
count = count + (objectInfo.numObjects or 0)
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
|
||||
@@ -550,12 +644,21 @@ function ADSources.describeAccepted(station)
|
||||
return table.concat(names, ", ")
|
||||
end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||||
-- Salgssted spilleren har slået fra (spuSellBlock, ADSellBlock).
|
||||
function ADSources.isSellBlocked(station)
|
||||
return ADSellBlock ~= nil and ADSellBlock.isBlocked ~= nil and ADSellBlock.isBlocked(station) == true
|
||||
end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted (og ikke slået fra): { {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)
|
||||
if station ~= nil and ADPrices.isSellingStation(station) then
|
||||
if station ~= nil and ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) then
|
||||
table.insert(result, {marker = marker, station = station})
|
||||
end
|
||||
end
|
||||
@@ -852,16 +955,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
|
||||
@@ -872,13 +976,38 @@ function ADSources.getFarmRates(adEnv, farmId)
|
||||
return rates
|
||||
end
|
||||
|
||||
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
|
||||
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
|
||||
function ADSources.getHusbandryConsumption(placeable)
|
||||
local rates, result = {}, {}
|
||||
if placeable == nil then return result end
|
||||
pcall(addHusbandryRates, rates, placeable)
|
||||
for fillType, rate in pairs(rates) do
|
||||
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
|
||||
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
|
||||
function ADSources.getHusbandryOutputs(placeable)
|
||||
local rates, outputs = {}, {}
|
||||
if placeable == nil then return outputs end
|
||||
pcall(addHusbandryRates, rates, placeable) -- giver kun forbrug; med for fuldstændighedens skyld
|
||||
pcall(addAnimalProduction, rates, placeable)
|
||||
for fillType, rate in pairs(rates) do
|
||||
if (rate.production or 0) > 0 then outputs[fillType] = rate.production end
|
||||
end
|
||||
return outputs
|
||||
end
|
||||
|
||||
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
|
||||
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
||||
function ADSources.getMarketQuote(fillType)
|
||||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||
local best = nil
|
||||
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
|
||||
if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then
|
||||
if ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) and ADSources.stationAccepts(station, fillType) then
|
||||
local quote = ADPrices.getQuote(station, fillType)
|
||||
if quote ~= nil and (best == nil or quote.price > best.price) then
|
||||
best = {price = quote.price, top = quote.top}
|
||||
@@ -999,6 +1128,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
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
-- AD Status Hud
|
||||
-- Statusvinduet på skærmen (i stil med Simple Inspector): slås til/fra med input-handlingen SPU_TOGGLE_STATUS
|
||||
-- (standard numpad *). Øjebliksbilledet bygges af modens egne statusfunktioner (samme som fanerne) højst
|
||||
-- hvert sekund i update() og kun mens vinduet er tændt; draw() tegner kun de cachede linjer og bredder.
|
||||
-- Synligheden gemmes pr. bruger i modSettings/FS25_ADSmartPickup/status.xml (standard skjult).
|
||||
-- Kører kun på en klient (ikke dedikeret server). Alle spilkald er beskyttet; fejl logges én gang pr. nøgle.
|
||||
|
||||
ADStatusHud = {}
|
||||
|
||||
ADStatusHud.LOG_PREFIX = "ADSmartPickup: status: "
|
||||
ADStatusHud.REFRESH_MS = 1000
|
||||
ADStatusHud.STUCK_WARN_MS = 120000 -- stået stille så længe (uden at Smart Pickup holder den) = advarsel
|
||||
ADStatusHud.TEXT_SIZE_PX = 14
|
||||
ADStatusHud.MARGIN_PX = 8
|
||||
ADStatusHud.SCREEN_OFFSET_PX = 16 -- afstand til skærmens højre kant
|
||||
ADStatusHud.TOP_Y = 1 - 0.12 -- øverste kant (normaliseret); under spillets info-boks
|
||||
ADStatusHud.SETTINGS_DIR = "modSettings/FS25_ADSmartPickup/"
|
||||
ADStatusHud.SETTINGS_FILE = "status.xml"
|
||||
ADStatusHud.SETTINGS_ROOT = "adSmartPickupStatus"
|
||||
ADStatusHud.HEADER_COLOR = {1, 1, 1, 1}
|
||||
|
||||
ADStatusHud.visible = false
|
||||
ADStatusHud.lines = {} -- {{text, level, header, color, width}}
|
||||
ADStatusHud.layout = nil -- {x, y, width, height, textSize, marginX, marginY, padding}
|
||||
|
||||
local timerMs = 0
|
||||
local settingsLoaded = false
|
||||
local overlays = nil
|
||||
local loggedErrors = {}
|
||||
local registerToggleActionRef -- sat nederst (tastens handler til krogen)
|
||||
|
||||
-- ---- hjælpere ----
|
||||
|
||||
local function logOnce(key, err)
|
||||
if loggedErrors[key] then return end
|
||||
loggedErrors[key] = true
|
||||
if Logging ~= nil and Logging.warning ~= nil then
|
||||
Logging.warning(ADStatusHud.LOG_PREFIX .. "%s fejlede: %s", tostring(key), tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
local function guarded(key, fn, ...)
|
||||
local ok, result = pcall(fn, ...)
|
||||
if not ok then
|
||||
logOnce(key, result)
|
||||
return nil
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function textOf(key)
|
||||
if g_i18n == nil then return key end
|
||||
local ok, text = pcall(g_i18n.getText, g_i18n, key)
|
||||
return ok and text or key
|
||||
end
|
||||
|
||||
function ADStatusHud.isClient()
|
||||
local mission = g_currentMission
|
||||
if mission == nil or mission.getIsClient == nil then return false end
|
||||
local ok, isClient = pcall(mission.getIsClient, mission)
|
||||
return ok and isClient == true
|
||||
end
|
||||
|
||||
-- Fanernes egne tekst-/statusfunktioner ligger som metoder på SmartPickupFrame og bruger kun self.i18n:
|
||||
-- en let "frame" med spillets i18n genbruger dem uden at røre menuen. "Start afvist" (lastStartFailed, rødt flow)
|
||||
-- huskes på den rigtige ESC-fane (ADSmartPickup.guiFrame): tabellen deles, så vinduet viser samme farve som
|
||||
-- Flows-listen (flowLevel rydder en afvisning når flowet kører — det samme som fanen selv gør).
|
||||
local function frameProxy()
|
||||
if SmartPickupFrame == nil then return nil end
|
||||
local real = ADSmartPickup ~= nil and ADSmartPickup.guiFrame or nil
|
||||
local failed = {}
|
||||
if type(real) == "table" then
|
||||
if type(real.lastStartFailed) ~= "table" then real.lastStartFailed = {} end
|
||||
failed = real.lastStartFailed
|
||||
end
|
||||
return setmetatable({i18n = g_i18n, lastStartFailed = failed}, {__index = SmartPickupFrame})
|
||||
end
|
||||
|
||||
local function fillTitle(fillType)
|
||||
if fillType == nil or g_fillTypeManager == nil then return "?" end
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType)
|
||||
or g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||
return tostring(title or "?")
|
||||
end
|
||||
|
||||
-- ---- øjebliksbillede ----
|
||||
|
||||
local WAGON_ACTIONS = {
|
||||
fetching = {key = "spu_status_act_fetchWagon", level = "waiting"},
|
||||
waiting = {key = "spu_status_act_waitWagon", level = "waiting"},
|
||||
emptying = {key = "spu_status_act_emptying", level = "running"},
|
||||
homing = {key = "spu_status_act_homing", level = "running"},
|
||||
}
|
||||
|
||||
-- Traktorens handling som kort tekst + niveau (ADRunsController.getVehicleActivity som Flows-fanen).
|
||||
local function vehicleAction(adEnv, vehicle)
|
||||
local wagonState = ADFlowWagons ~= nil and ADFlowWagons.statusOf ~= nil and ADFlowWagons.statusOf(vehicle) or nil
|
||||
if wagonState ~= nil and WAGON_ACTIONS[wagonState] ~= nil then
|
||||
return textOf(WAGON_ACTIONS[wagonState].key), WAGON_ACTIONS[wagonState].level
|
||||
end
|
||||
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
|
||||
local place = tostring(activity.place or "?")
|
||||
if activity.kind == "stopped" then
|
||||
return textOf("spu_activity_stopped"), "stopped"
|
||||
elseif activity.kind == "waiting" then
|
||||
return string.format(textOf("spu_status_act_waiting"), place), "waiting"
|
||||
elseif activity.kind == "holding" then
|
||||
return string.format(textOf("spu_status_act_holding"), place), "waiting"
|
||||
elseif activity.kind == "empty" and activity.fillType ~= nil then
|
||||
return string.format(textOf("spu_status_act_fetch"), fillTitle(activity.fillType), place), "running"
|
||||
end
|
||||
return string.format(textOf("spu_status_act_driving"), place), "running"
|
||||
end
|
||||
|
||||
-- Fyld i % af hele vogntoget (AutoDrives egne opgørelser, som kildevalget bruger); nil uden kapacitet.
|
||||
local function fillPercent(adEnv, vehicle)
|
||||
local autoDrive = adEnv.AutoDrive
|
||||
if autoDrive == nil or autoDrive.getAllUnits == nil or autoDrive.getAllFillLevels == nil then return nil end
|
||||
local level, capacity = autoDrive.getAllFillLevels(autoDrive.getAllUnits(vehicle))
|
||||
if type(level) ~= "number" or type(capacity) ~= "number" or capacity <= 0 then return nil end
|
||||
return math.max(0, math.min(100, level / capacity * 100))
|
||||
end
|
||||
|
||||
local function farmVehiclesById(adEnv)
|
||||
local byId = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if id ~= nil then byId[id] = vehicle end
|
||||
end
|
||||
return byId
|
||||
end
|
||||
|
||||
-- Én linje pr. traktor i et kørende flow (flows der kan stoppes: startet, eller holder vogn/henter).
|
||||
function ADStatusHud.flowEntries(adEnv, proxy)
|
||||
local entries = {}
|
||||
if ADRunsController == nil or ADRunsController.store == nil or ADFlowView == nil then return entries end
|
||||
local byId = farmVehiclesById(adEnv)
|
||||
for _, run in ipairs(ADFlowView.orderedRuns(ADRunsController.store.runs or {})) do
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
if status.canStop then
|
||||
local runLevel = proxy ~= nil and proxy.flowLevel ~= nil and guarded("flowLevel", proxy.flowLevel, proxy, adEnv, run) or nil
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = byId[vehicleId]
|
||||
if vehicle ~= nil then
|
||||
local action, level = vehicleAction(adEnv, vehicle)
|
||||
-- farven følger flowets niveau som Flows-listen (venter/fejl), ellers traktorens egen handling
|
||||
if runLevel == "error" or runLevel == "waiting" then level = runLevel end
|
||||
table.insert(entries, {vehicle = tostring(ADRuns.vehicleLabel(vehicle)), action = action,
|
||||
fillPercent = guarded("fillPercent", fillPercent, adEnv, vehicle), level = level})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Én linje pr. mark med aktivt markarbejde (samme statustekst og farve som Marker-fanen).
|
||||
function ADStatusHud.fieldEntries(proxy)
|
||||
local entries = {}
|
||||
if ADFields == nil or ADFieldJobs == nil or ADFieldModel == nil or proxy == nil then return entries end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
if field ~= nil and (ADFieldModel.isWorking(field) or ADFieldJobs.awaitingPlayer(info.id) ~= nil) then
|
||||
local names = {}
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
table.insert(entries, {
|
||||
fieldId = info.id,
|
||||
work = proxy:fieldStatusText(field, info, true),
|
||||
vehicle = #names > 0 and ADFlowView.joinCounted(names, ", ", nil) or nil,
|
||||
progress = SmartPickupFrame.monthCpProgress ~= nil and guarded("fieldProgress", SmartPickupFrame.monthCpProgress, info.id) or nil,
|
||||
level = proxy:fieldLevel(info.id),
|
||||
})
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Kæder med manglende led + ventende forslag + slukkede nødvendige fabrikker (samme punkter og tekster som
|
||||
-- "Kræver handling"; de to sidste står under Advarsler).
|
||||
function ADStatusHud.chainEntries(proxy)
|
||||
local chains, suggestions = {}, {}
|
||||
local result = {chains = chains, suggestions = suggestions, dependencies = {}}
|
||||
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
|
||||
return result
|
||||
end
|
||||
for _, entry in ipairs(ADStorageStats.chainEntries(ADChain.last)) do
|
||||
if entry.kind == "chainProblem" then
|
||||
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
|
||||
elseif entry.kind == "chainSuggest" then
|
||||
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
|
||||
elseif entry.kind == "dependencyOff" then
|
||||
table.insert(result.dependencies, {text = proxy:alertText(entry), level = "error"})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Solgt/slettet køretøj (ADStuckDiag kan holde det et øjeblik efter salget).
|
||||
local function isGone(vehicle)
|
||||
if vehicle == nil or vehicle.isDeleted then return true end
|
||||
if vehicle.rootNode == nil then return true end
|
||||
return entityExists ~= nil and not entityExists(vehicle.rootNode)
|
||||
end
|
||||
|
||||
-- Gårdens køretøjer der har stået stille over grænsen, uden at Smart Pickup selv holder dem (Wait/udkørsel).
|
||||
function ADStatusHud.stuckEntries()
|
||||
local entries = {}
|
||||
if ADStuckDiag == nil or ADStuckDiag.stillVehicles == nil then return entries end
|
||||
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or nil
|
||||
for _, still in ipairs(ADStuckDiag.stillVehicles(ADStatusHud.STUCK_WARN_MS)) do
|
||||
local vehicle = still.vehicle
|
||||
local owned = not isGone(vehicle) and (farmId == nil or (vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId))
|
||||
local held = owned and ((ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) ~= nil)
|
||||
or (ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) ~= nil))
|
||||
if owned and not held then
|
||||
table.insert(entries, {text = string.format(textOf("spu_status_stuck"), tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
math.floor(still.ms / 60000)), level = "error"})
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function ADStatusHud.snapshot(adEnv)
|
||||
local proxy = frameProxy()
|
||||
local snapshot = {flows = {}, fields = {}, chains = {}, warnings = {}}
|
||||
if adEnv ~= nil then
|
||||
snapshot.flows = guarded("flows", ADStatusHud.flowEntries, adEnv, proxy) or {}
|
||||
end
|
||||
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
|
||||
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
|
||||
snapshot.chains = chainResult.chains or {}
|
||||
for _, entry in ipairs(chainResult.dependencies or {}) do table.insert(snapshot.warnings, entry) end
|
||||
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
|
||||
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
|
||||
return snapshot
|
||||
end
|
||||
|
||||
-- ---- layout og cache (kun i update) ----
|
||||
|
||||
local function gameInfoDisplay()
|
||||
local hud = g_currentMission ~= nil and g_currentMission.hud or nil
|
||||
return hud ~= nil and hud.gameInfoDisplay or nil
|
||||
end
|
||||
|
||||
-- Størrelser i skærmkoordinater, skaleret som spillets egen HUD (Simple Inspector-mønster).
|
||||
local function measureUnits()
|
||||
local display = gameInfoDisplay()
|
||||
if display == nil or display.scalePixelToScreenHeight == nil or display.scalePixelToScreenVector == nil then return nil end
|
||||
local textSize = display:scalePixelToScreenHeight(ADStatusHud.TEXT_SIZE_PX)
|
||||
local marginX, marginY = display:scalePixelToScreenVector({ADStatusHud.MARGIN_PX, ADStatusHud.MARGIN_PX})
|
||||
local offsetX = display:scalePixelToScreenVector({ADStatusHud.SCREEN_OFFSET_PX, 0})
|
||||
return textSize, marginX, marginY, offsetX
|
||||
end
|
||||
|
||||
-- Øverste kant: fast under info-boksen; ligger info-boksens position lavere, rykkes vinduet ned under den.
|
||||
local function topY(marginY)
|
||||
local top = ADStatusHud.TOP_Y
|
||||
local display = gameInfoDisplay()
|
||||
if display ~= nil and display.getPosition ~= nil then
|
||||
local ok, _, y = pcall(display.getPosition, display)
|
||||
if ok and type(y) == "number" and y > 0.5 and y < 1 then top = math.min(top, y - marginY) end
|
||||
end
|
||||
return top
|
||||
end
|
||||
|
||||
function ADStatusHud.rebuild(adEnv)
|
||||
local lines = ADStatusView.lines(ADStatusHud.snapshot(adEnv), textOf, ADStatusView.MAX_LINES)
|
||||
local textSize, marginX, marginY, offsetX = measureUnits()
|
||||
if textSize == nil then
|
||||
-- spillets info-boks (skalering) findes ikke: vinduet kan ikke måles og tegnes
|
||||
logOnce("measure", "gameInfoDisplay mangler eller kan ikke skalere")
|
||||
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||
return
|
||||
end
|
||||
local maxWidth = 0
|
||||
for _, line in ipairs(lines) do
|
||||
setTextBold(line.header == true)
|
||||
line.width = getTextWidth(textSize, line.text)
|
||||
line.color = line.header and ADStatusHud.HEADER_COLOR or (ADFlowView.LEVEL_COLORS[line.level] or ADStatusHud.HEADER_COLOR)
|
||||
if line.width > maxWidth then maxWidth = line.width end
|
||||
end
|
||||
setTextBold(false)
|
||||
local width = maxWidth + 2 * marginX
|
||||
local height = #lines * textSize + 2 * marginY
|
||||
local top = topY(marginY)
|
||||
ADStatusHud.lines = lines
|
||||
ADStatusHud.layout = {x = 1 - offsetX - width, y = top - height, top = top, width = width, height = height,
|
||||
textSize = textSize, marginX = marginX, marginY = marginY, padding = marginX * 0.5}
|
||||
end
|
||||
|
||||
-- ---- overlays ----
|
||||
|
||||
local function createOverlays()
|
||||
if overlays ~= nil then return overlays end
|
||||
if g_overlayManager == nil or HUD == nil or HUD.COLOR == nil or HUD.COLOR.BACKGROUND == nil then return nil end
|
||||
local r, g, b, a = unpack(HUD.COLOR.BACKGROUND)
|
||||
local created = {
|
||||
middle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0),
|
||||
left = g_overlayManager:createOverlay("gui.gameInfo_left", 0, 0, 0, 0),
|
||||
right = g_overlayManager:createOverlay("gui.gameInfo_right", 0, 0, 0, 0),
|
||||
}
|
||||
for _, overlay in pairs(created) do overlay:setColor(r, g, b, a) end
|
||||
overlays = created
|
||||
return overlays
|
||||
end
|
||||
|
||||
local function deleteOverlays()
|
||||
if overlays == nil then return end
|
||||
for _, overlay in pairs(overlays) do
|
||||
if overlay.delete ~= nil then pcall(overlay.delete, overlay) end
|
||||
end
|
||||
overlays = nil
|
||||
end
|
||||
|
||||
-- ---- indstillinger (pr. bruger, ikke i savegamet) ----
|
||||
|
||||
local function settingsPath()
|
||||
if getUserProfileAppPath == nil then return nil end
|
||||
return getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR .. ADStatusHud.SETTINGS_FILE
|
||||
end
|
||||
|
||||
function ADStatusHud.loadSettings()
|
||||
settingsLoaded = true
|
||||
local path = settingsPath()
|
||||
if path == nil or fileExists == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupStatus", path)
|
||||
if xmlFile == nil then return end
|
||||
ADStatusHud.visible = xmlFile:getBool(ADStatusHud.SETTINGS_ROOT .. "#visible", false) == true
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
function ADStatusHud.saveSettings()
|
||||
local path = settingsPath()
|
||||
if path == nil then return end
|
||||
if createFolder ~= nil then
|
||||
createFolder(getUserProfileAppPath() .. "modSettings/")
|
||||
createFolder(getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR)
|
||||
end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupStatus", path, ADStatusHud.SETTINGS_ROOT)
|
||||
if xmlFile == nil then
|
||||
logOnce("saveSettings", "kunne ikke oprette " .. path)
|
||||
return
|
||||
end
|
||||
xmlFile:setBool(ADStatusHud.SETTINGS_ROOT .. "#visible", ADStatusHud.visible == true)
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
-- ---- offentlige kald fra moden ----
|
||||
|
||||
function ADStatusHud.toggle()
|
||||
ADStatusHud.visible = not ADStatusHud.visible
|
||||
timerMs = ADStatusHud.REFRESH_MS -- byg straks ved næste update
|
||||
if not ADStatusHud.visible then ADStatusHud.lines, ADStatusHud.layout = {}, nil end
|
||||
guarded("saveSettings", ADStatusHud.saveSettings)
|
||||
end
|
||||
|
||||
-- Action-event-callback (target = ADStatusHud).
|
||||
function ADStatusHud.onToggleAction()
|
||||
ADStatusHud.toggle()
|
||||
end
|
||||
|
||||
function ADStatusHud.update(dt, adEnv)
|
||||
if not ADStatusHud.isClient() then return end
|
||||
if not settingsLoaded then
|
||||
guarded("loadSettings", ADStatusHud.loadSettings)
|
||||
timerMs = ADStatusHud.REFRESH_MS
|
||||
end
|
||||
if not ADStatusHud.visible then return end
|
||||
timerMs = timerMs + (dt or 0)
|
||||
if timerMs < ADStatusHud.REFRESH_MS then return end
|
||||
timerMs = 0
|
||||
guarded("rebuild", ADStatusHud.rebuild, adEnv)
|
||||
end
|
||||
|
||||
-- Tegnes ikke ved åben menu/dialog, skjult HUD, søvn eller åben chat.
|
||||
local function shouldSkipDraw()
|
||||
if g_gui ~= nil and g_gui:getIsGuiVisible() then return true end
|
||||
if g_noHudModeEnabled then return true end
|
||||
local mission = g_currentMission
|
||||
local hud = mission ~= nil and mission.hud or nil
|
||||
if hud == nil or hud.isVisible == false then return true end
|
||||
if g_sleepManager ~= nil and g_sleepManager.getIsSleeping ~= nil and g_sleepManager:getIsSleeping() then return true end
|
||||
if hud.chatDisplay ~= nil and hud.chatDisplay.getVisible ~= nil and hud.chatDisplay:getVisible() then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
local function render(layout, bg)
|
||||
bg.middle:setDimension(layout.width, layout.height)
|
||||
bg.middle:setPosition(layout.x, layout.y)
|
||||
bg.middle:render()
|
||||
bg.left:setDimension(layout.padding, layout.height)
|
||||
bg.left:setPosition(layout.x - layout.padding, layout.y)
|
||||
bg.left:render()
|
||||
bg.right:setDimension(layout.padding, layout.height)
|
||||
bg.right:setPosition(layout.x + layout.width, layout.y)
|
||||
bg.right:render()
|
||||
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_TOP)
|
||||
local textX, textY = layout.x + layout.marginX, layout.top - layout.marginY
|
||||
for _, line in ipairs(ADStatusHud.lines) do
|
||||
local color = line.color
|
||||
setTextBold(line.header == true)
|
||||
setTextColor(color[1], color[2], color[3], color[4])
|
||||
renderText(textX, textY, layout.textSize, line.text)
|
||||
textY = textY - layout.textSize
|
||||
end
|
||||
end
|
||||
|
||||
function ADStatusHud.draw()
|
||||
if not ADStatusHud.visible or ADStatusHud.layout == nil or #ADStatusHud.lines == 0 then return end
|
||||
if shouldSkipDraw() then return end
|
||||
local ok0, bg = pcall(createOverlays)
|
||||
if not ok0 or bg == nil then
|
||||
-- uden baggrund tegnes intet (g_overlayManager/HUD.COLOR mangler eller createOverlay fejlede)
|
||||
logOnce("overlays", ok0 and "g_overlayManager eller HUD.COLOR.BACKGROUND mangler" or bg)
|
||||
return
|
||||
end
|
||||
local ok, err = pcall(render, ADStatusHud.layout, bg)
|
||||
-- tekststate nulstilles altid, også efter en fejl (andre mods/spillet tegner videre med den)
|
||||
setTextColor(1, 1, 1, 1)
|
||||
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)
|
||||
setTextBold(false)
|
||||
if not ok then logOnce("draw", err) end
|
||||
end
|
||||
|
||||
function ADStatusHud.delete()
|
||||
deleteOverlays()
|
||||
if PlayerInputComponent ~= nil and PlayerInputComponent.spuStatusToggleHandler == registerToggleActionRef then
|
||||
PlayerInputComponent.spuStatusToggleHandler = nil
|
||||
end
|
||||
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||
settingsLoaded = false
|
||||
timerMs = 0
|
||||
end
|
||||
|
||||
-- ---- tast: global spiller-handling (til fods og i køretøjer) ----
|
||||
|
||||
-- PlayerInputComponent.registerGlobalPlayerActionEvents kaldes ved hvert kontekstskift (til fods / i køretøj);
|
||||
-- samme mønster som FS25_Courseplay (CP_OPEN_INGAME_MENU) og FS25_EasyDevControls. Hjælpeteksten skjules i F1.
|
||||
local function registerToggleAction(component)
|
||||
if not ADStatusHud.isClient() then return end
|
||||
local player = component ~= nil and component.player or nil
|
||||
if player ~= nil and player.isOwner == false then return end
|
||||
if g_inputBinding == nil or InputAction == nil or InputAction.SPU_TOGGLE_STATUS == nil then return end
|
||||
local _, eventId = g_inputBinding:registerActionEvent(InputAction.SPU_TOGGLE_STATUS, ADStatusHud, ADStatusHud.onToggleAction,
|
||||
false, true, false, true)
|
||||
if eventId ~= nil then g_inputBinding:setActionEventTextVisibility(eventId, false) end
|
||||
end
|
||||
|
||||
registerToggleActionRef = function(component) guarded("registerAction", registerToggleAction, component) end
|
||||
|
||||
-- Spillets klasse overlever en genindlæsning af savegamet, moden gør ikke: krogen lægges kun én gang (markør på
|
||||
-- klassen) og kalder den aktuelle indlæsnings handler, så en gammel ADStatusHud aldrig registrerer tasten igen.
|
||||
function ADStatusHud.installInputHook()
|
||||
if PlayerInputComponent == nil or PlayerInputComponent.registerGlobalPlayerActionEvents == nil or Utils == nil then return false end
|
||||
PlayerInputComponent.spuStatusToggleHandler = registerToggleActionRef
|
||||
if PlayerInputComponent.spuStatusToggleHooked then return true end
|
||||
PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.appendedFunction(PlayerInputComponent.registerGlobalPlayerActionEvents,
|
||||
function(component, ...)
|
||||
local handler = PlayerInputComponent.spuStatusToggleHandler
|
||||
if handler ~= nil then handler(component) end
|
||||
end)
|
||||
PlayerInputComponent.spuStatusToggleHooked = true
|
||||
return true
|
||||
end
|
||||
|
||||
if PlayerInputComponent ~= nil and Utils ~= nil then
|
||||
ADStatusHud.installInputHook()
|
||||
end
|
||||
@@ -0,0 +1,143 @@
|
||||
-- AD Status View
|
||||
-- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet
|
||||
-- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med
|
||||
-- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, "..").
|
||||
--
|
||||
-- snapshot = {
|
||||
-- flows = {{vehicle, action, fillPercent (0-100|nil), level}},
|
||||
-- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}},
|
||||
-- chains = {{text, level}}, -- standard rød
|
||||
-- warnings = {{text, level}}, -- standard gul
|
||||
-- }
|
||||
-- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header".
|
||||
|
||||
ADStatusView = {}
|
||||
|
||||
ADStatusView.MAX_LINES = 18
|
||||
ADStatusView.MAX_CHARS = 48
|
||||
ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen
|
||||
ADStatusView.MIN_FLEX_CHARS = 6
|
||||
ADStatusView.SEPARATOR = " · "
|
||||
ADStatusView.ELLIPSIS = ".."
|
||||
|
||||
local LEVELS = {running = true, waiting = true, error = true, stopped = true}
|
||||
|
||||
local SECTIONS = {
|
||||
{key = "flows", header = "spu_status_flows", defaultLevel = "running"},
|
||||
{key = "fields", header = "spu_status_fields", defaultLevel = "running"},
|
||||
{key = "chains", header = "spu_status_chains", defaultLevel = "error"},
|
||||
{key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"},
|
||||
}
|
||||
|
||||
-- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ----
|
||||
|
||||
function ADStatusView.textLength(text)
|
||||
local count = 0
|
||||
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
local function utf8Prefix(text, chars)
|
||||
local result, count = {}, 0
|
||||
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||
if count >= chars then break end
|
||||
table.insert(result, char)
|
||||
count = count + 1
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
function ADStatusView.fit(text, chars)
|
||||
text = tostring(text or "")
|
||||
if ADStatusView.textLength(text) <= chars then return text end
|
||||
local ellipsis = ADStatusView.ELLIPSIS
|
||||
return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis
|
||||
end
|
||||
|
||||
-- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares.
|
||||
local function joinFitted(parts, flexIndex)
|
||||
local sep = ADStatusView.SEPARATOR
|
||||
local fixed = ADStatusView.textLength(sep) * (#parts - 1)
|
||||
for index, part in ipairs(parts) do
|
||||
if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end
|
||||
end
|
||||
local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed)
|
||||
local fitted = {}
|
||||
for index, part in ipairs(parts) do
|
||||
fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part
|
||||
end
|
||||
return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS)
|
||||
end
|
||||
|
||||
local function percentText(value)
|
||||
return string.format("%d %%", math.floor(value + 0.5))
|
||||
end
|
||||
|
||||
-- ---- linjer pr. sektion ----
|
||||
|
||||
local function flowText(entry)
|
||||
local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")}
|
||||
if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end
|
||||
return joinFitted(parts, 2)
|
||||
end
|
||||
|
||||
local function fieldText(entry)
|
||||
local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))}
|
||||
if entry.vehicle ~= nil and entry.vehicle ~= "" then
|
||||
table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS))
|
||||
end
|
||||
if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end
|
||||
return joinFitted(parts, 1)
|
||||
end
|
||||
|
||||
local function plainText(entry)
|
||||
return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS)
|
||||
end
|
||||
|
||||
local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText}
|
||||
|
||||
local function levelOf(entry, default)
|
||||
return LEVELS[entry.level] and entry.level or default
|
||||
end
|
||||
|
||||
-- Alle linjer uden loft.
|
||||
local function allLines(snapshot, textOf)
|
||||
local lines = {}
|
||||
for _, section in ipairs(SECTIONS) do
|
||||
local entries = snapshot[section.key] or {}
|
||||
if #entries > 0 then
|
||||
table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true})
|
||||
for _, entry in ipairs(entries) do
|
||||
table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false})
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden
|
||||
-- indhold lige før afkortningen droppes.
|
||||
local function capLines(lines, maxLines, textOf)
|
||||
if #lines <= maxLines then return lines end
|
||||
local kept = {}
|
||||
for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end
|
||||
while #kept > 0 and kept[#kept].header do table.remove(kept) end
|
||||
local hidden = 0
|
||||
for index = #kept + 1, #lines do
|
||||
if not lines[index].header then hidden = hidden + 1 end
|
||||
end
|
||||
local ok, text = pcall(string.format, textOf("spu_status_more"), hidden)
|
||||
table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false})
|
||||
return kept
|
||||
end
|
||||
|
||||
function ADStatusView.lines(snapshot, textOf, maxLines)
|
||||
snapshot = snapshot or {}
|
||||
textOf = textOf or function(key) return key end
|
||||
maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES))
|
||||
local lines = allLines(snapshot, textOf)
|
||||
if #lines == 0 then
|
||||
return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}}
|
||||
end
|
||||
return capLines(lines, maxLines, textOf)
|
||||
end
|
||||
@@ -108,3 +108,314 @@ 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
|
||||
return ADStorageStats.rankAttention(list, max)
|
||||
end
|
||||
|
||||
-- Kæde-forslag ligger ikke på en vare (ingen stat): navnet til den stabile rækkefølge tages fra teksten.
|
||||
local function entryName(entry)
|
||||
return tostring(entry.stat ~= nil and entry.stat.title or entry.text)
|
||||
end
|
||||
|
||||
function ADStorageStats.rankAttention(list, max)
|
||||
table.sort(list, function(a, b)
|
||||
if a.score ~= b.score then return a.score > b.score end
|
||||
return entryName(a) < entryName(b)
|
||||
end)
|
||||
local result = {}
|
||||
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
||||
return result
|
||||
end
|
||||
|
||||
-- "Kræver handling" fra kæden (ADChain.last): ÉT gult punkt "Kæde: N forslag" (klik -> fanen Kæder), så
|
||||
-- forslagene ikke fylder listen. Rangeres under alle røde punkter (>= 3000), øverst blandt de gule, over "sælg nu".
|
||||
ADStorageStats.CHAIN_SCORE = 2000
|
||||
-- Manglende led: ALLE kædeproblemer i ÉN rød post ("Kæder: N manglende led", klik -> fanen Kæder med kæderne
|
||||
-- foldet ud), så en gård med mange kæder (fx fermenteringssiloer) ikke fylder "Kræver handling". N = unikke
|
||||
-- (kæde, vare). Score CHAIN_PROBLEM_SCORE + alvor (0-99): under "rød fuld" (3000-3100), så lagerets røde poster
|
||||
-- altid vises først, og over de gule (<= 2000). Alvor = manglende andel af behovet; kredsløb = 99.
|
||||
ADStorageStats.CHAIN_PROBLEM_SCORE = 2900
|
||||
|
||||
local function severity(problems)
|
||||
local missing, need = 0, 0
|
||||
for _, problem in ipairs(problems) do
|
||||
if problem.kind == "cycle" then return 99 end
|
||||
missing, need = missing + (problem.detail or 0), need + (problem.need or 0)
|
||||
end
|
||||
if need <= 0 then return 99 end
|
||||
return math.floor(99 * math.min(1, missing / need))
|
||||
end
|
||||
|
||||
local function chainProblemEntries(last)
|
||||
local known = {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do known[root.key] = true end
|
||||
local problems, seen, rootKeys, rootSeen = {}, {}, {}, {}
|
||||
for _, problem in ipairs(last.chain ~= nil and last.chain.problems or {}) do
|
||||
if problem.rootKey ~= nil and known[problem.rootKey] then
|
||||
table.insert(problems, problem)
|
||||
seen[tostring(problem.rootKey) .. "|" .. tostring(problem.fillType)] = true
|
||||
if not rootSeen[problem.rootKey] then
|
||||
rootSeen[problem.rootKey] = true
|
||||
table.insert(rootKeys, problem.rootKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #problems == 0 then return {} end
|
||||
local count = 0
|
||||
for _ in pairs(seen) do count = count + 1 end
|
||||
table.sort(rootKeys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return {{kind = "chainProblem", level = "red", score = ADStorageStats.CHAIN_PROBLEM_SCORE + severity(problems), count = count,
|
||||
rootKeys = rootKeys, text = string.format("Kæder: %d", count)}}
|
||||
end
|
||||
|
||||
-- "Slukket men nødvendig" (ADChain.last.dependency.offNeeded): højst MAX_DEPENDENCY_ENTRIES post (den mest hastende
|
||||
-- fabrik; more = antal skjulte, vist som "+k"). Score DEPENDENCY_SCORE + hast (0-99, færre lagerdage = højere;
|
||||
-- ukendt = 50): over "rød fuld" (3000-3100), under "rød tom" (3900-4000).
|
||||
ADStorageStats.DEPENDENCY_SCORE = 3700
|
||||
ADStorageStats.MAX_DEPENDENCY_ENTRIES = 1
|
||||
|
||||
local function urgency(days)
|
||||
if days == nil then return 50 end
|
||||
if days == math.huge then return 0 end
|
||||
return math.max(0, 99 - math.min(99, math.floor(days)))
|
||||
end
|
||||
|
||||
function ADStorageStats.dependencyEntries(last)
|
||||
local entries = {}
|
||||
for _, item in ipairs(last ~= nil and last.dependency ~= nil and last.dependency.offNeeded or {}) do
|
||||
local name = tostring(item.buildingName or item.building or "?")
|
||||
table.insert(entries, {kind = "dependencyOff", level = "red", score = ADStorageStats.DEPENDENCY_SCORE + urgency(item.days),
|
||||
building = item.building, buildingName = name, fillType = item.fillType, onlyChain = item.onlyChain == true, text = name})
|
||||
end
|
||||
entries = ADStorageStats.rankAttention(entries)
|
||||
local result = {}
|
||||
for index = 1, math.min(#entries, ADStorageStats.MAX_DEPENDENCY_ENTRIES) do result[index] = entries[index] end
|
||||
if #entries > #result then result[#result].more = #entries - #result end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADStorageStats.chainEntries(last)
|
||||
if last == nil then return {} end
|
||||
local result = chainProblemEntries(last)
|
||||
for _, entry in ipairs(ADStorageStats.dependencyEntries(last)) do table.insert(result, entry) end
|
||||
local count = 0
|
||||
for _, action in ipairs(last.actions or {}) do
|
||||
if action.mode == "suggest" then count = count + 1 end
|
||||
end
|
||||
if count > 0 then
|
||||
table.insert(result, {kind = "chainSuggest", level = "yellow", score = ADStorageStats.CHAIN_SCORE, count = count,
|
||||
text = string.format("Kæde: %d", count)})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Varepunkterne (allerede rangeret) + kædens punkter, samme loft.
|
||||
function ADStorageStats.mergeAttention(attention, chainEntries, max)
|
||||
local list = {}
|
||||
for _, entry in ipairs(attention or {}) do table.insert(list, entry) end
|
||||
for _, entry in ipairs(chainEntries or {}) do table.insert(list, entry) end
|
||||
return ADStorageStats.rankAttention(list, max)
|
||||
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,89 @@
|
||||
-- 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
|
||||
|
||||
-- Kun læsning (statusvinduet): køretøjer der har holdt stille mindst minMs -> {{vehicle, ms}}, længst først.
|
||||
function ADStuckDiag.stillVehicles(minMs)
|
||||
local list = {}
|
||||
for vehicle, ms in pairs(stillMs) do
|
||||
if ms >= (minMs or ADStuckDiag.STILL_MS) then table.insert(list, {vehicle = vehicle, ms = ms}) end
|
||||
end
|
||||
table.sort(list, function(a, b) return a.ms > b.ms end)
|
||||
return list
|
||||
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,157 @@
|
||||
-- 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).
|
||||
|
||||
-- Lagerfanen for en bygningstype (TAB_KINDS), fx "factory" -> fanen Fabrikker.
|
||||
function SmartPickupFrame.tabOfKind(kind)
|
||||
for tab, tabKind in pairs(SmartPickupFrame.TAB_KINDS or {}) do
|
||||
if tabKind == kind then return tab end
|
||||
end
|
||||
return SmartPickupFrame.TAB_OVERVIEW
|
||||
end
|
||||
|
||||
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)
|
||||
-- kæde-forslag og manglende led har ingen vare (stat): klik fører til fanen Kæder
|
||||
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then return "chain", nil end
|
||||
-- slukket men nødvendig: fanen Fabrikker (Kæder når kun en kæde bruger varen)
|
||||
if entry ~= nil and entry.kind == "dependencyOff" then return entry.onlyChain and "chain" or "factories", nil end
|
||||
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 == "chain" then return self.i18n:getText("spu_attention_hintChain") end
|
||||
if advice == "factories" then return self.i18n:getText("spu_attention_hintFactories") end
|
||||
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)
|
||||
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then
|
||||
-- manglende led: kæderne med problemer foldes ud, så problemteksten står under den røde linje
|
||||
if entry.kind == "chainProblem" then
|
||||
frame.chainExpanded = frame.chainExpanded or {}
|
||||
for _, rootKey in ipairs(entry.rootKeys or {}) do frame.chainExpanded[rootKey] = true end
|
||||
end
|
||||
frame:switchTab(SmartPickupFrame.TAB_CHAINS)
|
||||
return
|
||||
end
|
||||
if entry ~= nil and entry.kind == "dependencyOff" then
|
||||
frame:switchTab(entry.onlyChain and SmartPickupFrame.TAB_CHAINS or SmartPickupFrame.tabOfKind("factory"))
|
||||
return
|
||||
end
|
||||
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,142 @@
|
||||
-- Smart Pickup — fanen Kæder
|
||||
-- Produktionskæderne som træ: rod (produktion spilleren har tændt) -> varer -> råvarer, med kilde, behov pr.
|
||||
-- måned og flowstatus; afgrødeforslag kan godkendes/afvises og en rod sættes på pause. Rækkerne bygges af
|
||||
-- ADChainView (testet); her kun kloning, farve og klik. Et resultat yngre end CHAIN_MAX_AGE_MS (fra kædens eget
|
||||
-- tick eller en tidligere visning) genbruges; ellers evalueres (visning, handler ikke). Klik evaluerer altid.
|
||||
|
||||
SmartPickupFrame.CHAIN_MAX_AGE_MS = 30000
|
||||
|
||||
function SmartPickupFrame:chainLast(adEnv, force)
|
||||
local age = ADChain.lastAge ~= nil and ADChain.lastAge() or math.huge
|
||||
if force or ADChain.last == nil or age >= SmartPickupFrame.CHAIN_MAX_AGE_MS then
|
||||
ADChain.evaluate(adEnv)
|
||||
end
|
||||
return ADChain.last
|
||||
end
|
||||
|
||||
function SmartPickupFrame:chainTitle(last, name)
|
||||
local title = last ~= nil and last.titles ~= nil and last.titles[name] or nil
|
||||
return title or self:fruitTitle(name)
|
||||
end
|
||||
|
||||
-- Klik på en kæde-knap: udfør, evaluér straks (ny visning) og genopbyg. Et forslag der ikke længere findes
|
||||
-- (approve/dismiss -> false) meldes i statuslinjen som i "Kræver handling".
|
||||
function SmartPickupFrame:onChainAction(action)
|
||||
local ok = true
|
||||
if action.kind == "approve" then
|
||||
ok = ADChain.approve(action.fieldId)
|
||||
elseif action.kind == "dismiss" then
|
||||
ok = ADChain.dismiss(action.fieldId)
|
||||
elseif action.kind == "optIn" or action.kind == "optOut" then
|
||||
ADChain.setOptIn(action.rootKey, action.kind == "optIn")
|
||||
elseif action.kind == "finalMode" then
|
||||
ADChain.setFinalKeep(action.rootKey, action.keep)
|
||||
elseif action.kind == "pause" or action.kind == "resume" then
|
||||
ADChain.setPaused(action.rootKey, action.kind == "pause")
|
||||
end
|
||||
if ok == false then
|
||||
local text = self.i18n:getText("spu_chain_gone")
|
||||
self.statusOverride, self.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
|
||||
end
|
||||
local adEnv = self.getAdEnv()
|
||||
if adEnv ~= nil then self:chainLast(adEnv, true) end
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Klik på en varelinje: åbn flowet der kører varen til modtageren (kædens eget flow til bygningen, ellers et flow
|
||||
-- der dækker varen) i Flows-fanens redigeringsvindue; uden flow en besked.
|
||||
function SmartPickupFrame:openChainFlow(consumer, fillType)
|
||||
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||
local runId = ADChainActions.chainRunFor(runs, consumer)
|
||||
if runId == nil then
|
||||
for _, run in ipairs(runs) do
|
||||
if runId == nil and ADChainActions.isCovered({consumer = consumer, fillType = fillType}, {run}) then runId = run.id end
|
||||
end
|
||||
end
|
||||
if runId == nil then
|
||||
self.statusOverride = self.i18n:getText("spu_chain_noFlow")
|
||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||
self:requestRebuild()
|
||||
return
|
||||
end
|
||||
self.selectedRunId = runId
|
||||
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||
self:openEditPanel("flow", runId)
|
||||
end
|
||||
|
||||
-- Klik på rodens linje uden for knappen: fold ud/sammen (huskes kun mens spillet kører).
|
||||
function SmartPickupFrame:toggleChainRoot(rootKey)
|
||||
self.chainExpanded = self.chainExpanded or {}
|
||||
self.chainExpanded[rootKey] = not self.chainExpanded[rootKey] or nil
|
||||
self:requestRebuild()
|
||||
end
|
||||
|
||||
-- Hvilken celle blev der klikket i? Højeste celle hvis venstre kant ligger til venstre for klikket.
|
||||
local function clickedCell(frame, cells)
|
||||
if frame.lastClickX == nil then return nil end
|
||||
for index = #cells, 1, -1 do
|
||||
local cell = cells[index]
|
||||
if cell ~= nil and cell.absPosition ~= nil and frame.lastClickX >= cell.absPosition[1] then return index end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Række med prik + c1..c5 (flowListPrefab). Knapper er celler (entry.actions[celle], som [Start]/[Stop] i
|
||||
-- Flows-listen); et klik andetsteds på en rodlinje folder den ud/sammen.
|
||||
function SmartPickupFrame:addChainRow(entry)
|
||||
local unpackFn = table.unpack or unpack
|
||||
local row = self:addRow(self.flowListPrefab, {kind = "chainRow"})
|
||||
local cells = {}
|
||||
for index = 1, 5 do cells[index] = row:getDescendantByName("c" .. index) end
|
||||
SmartPickupFrame.setCells(cells, entry.cells)
|
||||
local dot = row:getDescendantByName("dot")
|
||||
local color = entry.level ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
|
||||
if dot ~= nil and dot.setImageColor ~= nil and color ~= nil then dot:setImageColor(nil, unpackFn(color)) end
|
||||
-- kun rod og varer har en status-prik (overskrift, forslag og auto-skift har ingen)
|
||||
if dot ~= nil and dot.setVisible ~= nil and entry.kind ~= "root" and entry.kind ~= "node" then dot:setVisible(false) end
|
||||
if cells[4] ~= nil and cells[4].setTextColor ~= nil and color ~= nil and entry.level ~= "running" and entry.kind ~= "suggest" then
|
||||
cells[4]:setTextColor(unpackFn(color))
|
||||
end
|
||||
local actions, toggle = entry.actions or {}, entry.toggle
|
||||
if next(actions) == nil and toggle == nil and entry.openConsumer ~= nil then
|
||||
self:makeRowClickable(row, function(frame) frame:openChainFlow(entry.openConsumer, entry.openFillType) end)
|
||||
elseif next(actions) ~= nil or toggle ~= nil then
|
||||
self:makeRowClickable(row, function(frame)
|
||||
local action = actions[clickedCell(frame, cells) or 0]
|
||||
if action ~= nil then
|
||||
frame:onChainAction(action)
|
||||
elseif toggle ~= nil then
|
||||
frame:toggleChainRoot(toggle)
|
||||
end
|
||||
end)
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
-- Manglende led: problemteksten som (ombrudt) tekstrække i rød under varelinjen.
|
||||
function SmartPickupFrame:addChainProblemRow(entry)
|
||||
local unpackFn = table.unpack or unpack
|
||||
local color = ADFlowView.LEVEL_COLORS ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
|
||||
for _, line in ipairs(ADFlowView.wrapText(entry.text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||
title:setText(line)
|
||||
if color ~= nil and title.setTextColor ~= nil then title:setTextColor(unpackFn(color)) end
|
||||
end
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillChainRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
local last = self:chainLast(adEnv, false)
|
||||
local rows = ADChainView.rows(last, function(name) return self:chainTitle(last, name) end,
|
||||
SmartPickupFrame.formatCompact, function(key) return i18n:getText(key) end, self.chainExpanded or {})
|
||||
for _, entry in ipairs(rows) do
|
||||
if entry.kind == "info" or entry.kind == "empty" then
|
||||
self:addTextRow(entry.text)
|
||||
elseif entry.kind == "problem" then
|
||||
self:addChainProblemRow(entry)
|
||||
else
|
||||
self:addChainRow(entry)
|
||||
end
|
||||
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,44 +1,100 @@
|
||||
-- Smart Pickup — fanen Marker
|
||||
-- Tabel over gårdens slåafgrøde-marker (Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner); klik på
|
||||
-- en række viser markens detaljer under tabellen (plan, kørsel, riv, tving lager, marksiloer). Nederst
|
||||
-- markflåden: foreslåede sæt med flueben. Al logik ligger i ADFieldJobs/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 = {
|
||||
showAllFields = true, fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = 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.ROLE_KEYS = {mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake", collector = "spu_field_role_collector"}
|
||||
|
||||
-- ---- tekster ----
|
||||
|
||||
function SmartPickupFrame:roleText(role)
|
||||
return self.i18n:getText(SmartPickupFrame.ROLE_KEYS[role] or "spu_field_role_mower")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldReasonText(reason)
|
||||
if type(reason) ~= "table" then return nil end
|
||||
if reason.text ~= nil then return reason.text end
|
||||
local role = reason.role ~= nil and self.i18n:getText(SmartPickupFrame.ROLE_KEYS[reason.role] or "spu_field_role_mower") or ""
|
||||
local first = reason.name or (reason.role ~= nil and self:roleText(reason.role)) or ""
|
||||
local template = self.i18n:getText(reason.key)
|
||||
local args = reason.args or {}
|
||||
local ok, text = pcall(string.format, template, role, args[1] or 0, args[2] or 0)
|
||||
local ok, text = pcall(string.format, template, first, args[1] or 0, args[2] or 0)
|
||||
return ok and text or template
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldStatusText(field, info)
|
||||
function SmartPickupFrame:soilText(info)
|
||||
local texts = {}
|
||||
for _, part in ipairs(ADFieldView.soilParts(info)) do
|
||||
table.insert(texts, self.i18n:getText("spu_field_need_" .. part))
|
||||
end
|
||||
return table.concat(texts, ", ")
|
||||
end
|
||||
|
||||
-- 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 (field.stage == "idle" or field.stage == "ready") and info.mowable then
|
||||
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
|
||||
local decision = ADFieldJobs.lastDecision[info.id]
|
||||
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:fieldPlanText(field)
|
||||
local text = self.i18n:getText(field.plan.product == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage")
|
||||
if field.plan.product == "hay" and field.plan.rake then
|
||||
text = text .. " + " .. self.i18n:getText("spu_field_plan_rake")
|
||||
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
|
||||
if (field.plan.rigs or 1) > 1 then text = string.format("%s (x%d)", text, field.plan.rigs) end
|
||||
return text
|
||||
end
|
||||
|
||||
@@ -48,12 +104,15 @@ 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 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, info.cropTitle or "-", info.mowable and self:fieldPlanText(field) or "-",
|
||||
name, crop, self:fieldPlanText(field, info),
|
||||
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
||||
info.mowable and self:fieldStatusText(field, info) or self.i18n:getText("spu_field_notMowable"),
|
||||
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
|
||||
|
||||
@@ -61,188 +120,280 @@ end
|
||||
|
||||
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"))
|
||||
local hidden = ADFieldView.hiddenCount(infos, self.showAllFields == true)
|
||||
if hidden > 0 or self.showAllFields then
|
||||
self:addToggleRow("showAllFields", string.format(i18n:getText("spu_field_showAll"), hidden), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, self.showAllFields == true)
|
||||
-- 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.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")})
|
||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields, self.showAllFields == true)) do
|
||||
local cells = self:addCellRow(self.fieldPrefab, self:fieldCells(row))
|
||||
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.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)
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, tostring(info.cropTitle or "-")))
|
||||
if not info.mowable then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_notMowable"))
|
||||
return
|
||||
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
|
||||
local isHay = field.plan.product == "hay"
|
||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, field.plan.mode == "auto")
|
||||
if isHay then
|
||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, field.plan.rake == true)
|
||||
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
|
||||
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))
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_guide_noMarker"), info.id))
|
||||
end
|
||||
if field.mowWidth ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_mowWidth"), field.mowWidth))
|
||||
end
|
||||
self:fillFieldStoreRows(adEnv, info, field)
|
||||
-- marksilo-advarsler vises ikke her: de gælder ikke marken, og Field Silos-fanen tømmer marksiloerne selv
|
||||
end
|
||||
|
||||
-- Tving lager: siloer der tager markens vare (skår ved ensilage, tørskår ved hø); valgte først.
|
||||
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
||||
function SmartPickupFrame:rigDetails(rig)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_field_section_store"))
|
||||
local fillType = field.plan.product == "hay" and info.hay or info.windrow
|
||||
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 count = 0
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
if building.kind == "silo" and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})) 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 ""
|
||||
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("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)))
|
||||
local parts = {}
|
||||
if rig.role == "harvester" then
|
||||
local count = 0
|
||||
for _ in pairs(rig.fruits or {}) do count = count + 1 end
|
||||
table.insert(parts, string.format(i18n:getText("spu_field_rig_crops"), count))
|
||||
elseif rig.role == "seeder" then
|
||||
table.insert(parts, i18n:getText(rig.hasSeeds and "spu_field_rig_seeds" or "spu_field_rig_noSeeds"))
|
||||
elseif rig.role == "weeder" then
|
||||
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
||||
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(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)
|
||||
end)
|
||||
for _, rig in ipairs(rigs) do
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = rig.id})
|
||||
local title = string.format(" %s — %s, %.1f m", tostring(rig.label), i18n:getText(SmartPickupFrame.ROLE_KEYS[rig.role]), rig.width or 0)
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then
|
||||
title = title .. " — " .. i18n:getText("spu_field_tooWideShort")
|
||||
end
|
||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||
if assignment ~= nil then
|
||||
title = title .. string.format(" — F%d", assignment.fieldId)
|
||||
end
|
||||
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), ADFlowView.shortVehicleName(tractor.arg))
|
||||
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", ADFlowView.shortVehicleName(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), ADFlowView.shortVehicleName(implement.arg))
|
||||
self:addTextRow(string.format(" %s — %s: %s", ADFlowView.shortVehicleName(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, true)) do rowsById[row.info.id] = row end
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
|
||||
local working, total = 0, 0
|
||||
for _, entry in ipairs(self.fieldRows or {}) do
|
||||
local row = rowsById[entry.id]
|
||||
if row ~= nil then
|
||||
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
||||
if row.info.mowable then total = total + 1 end
|
||||
if ADFieldModel.isWorking(row.field) then working = working + 1 end
|
||||
total = total + 1
|
||||
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")
|
||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or not info.mowable
|
||||
or (not isWorking and (info.markerId == nil or (info.readyRatio or 0) < ADFieldModel.CUT_RATIO))
|
||||
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 == "showAllFields" then
|
||||
frame.showAllFields = isOn
|
||||
elseif data.kind == "fleetVehicle" then
|
||||
if data.kind == "fleetVehicle" then
|
||||
ADFieldJobs.toggleFleet(data.vehicleId)
|
||||
elseif id == nil then
|
||||
return
|
||||
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 data.kind == "fieldRake" then
|
||||
ADFieldJobs.setPlan(id, {rake = 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)
|
||||
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 field.stage ~= "ready" then
|
||||
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)
|
||||
end
|
||||
ADFieldJobs.requestStart(id)
|
||||
@@ -252,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,498 @@
|
||||
-- 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"))
|
||||
local function addFlowRow(run)
|
||||
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
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
local runs, idle = group.runs, {}
|
||||
-- kædeflows uden traktor (venter på en ledig traktor/vogn) foldes sammen (in-game 30/9)
|
||||
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
|
||||
for _, run in ipairs(runs) do addFlowRow(run) end
|
||||
if #idle > 0 then
|
||||
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
|
||||
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
|
||||
frame.showIdleChainFlows = not frame.showIdleChainFlows
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
if self.showIdleChainFlows then
|
||||
for _, run in ipairs(idle) do addFlowRow(run) end
|
||||
end
|
||||
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(ADFlowView.listName(run))
|
||||
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, row.sellAtPercent == 0 and i18n:getText("spu_flow_ruleSellAlways") or 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")
|
||||
@@ -60,7 +64,9 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
||||
elseif row.reason ~= nil then
|
||||
status = row.reason
|
||||
end
|
||||
local name = (run.id == self.selectedRunId and "> " or "") .. tostring(run.name)
|
||||
-- kædeflow uden traktor: sig hvorfor det står stille i stedet for flowets regel
|
||||
if ADFlowView.isIdleChain(run) then status = i18n:getText("spu_flow_chainIdle") end
|
||||
local name = (run.id == self.selectedRunId and "> " or "") .. ADFlowView.listName(run)
|
||||
return {name, products, from, to, string.format("%d/%d", row.running, row.total), status}
|
||||
end
|
||||
|
||||
@@ -80,8 +86,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
|
||||
@@ -95,15 +105,29 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_overview_hint"))
|
||||
self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")})
|
||||
local function addBoardRow(run)
|
||||
local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
local runId = run.id
|
||||
self:makeRowClickable(cells[1].parent, function(frame) frame:openFlow(runId) end)
|
||||
end
|
||||
table.insert(self.overviewRows, {runId = run.id, cells = cells})
|
||||
end
|
||||
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 cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
local runId = run.id
|
||||
self:makeRowClickable(cells[1].parent, function(frame) frame:openFlow(runId) end)
|
||||
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
local runs, idle = group.runs, {}
|
||||
-- kædeflows uden traktor foldes sammen som i Flows-listen (in-game 30/9)
|
||||
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
|
||||
for _, run in ipairs(runs) do addBoardRow(run) end
|
||||
if #idle > 0 then
|
||||
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
|
||||
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
|
||||
frame.showIdleChainFlows = not frame.showIdleChainFlows
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
if self.showIdleChainFlows then
|
||||
for _, run in ipairs(idle) do addBoardRow(run) end
|
||||
end
|
||||
table.insert(self.overviewRows, {runId = run.id, cells = cells})
|
||||
end
|
||||
end
|
||||
if #ADRunsController.store.runs == 0 then
|
||||
@@ -137,115 +161,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,32 @@ 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
|
||||
-- Kæder-fanen (gui/SmartPickupChainTab.lua): klikbare forslag/pause, så genopbygning sjældent (som Marksiloer)
|
||||
SmartPickupFrame.TAB_CHAINS = 10
|
||||
SmartPickupFrame.CHAIN_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", "spu_tab_chains"}
|
||||
-- Visningsrækkefølge (fane-id'erne ovenfor er faste og bruges overalt): Kæder står efter Marker (in-game 30/9).
|
||||
SmartPickupFrame.TAB_DISPLAY = {1, 2, 3, 10, 4, 5, 6, 7, 8, 9}
|
||||
function SmartPickupFrame.tabPosition(tabId)
|
||||
for position, id in ipairs(SmartPickupFrame.TAB_DISPLAY) do
|
||||
if id == tabId then return position end
|
||||
end
|
||||
return tabId
|
||||
end
|
||||
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 +47,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 +65,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 +97,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()
|
||||
@@ -81,8 +107,9 @@ function SmartPickupFrame:initialize()
|
||||
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
|
||||
self.selectorPrefab:unlinkElement()
|
||||
FocusManager:removeElement(self.selectorPrefab)
|
||||
for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do
|
||||
self.subCategoryPaging:addText(tostring(index))
|
||||
for position, index in ipairs(SmartPickupFrame.TAB_DISPLAY) do
|
||||
local key = SmartPickupFrame.TAB_NAMES[index]
|
||||
self.subCategoryPaging:addText(tostring(position))
|
||||
local tab = self.selectorPrefab:clone(self.subCategoryBox)
|
||||
FocusManager:loadElementFromCustomValues(tab)
|
||||
tab:setText(self.i18n:getText(key))
|
||||
@@ -197,6 +224,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,12 +273,17 @@ 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
|
||||
for index, tab in pairs(self.subCategoryTabs) do
|
||||
tab:setSelected(index == tabIndex)
|
||||
end
|
||||
if self.subCategoryPaging ~= nil then
|
||||
self.subCategoryPaging:setState(tabIndex, false)
|
||||
self.subCategoryPaging:setState(SmartPickupFrame.tabPosition(tabIndex), false)
|
||||
end
|
||||
self:requestRebuild()
|
||||
self:safe(function(frame)
|
||||
@@ -256,14 +291,16 @@ function SmartPickupFrame:switchTab(tabIndex)
|
||||
end)
|
||||
end
|
||||
|
||||
-- pilene/paging giver en position i visningsrækkefølgen
|
||||
function SmartPickupFrame:onClickTab(state)
|
||||
self:switchTab(state)
|
||||
self:switchTab(SmartPickupFrame.TAB_DISPLAY[state] or state)
|
||||
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 +349,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 +366,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 +379,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 +463,45 @@ 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_CHAINS then
|
||||
if adEnv ~= nil then
|
||||
self:fillChainRows(adEnv)
|
||||
end
|
||||
self.statusRow = self:addTextRow(self.statusOverride or "")
|
||||
self.monthRebuildTicks = SmartPickupFrame.CHAIN_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 +515,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 +534,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 +628,20 @@ 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
|
||||
or self.currentTab == SmartPickupFrame.TAB_CHAINS 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 +662,24 @@ 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
|
||||
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||
-- "forslaget gælder ikke længere" står STATUS_OVERRIDE_TICKS sekunder
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
end
|
||||
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 +687,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 +703,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 +724,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 +760,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 +793,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 +825,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 +865,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 +913,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,24 @@ 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
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil or data.kind ~= "product" then return end
|
||||
@@ -207,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,354 @@
|
||||
-- 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
|
||||
|
||||
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet
|
||||
-- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS,
|
||||
-- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks.
|
||||
function SmartPickupFrame:attentionFor(stats)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then
|
||||
local okEnv, adEnv = pcall(self.getAdEnv)
|
||||
if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end
|
||||
end
|
||||
if ADChain == nil or ADChain.last == nil then return attention end
|
||||
return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:alertText(entry)
|
||||
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
||||
if entry.kind == "chainSuggest" then
|
||||
return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0)
|
||||
end
|
||||
if entry.kind == "chainProblem" then
|
||||
return string.format(i18n:getText("spu_attention_chainProblems"), entry.count or 0)
|
||||
end
|
||||
if entry.kind == "dependencyOff" then
|
||||
if (entry.more or 0) > 0 then
|
||||
return string.format(i18n:getText("spu_attention_depOffMore"), tostring(entry.buildingName), entry.more)
|
||||
end
|
||||
return string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName))
|
||||
end
|
||||
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 = self:attentionFor(stats)
|
||||
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 = self:attentionFor(stats)
|
||||
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,9 +75,46 @@
|
||||
<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_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m"/>
|
||||
<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"/>
|
||||
<text name="spu_field_role_rake" text="river"/>
|
||||
@@ -65,35 +126,150 @@
|
||||
<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 høet sammen før opsamling"/>
|
||||
<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_tooWideShort" text="!! over 50 m"/>
|
||||
<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="kursus 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"/>
|
||||
<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_windrowOffset" text=" Riven lægger skåret %.1f m %s — læssevognen forskydes tilsvarende"/>
|
||||
<text name="spu_field_side_center" text="i midten"/>
|
||||
<text name="spu_field_side_left" text="til venstre"/>
|
||||
<text name="spu_field_side_right" text="til højre"/>
|
||||
<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"/>
|
||||
<text name="spu_field_chainFree" text="Fri til kæden (moden må skifte afgrøde)"/>
|
||||
<text name="spu_field_mulch" text="Stubbehakning efter høst"/>
|
||||
<text name="spu_field_need_fert" text="gødning"/>
|
||||
<text name="spu_field_need_lime" text="kalk"/>
|
||||
<text name="spu_field_need_plow" text="pløjning"/>
|
||||
<text name="spu_field_need_weed" text="ukrudt"/>
|
||||
<text name="spu_field_plan_harvest" text="Høst"/>
|
||||
<text name="spu_field_plan_prep" text="Klargøring"/>
|
||||
<text name="spu_field_plan_sow" text="så"/>
|
||||
<text name="spu_field_reason_cannotDo" text="ingen %s i markflåden kan tage opgaven (afgrøde/frø/ukrudt)"/>
|
||||
<text name="spu_field_reason_noSowFruit" text="vælg en afgrøde at så"/>
|
||||
<text name="spu_field_reason_notEnough" text="multi-tool: %s — kun %d af %d ledige"/>
|
||||
<text name="spu_field_reason_notPlantable" text="%s kan ikke sås i denne periode"/>
|
||||
<text name="spu_field_rig_crops" text="%d afgrøder"/>
|
||||
<text name="spu_field_rig_herbicide" text="sprøjte"/>
|
||||
<text name="spu_field_rig_mechanical" text="strigle (småt ukrudt)"/>
|
||||
<text name="spu_field_rig_noSeeds" text="!! tom for frø"/>
|
||||
<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ø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"/>
|
||||
<text name="spu_field_role_plow" text="plov"/>
|
||||
<text name="spu_field_role_roller" text="tromle"/>
|
||||
<text name="spu_field_role_seeder" text="såmaskine"/>
|
||||
<text name="spu_field_role_unloader" text="aflæsningsvogn"/>
|
||||
<text name="spu_field_role_weeder" text="ukrudtsmaskine"/>
|
||||
<text name="spu_field_roll" text="Tromle efter såning"/>
|
||||
<text name="spu_field_section_after" text="— Efter høst —"/>
|
||||
<text name="spu_field_section_care" text="— Pleje mens afgrøden vokser —"/>
|
||||
<text name="spu_field_section_prep" text="— Klargøring —"/>
|
||||
<text name="spu_field_soil" text="Jordbehandling"/>
|
||||
<text name="spu_field_soilOk" text="intet"/>
|
||||
<text name="spu_field_soilState" text="Jorden trænger til: %s"/>
|
||||
<text name="spu_field_soil_auto" text="Auto (pløj når nødvendigt)"/>
|
||||
<text name="spu_field_soil_cultivate" text="Kultivér"/>
|
||||
<text name="spu_field_soil_none" text="Ingen"/>
|
||||
<text name="spu_field_soil_plow" text="Pløj"/>
|
||||
<text name="spu_field_sow" text="Så bagefter"/>
|
||||
<text name="spu_field_sowFruit" text="Afgrøde at så"/>
|
||||
<text name="spu_field_sowSame" text="Samme som høstet"/>
|
||||
<text name="spu_field_stage_careLiming" text="Kalkes"/>
|
||||
<text name="spu_field_stage_cultivating" text="Kultiveres"/>
|
||||
<text name="spu_field_stage_fertilizing" text="Gødes"/>
|
||||
<text name="spu_field_stage_harvesting" text="Høstes"/>
|
||||
<text name="spu_field_stage_liming" text="Kalkes"/>
|
||||
<text name="spu_field_stage_mulching" text="Stub hakkes"/>
|
||||
<text name="spu_field_stage_plowing" text="Pløjes"/>
|
||||
<text name="spu_field_stage_rolling" text="Tromles"/>
|
||||
<text name="spu_field_stage_sowing" text="Sås"/>
|
||||
<text name="spu_field_stage_weeding" text="Ukrudt bekæmpes"/>
|
||||
<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_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)"/>
|
||||
@@ -134,9 +310,12 @@
|
||||
<text name="spu_policy_sell" text="Salg"/>
|
||||
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
|
||||
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
|
||||
<text name="spu_sellAt_always" text="Altid (uanset pris)"/>
|
||||
<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."/>
|
||||
@@ -146,6 +325,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)"/>
|
||||
@@ -171,14 +351,18 @@
|
||||
<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_ruleSellAlways" text="sælg altid"/>
|
||||
<text name="spu_flow_chainIdle" text="mangler traktor/vogn"/>
|
||||
<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"/>
|
||||
@@ -196,6 +380,9 @@
|
||||
<text name="spu_col_to" text="Til"/>
|
||||
<text name="spu_col_tractors" text="Kører"/>
|
||||
<text name="spu_col_status" text="Status"/>
|
||||
<text name="spu_board_chainIdleShow" text="%d kædeflows venter på en ledig traktor/vogn — klik for at vise"/>
|
||||
<text name="spu_board_chainIdleHide" text="%d kædeflows uden traktor — klik for at skjule"/>
|
||||
<text name="spu_board_chain" text="· Kædernes egne flows (oprettes af fanen Kæder) ·"/>
|
||||
<text name="spu_board_supply" text="· Ind til bygning ·"/>
|
||||
<text name="spu_board_outbound" text="· Ud fra bygning ·"/>
|
||||
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
|
||||
@@ -218,5 +405,257 @@
|
||||
<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_sumSellAlways" text="sælg altid"/>
|
||||
<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"/>
|
||||
<text name="spu_tab_chains" text="Kæder"/>
|
||||
<text name="spu_chain_noFlow" text="Intet flow kører den vare endnu — kæden opretter det, når der er en ledig traktor/vogn"/>
|
||||
<text name="spu_chain_optIn" text="[Kæde til]"/>
|
||||
<text name="spu_chain_optOut" text="[Fjern]"/>
|
||||
<text name="spu_chain_notChosen" text="ingen kæde"/>
|
||||
<text name="spu_chain_candidatesShow" text="%d tændte produktioner uden kæde — klik for at vise"/>
|
||||
<text name="spu_chain_candidatesHide" text="%d tændte produktioner uden kæde — klik for at skjule"/>
|
||||
<text name="spu_chain_finalFarm" text="Gård"/>
|
||||
<text name="spu_chain_finalSell" text="Sælg"/>
|
||||
<text name="spu_chain_finalKeep" text="Behold"/>
|
||||
<text name="spu_chain_suggestHeader" text="Forslag til ny afgrøde"/>
|
||||
<text name="spu_chain_empty" text="Ingen kæder: slå et produkt til i en fabrik"/>
|
||||
<text name="spu_chain_col_item" text="Vare"/>
|
||||
<text name="spu_chain_col_source" text="Kilde"/>
|
||||
<text name="spu_chain_col_perMonth" text="l/md"/>
|
||||
<text name="spu_chain_col_status" text="Status"/>
|
||||
<text name="spu_chain_col_stock" text="Lager"/>
|
||||
<text name="spu_chain_approve" text="Forslag [Godkend]"/>
|
||||
<text name="spu_chain_dismiss" text="[Afvis]"/>
|
||||
<text name="spu_chain_auto" text="skiftet (fri mark)"/>
|
||||
<text name="spu_chain_gone" text="Forslaget gælder ikke længere"/>
|
||||
<text name="spu_chain_flowOk" text="flow kører"/>
|
||||
<text name="spu_chain_flowMissing" text="mangler traktor/vogn"/>
|
||||
<text name="spu_chain_flowNone" text="intet flow"/>
|
||||
<text name="spu_chain_flowAuto" text="fordeles af spillet"/>
|
||||
<text name="spu_chain_flowPallet" text="palle: vælg UAL-vogn"/>
|
||||
<text name="spu_chain_flowStopped" text="flow stoppet"/>
|
||||
<text name="spu_chain_bottleneck" text="flaskehals: -%s/md"/>
|
||||
<text name="spu_chain_buy" text="skal købes: %s/md"/>
|
||||
<text name="spu_chain_cycle" text="kredsløb"/>
|
||||
<text name="spu_chain_fieldShort" text="mangler %s ha"/>
|
||||
<text name="spu_chain_tight" text="lager < 3 md"/>
|
||||
<text name="spu_chain_noField" text="ingen mark"/>
|
||||
<text name="spu_chain_buySource" text="køb"/>
|
||||
<text name="spu_chain_fromFields" text="fra marker"/>
|
||||
<text name="spu_chain_notifyMore" text="+%d manglende led — se fanen Kæder"/>
|
||||
<text name="spu_chain_pause" text="[Pause]"/>
|
||||
<text name="spu_chain_resume" text="[Start]"/>
|
||||
<text name="spu_chain_rootOk" text="dækket"/>
|
||||
<text name="spu_chain_rootError" text="mangel"/>
|
||||
<text name="spu_chain_rootWaiting" text="venter"/>
|
||||
<text name="spu_chain_rootPaused" text="på pause"/>
|
||||
<text name="spu_chain_rootSummary" text="%s · %d forslag"/>
|
||||
<text name="spu_chain_free" text="Marker fri til kæden: %s"/>
|
||||
<text name="spu_chain_switches" text="Automatiske skift siden indlæsning: %d"/>
|
||||
<text name="spu_chain_scanning" text="Marker scannes endnu – kæden skifter ingen marker før alle er set"/>
|
||||
<text name="spu_attention_chainSuggests" text="Kæde: %d forslag"/>
|
||||
<text name="spu_attention_hintChain" text=" → klik: se Kæder"/>
|
||||
<text name="spu_chain_problem_buy" text="Ingen af dine fabrikker laver %s — byg en eller køb"/>
|
||||
<text name="spu_chain_problem_barn" text="Dine stalde leverer ikke nok %s — køb resten eller flere dyr"/>
|
||||
<text name="spu_chain_problem_bottleneck" text="%s kan kun lave %s af %s/md"/>
|
||||
<text name="spu_chain_problem_off" text="%s (%s) er slukket af dig — tænd den eller sæt kæden på pause"/>
|
||||
<text name="spu_chain_problem_cycle" text="Kredsløb omkring %s — kæden kan ikke beregnes"/>
|
||||
<text name="spu_attention_chainProblems" text="Kæder: %d manglende led"/>
|
||||
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: statusvindue"/>
|
||||
<text name="spu_status_flows" text="Flows"/>
|
||||
<text name="spu_status_fields" text="Marker"/>
|
||||
<text name="spu_status_chains" text="Kæder"/>
|
||||
<text name="spu_status_warnings" text="Advarsler"/>
|
||||
<text name="spu_status_more" text="+%d mere"/>
|
||||
<text name="spu_status_idle" text="Smart Pickup: intet kører lige nu"/>
|
||||
<text name="spu_status_act_fetch" text="henter %s ved %s"/>
|
||||
<text name="spu_status_act_driving" text="kører til %s"/>
|
||||
<text name="spu_status_act_waiting" text="venter ved %s"/>
|
||||
<text name="spu_status_act_holding" text="holder ved %s"/>
|
||||
<text name="spu_status_act_fetchWagon" text="henter vogn"/>
|
||||
<text name="spu_status_act_waitWagon" text="venter på vogn"/>
|
||||
<text name="spu_status_act_emptying" text="tømmer vognen"/>
|
||||
<text name="spu_status_act_homing" text="på vej hjem"/>
|
||||
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
|
||||
<text name="spu_dep_warn" text="Advarsel: du slukkede %s (%s) — %s bruger det. Lageret rækker ca. %d dage."/>
|
||||
<text name="spu_dep_warnNoDays" text="Advarsel: du slukkede %s (%s) — %s bruger det."/>
|
||||
<text name="spu_attention_depOff" text="Slukket men nødvendig: %s"/>
|
||||
<text name="spu_attention_depOffMore" text="Slukket men nødvendig: %s +%d"/>
|
||||
<text name="spu_attention_hintFactories" text=" → klik: se Fabrikker"/>
|
||||
</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,9 +75,46 @@
|
||||
<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_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m"/>
|
||||
<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"/>
|
||||
<text name="spu_field_role_rake" text="rake"/>
|
||||
@@ -65,35 +126,150 @@
|
||||
<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 hay before collecting"/>
|
||||
<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_tooWideShort" text="!! over 50 m"/>
|
||||
<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="course 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"/>
|
||||
<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_windrowOffset" text=" The rake drops the swath %.1f m %s — the loading wagon is offset to match"/>
|
||||
<text name="spu_field_side_center" text="in the middle"/>
|
||||
<text name="spu_field_side_left" text="to the left"/>
|
||||
<text name="spu_field_side_right" text="to the right"/>
|
||||
<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"/>
|
||||
<text name="spu_field_chainFree" text="Free for chains (mod may change the crop)"/>
|
||||
<text name="spu_field_mulch" text="Mulch stubble after harvest"/>
|
||||
<text name="spu_field_need_fert" text="fertilizer"/>
|
||||
<text name="spu_field_need_lime" text="lime"/>
|
||||
<text name="spu_field_need_plow" text="plowing"/>
|
||||
<text name="spu_field_need_weed" text="weeds"/>
|
||||
<text name="spu_field_plan_harvest" text="Harvest"/>
|
||||
<text name="spu_field_plan_prep" text="Preparation"/>
|
||||
<text name="spu_field_plan_sow" text="sow"/>
|
||||
<text name="spu_field_reason_cannotDo" text="no %s in the field fleet can do the job (crop/seeds/weeds)"/>
|
||||
<text name="spu_field_reason_noSowFruit" text="choose a crop to sow"/>
|
||||
<text name="spu_field_reason_notEnough" text="multi-tool: %s — only %d of %d free"/>
|
||||
<text name="spu_field_reason_notPlantable" text="%s cannot be sown in this period"/>
|
||||
<text name="spu_field_rig_crops" text="%d crops"/>
|
||||
<text name="spu_field_rig_herbicide" text="sprayer"/>
|
||||
<text name="spu_field_rig_mechanical" text="weeder (small weeds)"/>
|
||||
<text name="spu_field_rig_noSeeds" text="!! out of seeds"/>
|
||||
<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="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"/>
|
||||
<text name="spu_field_role_plow" text="plow"/>
|
||||
<text name="spu_field_role_roller" text="roller"/>
|
||||
<text name="spu_field_role_seeder" text="seeder"/>
|
||||
<text name="spu_field_role_unloader" text="unloading trailer"/>
|
||||
<text name="spu_field_role_weeder" text="weeding machine"/>
|
||||
<text name="spu_field_roll" text="Roll after sowing"/>
|
||||
<text name="spu_field_section_after" text="— After harvest —"/>
|
||||
<text name="spu_field_section_care" text="— Care while the crop grows —"/>
|
||||
<text name="spu_field_section_prep" text="— Preparation —"/>
|
||||
<text name="spu_field_soil" text="Soil work"/>
|
||||
<text name="spu_field_soilOk" text="nothing"/>
|
||||
<text name="spu_field_soilState" text="The soil needs: %s"/>
|
||||
<text name="spu_field_soil_auto" text="Auto (plow when needed)"/>
|
||||
<text name="spu_field_soil_cultivate" text="Cultivate"/>
|
||||
<text name="spu_field_soil_none" text="None"/>
|
||||
<text name="spu_field_soil_plow" text="Plow"/>
|
||||
<text name="spu_field_sow" text="Sow afterwards"/>
|
||||
<text name="spu_field_sowFruit" text="Crop to sow"/>
|
||||
<text name="spu_field_sowSame" text="Same as harvested"/>
|
||||
<text name="spu_field_stage_careLiming" text="Liming"/>
|
||||
<text name="spu_field_stage_cultivating" text="Cultivating"/>
|
||||
<text name="spu_field_stage_fertilizing" text="Fertilizing"/>
|
||||
<text name="spu_field_stage_harvesting" text="Harvesting"/>
|
||||
<text name="spu_field_stage_liming" text="Liming"/>
|
||||
<text name="spu_field_stage_mulching" text="Mulching"/>
|
||||
<text name="spu_field_stage_plowing" text="Plowing"/>
|
||||
<text name="spu_field_stage_rolling" text="Rolling"/>
|
||||
<text name="spu_field_stage_sowing" text="Sowing"/>
|
||||
<text name="spu_field_stage_weeding" text="Weeding"/>
|
||||
<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_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)"/>
|
||||
@@ -134,9 +310,12 @@
|
||||
<text name="spu_policy_sell" text="Sell"/>
|
||||
<text name="spu_policy_sellElseStore" text="Sell, else store"/>
|
||||
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
|
||||
<text name="spu_sellAt_always" text="Always (any price)"/>
|
||||
<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."/>
|
||||
@@ -146,6 +325,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)"/>
|
||||
@@ -171,14 +351,18 @@
|
||||
<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_ruleSellAlways" text="always sell"/>
|
||||
<text name="spu_flow_chainIdle" text="needs a tractor/trailer"/>
|
||||
<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"/>
|
||||
@@ -196,6 +380,9 @@
|
||||
<text name="spu_col_to" text="To"/>
|
||||
<text name="spu_col_tractors" text="Running"/>
|
||||
<text name="spu_col_status" text="Status"/>
|
||||
<text name="spu_board_chainIdleShow" text="%d chain flows are waiting for a free tractor/trailer — click to show"/>
|
||||
<text name="spu_board_chainIdleHide" text="%d chain flows without a tractor — click to hide"/>
|
||||
<text name="spu_board_chain" text="· Flows created by chains (Chains tab) ·"/>
|
||||
<text name="spu_board_supply" text="· Into building ·"/>
|
||||
<text name="spu_board_outbound" text="· Out of building ·"/>
|
||||
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
|
||||
@@ -218,5 +405,257 @@
|
||||
<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_sumSellAlways" text="always sell"/>
|
||||
<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"/>
|
||||
<text name="spu_tab_chains" text="Chains"/>
|
||||
<text name="spu_chain_noFlow" text="No flow carries that good yet — the chain creates one when a tractor/trailer is free"/>
|
||||
<text name="spu_chain_optIn" text="[Chain on]"/>
|
||||
<text name="spu_chain_optOut" text="[Remove]"/>
|
||||
<text name="spu_chain_notChosen" text="no chain"/>
|
||||
<text name="spu_chain_candidatesShow" text="%d productions on without a chain — click to show"/>
|
||||
<text name="spu_chain_candidatesHide" text="%d productions on without a chain — click to hide"/>
|
||||
<text name="spu_chain_finalFarm" text="Farm"/>
|
||||
<text name="spu_chain_finalSell" text="Sell"/>
|
||||
<text name="spu_chain_finalKeep" text="Keep"/>
|
||||
<text name="spu_chain_suggestHeader" text="Crop change suggestions"/>
|
||||
<text name="spu_chain_empty" text="No chains: enable a product in a factory"/>
|
||||
<text name="spu_chain_col_item" text="Goods"/>
|
||||
<text name="spu_chain_col_source" text="Source"/>
|
||||
<text name="spu_chain_col_perMonth" text="l/mo"/>
|
||||
<text name="spu_chain_col_status" text="Status"/>
|
||||
<text name="spu_chain_col_stock" text="Stock"/>
|
||||
<text name="spu_chain_approve" text="Suggest [Approve]"/>
|
||||
<text name="spu_chain_dismiss" text="[Reject]"/>
|
||||
<text name="spu_chain_auto" text="switched (free field)"/>
|
||||
<text name="spu_chain_gone" text="The suggestion no longer applies"/>
|
||||
<text name="spu_chain_flowOk" text="flow running"/>
|
||||
<text name="spu_chain_flowMissing" text="no tractor/trailer"/>
|
||||
<text name="spu_chain_flowNone" text="no flow"/>
|
||||
<text name="spu_chain_flowAuto" text="distributed by game"/>
|
||||
<text name="spu_chain_flowPallet" text="pallet: UAL trailer"/>
|
||||
<text name="spu_chain_flowStopped" text="flow stopped"/>
|
||||
<text name="spu_chain_bottleneck" text="bottleneck: -%s/mo"/>
|
||||
<text name="spu_chain_buy" text="must buy: %s/mo"/>
|
||||
<text name="spu_chain_cycle" text="loop"/>
|
||||
<text name="spu_chain_fieldShort" text="short %s ha"/>
|
||||
<text name="spu_chain_tight" text="stock < 3 mo"/>
|
||||
<text name="spu_chain_noField" text="no field"/>
|
||||
<text name="spu_chain_buySource" text="buy"/>
|
||||
<text name="spu_chain_fromFields" text="from fields"/>
|
||||
<text name="spu_chain_notifyMore" text="+%d more missing links — see the Chains tab"/>
|
||||
<text name="spu_chain_pause" text="[Pause]"/>
|
||||
<text name="spu_chain_resume" text="[Resume]"/>
|
||||
<text name="spu_chain_rootOk" text="covered"/>
|
||||
<text name="spu_chain_rootError" text="short"/>
|
||||
<text name="spu_chain_rootWaiting" text="waiting"/>
|
||||
<text name="spu_chain_rootPaused" text="paused"/>
|
||||
<text name="spu_chain_rootSummary" text="%s · %d sugg."/>
|
||||
<text name="spu_chain_free" text="Fields free for the chain: %s"/>
|
||||
<text name="spu_chain_switches" text="Automatic switches since load: %d"/>
|
||||
<text name="spu_chain_scanning" text="Fields still being scanned – no field changes until all are seen"/>
|
||||
<text name="spu_attention_chainSuggests" text="Chain: %d suggestions"/>
|
||||
<text name="spu_attention_hintChain" text=" → click: see Chains"/>
|
||||
<text name="spu_chain_problem_buy" text="None of your factories makes %s — build one or buy it"/>
|
||||
<text name="spu_chain_problem_barn" text="Barns make too little %s — buy the rest or more animals"/>
|
||||
<text name="spu_chain_problem_bottleneck" text="%s can only make %s of %s/mo"/>
|
||||
<text name="spu_chain_problem_off" text="%s (%s) was turned off by you — turn it on or pause the chain"/>
|
||||
<text name="spu_chain_problem_cycle" text="Loop around %s — the chain cannot be calculated"/>
|
||||
<text name="spu_attention_chainProblems" text="Chains: %d missing links"/>
|
||||
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: status window"/>
|
||||
<text name="spu_status_flows" text="Flows"/>
|
||||
<text name="spu_status_fields" text="Fields"/>
|
||||
<text name="spu_status_chains" text="Chains"/>
|
||||
<text name="spu_status_warnings" text="Warnings"/>
|
||||
<text name="spu_status_more" text="+%d more"/>
|
||||
<text name="spu_status_idle" text="Smart Pickup: nothing running right now"/>
|
||||
<text name="spu_status_act_fetch" text="fetching %s at %s"/>
|
||||
<text name="spu_status_act_driving" text="driving to %s"/>
|
||||
<text name="spu_status_act_waiting" text="waiting at %s"/>
|
||||
<text name="spu_status_act_holding" text="holding at %s"/>
|
||||
<text name="spu_status_act_fetchWagon" text="fetching wagon"/>
|
||||
<text name="spu_status_act_waitWagon" text="waiting for wagon"/>
|
||||
<text name="spu_status_act_emptying" text="emptying the wagon"/>
|
||||
<text name="spu_status_act_homing" text="heading home"/>
|
||||
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
|
||||
<text name="spu_dep_warn" text="Warning: you switched off %s (%s) — %s uses it. Stock lasts about %d days."/>
|
||||
<text name="spu_dep_warnNoDays" text="Warning: you switched off %s (%s) — %s uses it."/>
|
||||
<text name="spu_attention_depOff" text="Switched off but needed: %s"/>
|
||||
<text name="spu_attention_depOffMore" text="Switched off but needed: %s +%d"/>
|
||||
<text name="spu_attention_hintFactories" text=" → click: see Factories"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.21.0.0</version>
|
||||
<version>1.62.7.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,17 +26,56 @@
|
||||
<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="adLoadDiag.lua"/>
|
||||
<sourceFile filename="adSellBlock.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="adChainModel.lua"/>
|
||||
<sourceFile filename="adCropPlan.lua"/>
|
||||
<sourceFile filename="adChainActions.lua"/>
|
||||
<sourceFile filename="adDependency.lua"/>
|
||||
<sourceFile filename="adChainData.lua"/>
|
||||
<sourceFile filename="adChain.lua"/>
|
||||
<sourceFile filename="adChainRun.lua"/>
|
||||
<sourceFile filename="adChainView.lua"/>
|
||||
<sourceFile filename="adStatusView.lua"/>
|
||||
<sourceFile filename="adStatusHud.lua"/>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
<actions>
|
||||
<action name="SPU_TOGGLE_STATUS"/>
|
||||
</actions>
|
||||
<inputBinding>
|
||||
<actionBinding action="SPU_TOGGLE_STATUS">
|
||||
<!-- numpad * (Insert bruges af FS25_UniversalAutoload) -->
|
||||
<binding device="KB_MOUSE_DEFAULT" input="KEY_KP_multiply"/>
|
||||
</actionBinding>
|
||||
</inputBinding>
|
||||
</modDesc>
|
||||
|
||||
@@ -1,265 +1,106 @@
|
||||
# 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.
|
||||
- **Produktionskæder:** slå et produkt til i en fabrik, og moden holder det kørende tilbage til marken. Fanen Kæder viser træet, marker markeret "Fri til kæden" skifter afgrøde automatisk, og andre marker får forslag, du godkender eller afviser. Mellemled og flows oprettes efter behov.
|
||||
- **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.
|
||||
- **Statusvindue:** numpad \* viser et lille vindue øverst til højre med flow-traktorernes handling og fyld,
|
||||
markarbejde i gang, kæder med manglende led og advarsler.
|
||||
- **Advarsel når en nødvendig fabrik slukkes:** slukker du en produktion, som et flow, en stald eller en kæde
|
||||
bruger, advarer moden og viser hvor længe lageret rækker.
|
||||
- **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.
|
||||
- **Production chains:** switch on a product in a factory and the mod keeps it running back to the field. The Chains tab shows the tree, fields marked "Free for chains" change crop automatically, and other fields get suggestions you approve or reject. Intermediate goods and flows are set up as needed.
|
||||
- **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.
|
||||
- **Status window:** numpad \* shows a small window in the top right with the flow tractors' action and
|
||||
fill, field work in progress, chains with missing links and warnings.
|
||||
- **Warning when a needed factory is switched off:** switch off a production that a flow, a barn or a chain
|
||||
uses, and the mod warns you and shows how long the stock lasts.
|
||||
- **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 .`
|
||||
|
||||
+942
-284
File diff suppressed because it is too large
Load Diff
+1079
-393
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,130 +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, så hver bane
|
||||
dækker det, slåmaskinen lagde. Så behøver banerne ikke flugte med skårene.
|
||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`): nærmeste silo med plads, der tager varen,
|
||||
med vej fra `F`-markøren. *Tving lager* pr. mark (som "Lager til" i flows). Fabrikker er ikke mål.
|
||||
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 …`.
|
||||
@@ -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,53 +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_plow = 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)
|
||||
|
||||
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_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")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,103 +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", f.plan.product, "silage")
|
||||
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 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)
|
||||
|
||||
-- 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", M.maxRigs("collecting"), 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), "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), "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.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 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, "silage")
|
||||
|
||||
-- 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")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,36 +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: kun slåafgrøder, sorteret efter nummer
|
||||
local rows = V.rows(infos, fields, false)
|
||||
check("V1 to rækker", #rows, 2)
|
||||
check("V1 sorteret", rows[1].info.id, 12)
|
||||
check("V1 model med", rows[2].field.id, 109)
|
||||
-- V2: vis alle tager resten med; mangler model -> ny standardmark
|
||||
local all = V.rows(infos, fields, true)
|
||||
check("V2 tre rækker", #all, 3)
|
||||
check("V2 standardmodel", all[2].field.stage, "idle")
|
||||
check("V2 skjulte", V.hiddenCount(infos, false), 1)
|
||||
check("V2 vis alle skjuler intet", V.hiddenCount(infos, true), 0)
|
||||
-- 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")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -1,33 +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)
|
||||
|
||||
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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user