Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1 +1,5 @@
|
|||||||
*.zip
|
*.zip
|
||||||
|
docs/superpowers/
|
||||||
|
bench/
|
||||||
|
.superpowers/
|
||||||
|
tests/
|
||||||
|
|||||||
+445
@@ -0,0 +1,445 @@
|
|||||||
|
# Æ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.61 — Produktionskæder
|
||||||
|
|
||||||
|
- **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
|
ADBuildings.MAX_MARKER_DISTANCE = 40
|
||||||
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
|
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
|
||||||
ADBuildings.MAX_DROP_DISTANCE = 25
|
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}
|
local cache = {list = nil, farmId = nil, time = -math.huge}
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ local function getNodePosition(node)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function getName(placeable, fallback)
|
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
|
if placeable ~= nil and placeable.getName ~= nil then
|
||||||
local ok, name = pcall(placeable.getName, placeable)
|
local ok, name = pcall(placeable.getName, placeable)
|
||||||
if ok and name ~= nil then return tostring(name) end
|
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
|
for _, place in ipairs(spawner.spawnPlaces or {}) do
|
||||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||||
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
|
||||||
-- stald uden aflæsningsnoder i stationen (hønsehus: kun foder-triggeren) -> søg i staldens foderdata
|
-- stald uden aflæsningsnoder i stationen (hønsehus: kun foder-triggeren) -> søg i staldens foderdata
|
||||||
if #unloadNodes == 0 and placeable.getHusbandryFillLevel ~= nil then
|
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
|
for _, place in ipairs(palletSpawner.spawnPlaces or {}) do
|
||||||
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
|
||||||
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
|
||||||
end
|
end
|
||||||
if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then
|
if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then
|
||||||
@@ -218,6 +227,82 @@ end
|
|||||||
-- Aflæsningsmarkør: nærmeste ikke-Wait-markør ≤ MAX_MARKER_DISTANCE til aflæsningsnoderne — men ikke
|
-- 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å
|
-- 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.
|
-- aflæsningstriggeren end drop). Kun én fælles markør (AIO Load / Unload) bruges til begge.
|
||||||
|
-- Alle ikke-Wait-markører ≤ MAX_MARKER_DISTANCE til nodes, nærmeste først: {{id, name, distance}}.
|
||||||
|
local function markerCandidates(adEnv, nodes)
|
||||||
|
local positions, found = {}, {}
|
||||||
|
for _, node in ipairs(nodes) do
|
||||||
|
local x, z = getNodePosition(node)
|
||||||
|
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||||
|
end
|
||||||
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
|
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||||
|
if wayPoint ~= nil then
|
||||||
|
local nearest = nil
|
||||||
|
for _, position in ipairs(positions) do
|
||||||
|
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||||
|
if nearest == nil or distance < nearest then nearest = distance end
|
||||||
|
end
|
||||||
|
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then
|
||||||
|
table.insert(found, {id = marker.id, name = tostring(marker.name), distance = nearest})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||||
|
return found
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
||||||
|
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
||||||
|
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
||||||
|
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
||||||
|
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
||||||
|
for _, candidate in ipairs(candidates or {}) do
|
||||||
|
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
||||||
|
if not isUnloads then
|
||||||
|
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Diagnose (én gang pr. stald pr. session): hvilke markører ligger nærmest læsse- og aflæsningspunkterne.
|
||||||
|
-- (in-game 27/9: Goat Food fik "Sheep Milk"-markørerne som mål og Goat Milk Pickup "Sheep Food" som kilde)
|
||||||
|
local diagnosed = {}
|
||||||
|
local function closestMarkers(adEnv, nodes, count)
|
||||||
|
local positions, found = {}, {}
|
||||||
|
for _, node in ipairs(nodes) do
|
||||||
|
local x, z = getNodePosition(node)
|
||||||
|
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||||
|
end
|
||||||
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
|
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||||
|
if wayPoint ~= nil then
|
||||||
|
local nearest = nil
|
||||||
|
for _, position in ipairs(positions) do
|
||||||
|
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
|
||||||
|
if nearest == nil or distance < nearest then nearest = distance end
|
||||||
|
end
|
||||||
|
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then table.insert(found, {name = tostring(marker.name), distance = nearest}) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(found, function(a, b) return a.distance < b.distance end)
|
||||||
|
local parts = {}
|
||||||
|
for index = 1, math.min(count, #found) do table.insert(parts, string.format("%s %.0f m", found[index].name, found[index].distance)) end
|
||||||
|
return #parts > 0 and table.concat(parts, ", ") or "ingen markør"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function diagnoseHusbandry(adEnv, id, name, loadGroups, unloadNodes, loadMarkers, unloadMarker)
|
||||||
|
if diagnosed[id] then return end
|
||||||
|
diagnosed[id] = true
|
||||||
|
local groups = {}
|
||||||
|
for index, nodes in ipairs(loadGroups) do table.insert(groups, string.format("læs%d: %s", index, closestMarkers(adEnv, nodes, 2))) end
|
||||||
|
local loads = {}
|
||||||
|
for _, marker in ipairs(loadMarkers) do table.insert(loads, tostring(marker.name)) end
|
||||||
|
Logging.info("ADSmartPickup: stald '%s': %s | aflæsning nærmest: %s | valgt læs=[%s] aflæs=%s", tostring(name),
|
||||||
|
#groups > 0 and table.concat(groups, "; ") or "ingen læssegrupper", closestMarkers(adEnv, unloadNodes, 3),
|
||||||
|
table.concat(loads, ", "), unloadMarker ~= nil and tostring(unloadMarker.name) or "-")
|
||||||
|
end
|
||||||
|
|
||||||
local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||||
if #nodes == 0 then
|
if #nodes == 0 then
|
||||||
return nil
|
return nil
|
||||||
@@ -332,8 +417,9 @@ local function build(adEnv, farmId)
|
|||||||
local marker = siloLoadMarker(adEnv, farmId, placeable, loadStations)
|
local marker = siloLoadMarker(adEnv, farmId, placeable, loadStations)
|
||||||
if marker ~= nil then table.insert(loadMarkers, marker) end
|
if marker ~= nil then table.insert(loadMarkers, marker) end
|
||||||
else
|
else
|
||||||
|
local unloadBest = markerCandidates(adEnv, unloadNodes)[1]
|
||||||
for _, nodes in ipairs(loadGroups) do
|
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
|
if marker ~= nil and not seen[marker.id] then
|
||||||
seen[marker.id] = true
|
seen[marker.id] = true
|
||||||
table.insert(loadMarkers, marker)
|
table.insert(loadMarkers, marker)
|
||||||
@@ -343,6 +429,7 @@ local function build(adEnv, farmId)
|
|||||||
end
|
end
|
||||||
-- siloer og palleskure læsser og aflæsser samme sted: altid nærmeste markør
|
-- 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")
|
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,
|
-- 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
|
-- ingen læssestation -> et forsyningsmål, aldrig en kilde
|
||||||
local isFillOnlySilo = kind == "silo" and loadMarkers[1] == nil and #loadGroups == 0 and unloadMarker ~= nil
|
local isFillOnlySilo = kind == "silo" and loadMarkers[1] == nil and #loadGroups == 0 and unloadMarker ~= nil
|
||||||
@@ -363,6 +450,7 @@ local function build(adEnv, farmId)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ADBuildings.invalidate()
|
function ADBuildings.invalidate()
|
||||||
|
if ADCache ~= nil then ADCache.clear() end
|
||||||
cache.list, cache.time = nil, -math.huge
|
cache.list, cache.time = nil, -math.huge
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -372,6 +460,11 @@ function ADBuildings.list(adEnv, farmId)
|
|||||||
return cache.list
|
return cache.list
|
||||||
end
|
end
|
||||||
cache.list, cache.farmId, cache.time = build(adEnv, farmId), farmId, time
|
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
|
return cache.list
|
||||||
end
|
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,573 @@
|
|||||||
|
-- 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 = {}}
|
||||||
|
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()
|
||||||
|
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil}
|
||||||
|
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}
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
function ADChain.modelRecipes(recipes, paused, userOff)
|
||||||
|
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 (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"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
|
||||||
|
function ADChain.flowStatus(edges, runs, noVehicle)
|
||||||
|
local result = {}
|
||||||
|
for _, edge in ipairs(edges or {}) do
|
||||||
|
local status = "missing"
|
||||||
|
for _, run in ipairs(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})
|
||||||
|
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)
|
||||||
|
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), 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))
|
||||||
|
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)}
|
||||||
|
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), 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
|
||||||
|
|
||||||
|
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
|
||||||
|
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 = {}}
|
||||||
|
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,67 @@
|
|||||||
|
-- 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}
|
||||||
|
|
||||||
|
function ADChainActions.edges(chain)
|
||||||
|
local list, seen = {}, {}
|
||||||
|
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
|
||||||
|
table.insert(list, {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey})
|
||||||
|
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 not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainActions.flowsToStop(runs, activeRootKeys)
|
||||||
|
local list = {}
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
if run.chain ~= nil and run.chain ~= "" and not run.stopped and not (activeRootKeys or {})[run.chain] then
|
||||||
|
table.insert(list, run)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
-- 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).
|
||||||
|
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)})
|
||||||
|
points[key] = {point = point, productionId = production.id, placeable = placeable}
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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 = {}}
|
||||||
|
local nameOf = nameResolver(data)
|
||||||
|
data.priceOf = priceResolver(data)
|
||||||
|
readPlaceables(data, farmId, byChain, nameOf)
|
||||||
|
readCrops(data, nameOf)
|
||||||
|
readFieldGoods(data, nameOf)
|
||||||
|
readStock(data, adEnv, 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 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,217 @@
|
|||||||
|
-- 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
|
||||||
|
|
||||||
|
local function createFlow(adEnv, data, edge, pool, used)
|
||||||
|
local title = data.fillTitles[edge.fillType] or edge.fillType
|
||||||
|
local created = ADRunsController.createRun(ADChain.flowName(title, edge.consumerName or edge.consumer), "supply")
|
||||||
|
local runId = created.id
|
||||||
|
ADRunsController.updateRun(runId, {targetBuildingIds = {edge.consumer}, ingredientMode = "manual", ingredients = {edge.fillType},
|
||||||
|
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, edge.consumer), chain = edge.rootKey})
|
||||||
|
-- updateRun erstatter store'en: slå altid run'et op igen
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
local fillIndex = data.fillIndex[edge.fillType]
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
function ADChainRun.createFlows(adEnv, data, edges)
|
||||||
|
local missing = ADChainActions.missingFlows(edges, ADChain.runsView(ADRunsController.store.runs))
|
||||||
|
local pool, used, count = {}, {}, 0
|
||||||
|
for _, edge in ipairs(missing) do
|
||||||
|
if count >= ADChain.MAX_NEW_FLOWS then break end
|
||||||
|
count = count + 1
|
||||||
|
local ok, err = pcall(createFlow, adEnv, data, edge, pool, used)
|
||||||
|
if not ok then warn("flow til %s (%s) kunne ikke oprettes: %s", tostring(edge.consumerName), tostring(edge.fillType), tostring(err)) end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
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
|
||||||
|
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 okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
|
||||||
|
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,392 @@
|
|||||||
|
-- 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 = {15, 14, 6, 21, 8}
|
||||||
|
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_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_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"}
|
||||||
|
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") 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
|
||||||
|
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
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""), "", status,
|
||||||
|
ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")},
|
||||||
|
{[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}, root.key))
|
||||||
|
if not isExpanded then
|
||||||
|
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 1, ctx.titleOf, ctx.textOf)) end
|
||||||
|
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
|
||||||
|
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, {suggest = true, 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)
|
||||||
|
table.insert(rows, makeRow("root", 0, "stopped", {name, recipe ~= nil and tostring(recipe.buildingName or "") or "", "",
|
||||||
|
ctx.textOf("spu_chain_rootPaused"), ctx.textOf("spu_chain_resume")}, {[5] = {kind = "resume", rootKey = key}}))
|
||||||
|
end
|
||||||
|
return #keys
|
||||||
|
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)}
|
||||||
|
for _, root in ipairs(roots) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
|
||||||
|
pausedRows(rows, last, ctx)
|
||||||
|
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
|
||||||
@@ -62,16 +62,92 @@ local function collect(vehicle, list)
|
|||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sprøjtens/sprederens rolle ud fra det fyld den har på (tom -> nil).
|
-- Hvilke varer der hører til en rolle (påfyldning af en tom spreder/sprøjte), i foretrukken rækkefølge.
|
||||||
|
ADCpBridge.ROLE_FILLTYPES = {
|
||||||
|
fertilizer = {"FERTILIZER", "LIQUIDFERTILIZER", "MANURE", "LIQUIDMANURE", "DIGESTATE"},
|
||||||
|
limer = {"LIME"},
|
||||||
|
weeder = {"HERBICIDE"},
|
||||||
|
}
|
||||||
|
ADCpBridge.ROLE_ORDER = {"fertilizer", "limer", "weeder"}
|
||||||
|
|
||||||
|
-- Sprøjtens/sprederens roller: alle roller dens fyldenhed kan tage (Bredal-spreder: gødning OG kalk).
|
||||||
|
-- Primær rolle = fyldets rolle (har den kalk på, er den kalkspreder), ellers den første den kan tage.
|
||||||
|
-- Returnerer primær, sæt {[rolle] = true}, fyldets rolle (nil = tom); nil hvis ingen roller.
|
||||||
|
function ADCpBridge.sprayRolesFor(filledName, supportedNames)
|
||||||
|
local roles, first = {}, nil
|
||||||
|
for _, role in ipairs(ADCpBridge.ROLE_ORDER) do
|
||||||
|
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role]) do
|
||||||
|
if supportedNames[name] then
|
||||||
|
roles[role] = true
|
||||||
|
first = first or role
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local filledRole = filledName ~= nil and ADCpBridge.sprayRoleForFillType(filledName) or nil
|
||||||
|
if filledRole ~= nil then roles[filledRole] = true end
|
||||||
|
local primary = filledRole or first
|
||||||
|
return primary, primary ~= nil and roles or nil, filledRole
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sprayerNames(object)
|
||||||
|
local unitIndex = object:getSprayerFillUnitIndex()
|
||||||
|
local filledName = nil
|
||||||
|
local level = object:getFillUnitFillLevel(unitIndex) or 0
|
||||||
|
local capacity = object.getFillUnitCapacity ~= nil and (object:getFillUnitCapacity(unitIndex) or 0) or 0
|
||||||
|
if level > 0 then
|
||||||
|
filledName = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unitIndex))
|
||||||
|
end
|
||||||
|
local supported = {}
|
||||||
|
local unit = object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits ~= nil and object.spec_fillUnit.fillUnits[unitIndex] or nil
|
||||||
|
for fillType, isSupported in pairs(unit ~= nil and unit.supportedFillTypes or {}) do
|
||||||
|
if isSupported then
|
||||||
|
local name = g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||||
|
if name ~= nil then supported[name] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return filledName, supported, level, capacity
|
||||||
|
end
|
||||||
|
|
||||||
local function sprayRole(object)
|
local function sprayRole(object)
|
||||||
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
|
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
|
||||||
local ok, role = pcall(function()
|
local ok, role, roles, filledRole, wrongFillDumpable = pcall(function()
|
||||||
local unit = object:getSprayerFillUnitIndex()
|
local filledName, supported, level, capacity = sprayerNames(object)
|
||||||
if (object:getFillUnitFillLevel(unit) or 0) <= 0 then return nil end
|
local r, rs, fr = ADCpBridge.sprayRolesFor(filledName, supported)
|
||||||
local name = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unit))
|
return r, rs, fr, ADCpBridge.isDumpable(level, capacity)
|
||||||
return ADCpBridge.sprayRoleForFillType(name)
|
|
||||||
end)
|
end)
|
||||||
return ok and role or nil
|
if not ok then return nil end
|
||||||
|
return role, roles, filledRole, wrongFillDumpable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Må en forkert vare hældes ud? Kun en lille rest: højst 10 % af tanken og aldrig over 2.000 l
|
||||||
|
-- (in-game 26/9: 100.000 l flydende gødning og 4.000 l sprøjtemiddel blev hældt ud for at skifte opgave).
|
||||||
|
ADCpBridge.MAX_DUMP_LITERS = 2000
|
||||||
|
ADCpBridge.MAX_DUMP_SHARE = 0.10
|
||||||
|
function ADCpBridge.isDumpable(level, capacity)
|
||||||
|
level = level or 0
|
||||||
|
if level <= 0 then return true end
|
||||||
|
if level > ADCpBridge.MAX_DUMP_LITERS then return false end
|
||||||
|
return capacity == nil or capacity <= 0 or level <= capacity * ADCpBridge.MAX_DUMP_SHARE
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tøm sprøjten/sprederen for en vare der ikke hører til opgaven (kalk på, skal gøde). FS25 kan ikke
|
||||||
|
-- tippe gødning/kalk af, så resten kasseres. Returnerer liter og varenavn (0 hvis intet blev tømt).
|
||||||
|
function ADCpBridge.emptySprayerIfWrong(vehicle, role)
|
||||||
|
local liters, name = 0, nil
|
||||||
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
|
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||||
|
pcall(function()
|
||||||
|
local filledName, _, level, capacity = sprayerNames(object)
|
||||||
|
if filledName == nil or ADCpBridge.sprayRoleForFillType(filledName) == role then return end
|
||||||
|
if not ADCpBridge.isDumpable(level, capacity) then return end
|
||||||
|
local unit = object:getSprayerFillUnitIndex()
|
||||||
|
object:addFillUnitFillLevel(object:getOwnerFarmId(), unit, -level, object:getFillUnitFillType(unit), ToolType.UNDEFINED, nil)
|
||||||
|
liters, name = liters + level, filledName
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return liters, name
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADCpBridge.getSpecs(vehicle)
|
function ADCpBridge.getSpecs(vehicle)
|
||||||
@@ -80,7 +156,7 @@ function ADCpBridge.getSpecs(vehicle)
|
|||||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||||
if object[name] ~= nil then specs[name] = true end
|
if object[name] ~= nil then specs[name] = true end
|
||||||
end
|
end
|
||||||
specs.sprayRole = specs.sprayRole or sprayRole(object)
|
if specs.sprayRole == nil then specs.sprayRole, specs.sprayRoles, specs.filledRole, specs.dumpable = sprayRole(object) end
|
||||||
end
|
end
|
||||||
if vehicle.getCanStartCpCombineUnloader ~= nil then
|
if vehicle.getCanStartCpCombineUnloader ~= nil then
|
||||||
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
|
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
|
||||||
@@ -93,12 +169,43 @@ function ADCpBridge.getSpecs(vehicle)
|
|||||||
return specs
|
return specs
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
|
-- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter
|
||||||
|
-- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere).
|
||||||
|
function ADCpBridge.vineFruitSet(spec)
|
||||||
|
local fruits = {}
|
||||||
|
if spec == nil then return fruits end
|
||||||
|
for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do
|
||||||
|
if type(spec[key]) == "number" then fruits[spec[key]] = true end
|
||||||
|
end
|
||||||
|
if type(spec.fruitTypes) == "table" then
|
||||||
|
for key, value in pairs(spec.fruitTypes) do
|
||||||
|
if type(value) == "number" then fruits[value] = true
|
||||||
|
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return fruits
|
||||||
|
end
|
||||||
|
|
||||||
|
local vineLogged = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
-- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere i kæden). nil = ikke en høster.
|
||||||
function ADCpBridge.getHarvestFruits(vehicle)
|
function ADCpBridge.getHarvestFruits(vehicle)
|
||||||
if vehicle.spec_combine == nil then return nil end
|
if vehicle.spec_combine == nil then return nil end
|
||||||
local fruits = {}
|
local fruits = {}
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
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
|
end
|
||||||
return fruits
|
return fruits
|
||||||
end
|
end
|
||||||
@@ -164,8 +271,9 @@ function ADCpBridge.isPlantableNow(fruitIndex)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||||
-- nuværende fyld (gødning, kalk, sprøjtemiddel). nil = intet at fylde på (strigle, plov ...).
|
-- fyld; en TOM spreder får rollens vare (gødning ved gødskning, kalk ved kalkning), hvis dens fyldenhed
|
||||||
function ADCpBridge.getRefillFillType(vehicle)
|
-- kan tage den. nil = intet at fylde på (strigle, plov ...).
|
||||||
|
function ADCpBridge.getRefillFillType(vehicle, role)
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
||||||
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||||
@@ -173,7 +281,16 @@ function ADCpBridge.getRefillFillType(vehicle)
|
|||||||
end
|
end
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||||
local ok, fillType = pcall(function() return object:getFillUnitFillType(object:getSprayerFillUnitIndex()) end)
|
local ok, fillType = pcall(function()
|
||||||
|
local filledName, supported = sprayerNames(object)
|
||||||
|
if filledName ~= nil and (role == nil or ADCpBridge.sprayRoleForFillType(filledName) == role) then
|
||||||
|
return g_fillTypeManager:getFillTypeIndexByName(filledName)
|
||||||
|
end
|
||||||
|
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role] or {}) do
|
||||||
|
if supported[name] then return g_fillTypeManager:getFillTypeIndexByName(name) end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -274,9 +391,21 @@ local function courseplayWidth(vehicle)
|
|||||||
return nil
|
return nil
|
||||||
end
|
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)
|
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)
|
local cpWidth = courseplayWidth(vehicle)
|
||||||
if cpWidth ~= nil then return cpWidth end
|
if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) end
|
||||||
local best = 0
|
local best = 0
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
local variable = variableWidth(object)
|
local variable = variableWidth(object)
|
||||||
@@ -288,11 +417,15 @@ function ADCpBridge.getWorkWidth(vehicle)
|
|||||||
if ok and type(width) == "number" and width > best then best = width end
|
if ok and type(width) == "number" and width > best then best = width end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return best
|
return ADCpBridge.widthOrVine(best, hasVine)
|
||||||
end
|
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)
|
function ADCpBridge.isWorking(vehicle)
|
||||||
if vehicle.getIsCpFieldWorkActive ~= nil and vehicle:getIsCpFieldWorkActive() then return true end
|
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
|
return vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() or false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -365,6 +498,27 @@ function ADCpBridge.getCourse(vehicle)
|
|||||||
return ok and course or nil
|
return ok and course or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Forager: nye kurser får altid HEADLANDS runder og starter på forageren, så der ikke køres ind i træer, hegn
|
||||||
|
-- o.l. i hjørnerne (brugerkrav 29/9). Kan kurset ikke laves med forager (smal mark), prøves én gang uden.
|
||||||
|
ADCpBridge.HEADLANDS = 2
|
||||||
|
|
||||||
|
function ADCpBridge.setHeadlands(vehicle, n)
|
||||||
|
if vehicle.getCourseGeneratorSettings == nil then return false end
|
||||||
|
return (pcall(function()
|
||||||
|
local settings = vehicle:getCourseGeneratorSettings()
|
||||||
|
settings.numberOfHeadlands:setValue(n)
|
||||||
|
if settings.startOnHeadland ~= nil then settings.startOnHeadland:setValue(n > 0) end
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Har kurset forager? (ukendt API = antag ja, så intet genberegnes unødigt)
|
||||||
|
function ADCpBridge.courseHasHeadlands(course)
|
||||||
|
if course == nil then return false end
|
||||||
|
if course.getNumberOfHeadlands == nil then return true end
|
||||||
|
local ok, n = pcall(course.getNumberOfHeadlands, course)
|
||||||
|
return not ok or type(n) ~= "number" or n > 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||||
local function setMultiTools(vehicle, n)
|
local function setMultiTools(vehicle, n)
|
||||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||||
@@ -384,7 +538,36 @@ end
|
|||||||
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
|
-- 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)
|
-- 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)
|
-- lane = køretøjets bane på et multi-tool-kursus (laneOffset -2..2)
|
||||||
-- onDone(ok, reason) kaldes én gang; reason "copied" ved succes med kopieret kursus.
|
-- reuse = start på køretøjets eget nuværende kursus uden ny beregning (ADCourseCache fandt det passende)
|
||||||
|
-- onDone(ok, reason) kaldes én gang; reason "copied"/"reused" ved succes med kopieret/genbrugt kursus.
|
||||||
|
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
||||||
|
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
||||||
|
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
||||||
|
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||||
|
function ADCpBridge.vineOffsetFor(fruitName, role)
|
||||||
|
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
||||||
|
if role == "harvester" then return 0 end
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vinrækker der springes over: sprøjter/spredere arbejder på begge sider af gangen på én gang, så de kører kun
|
||||||
|
-- i hver anden gang (1). Andre redskaber (harve, mulcher) arbejder mod én side (0). nil = ikke en vinmark.
|
||||||
|
ADCpBridge.VINE_BOTH_SIDES = {fertilizer = true, weeder = true, limer = true}
|
||||||
|
function ADCpBridge.vineSkipFor(fruitName, role)
|
||||||
|
if ADCpBridge.vineOffsetFor(fruitName, role) == nil then return nil end
|
||||||
|
return ADCpBridge.VINE_BOTH_SIDES[role] and 1 or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCpBridge.setVineSkip(vehicle, value)
|
||||||
|
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||||
|
return (pcall(function() vehicle:getCpVineSettings().vineRowsToSkip:setValue(value) end))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCpBridge.setVineOffset(vehicle, value)
|
||||||
|
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
|
||||||
|
return (pcall(function() vehicle:getCpVineSettings().vineCenterOffset:setValue(value) end))
|
||||||
|
end
|
||||||
|
|
||||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if not ADCpBridge.isAvailable() then
|
if not ADCpBridge.isAvailable() then
|
||||||
@@ -398,6 +581,12 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
|||||||
local params = job.cpJobParameters
|
local params = job.cpJobParameters
|
||||||
if params ~= nil and params.fieldPosition ~= nil then params.fieldPosition:setPosition(fieldX, fieldZ) end
|
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
|
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()
|
job:setValues()
|
||||||
local handle = {cancelled = false, fired = false, finished = false}
|
local handle = {cancelled = false, fired = false, finished = false}
|
||||||
local function finish(ok, reason)
|
local function finish(ok, reason)
|
||||||
@@ -411,10 +600,16 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
|||||||
local ok, err = job:validate(farmId)
|
local ok, err = job:validate(farmId)
|
||||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||||
g_currentMission.aiSystem:startJob(job, farmId)
|
g_currentMission.aiSystem:startJob(job, farmId)
|
||||||
finish(true, handle.copiedCourse and "copied" or nil)
|
finish(true, handle.copiedCourse and "copied" or (handle.reusedCourse and "reused" or nil))
|
||||||
end
|
end
|
||||||
local function generateAndStart()
|
local function generateAndStart()
|
||||||
if handle.cancelled then return end
|
if handle.cancelled then return end
|
||||||
|
-- genbrug: maskinens eget kursus (CP's validate kræver kun hasCpCourse) — ingen ny beregning
|
||||||
|
-- eget kursus genbruges kun med forager (ellers nyt kursus med 2 runder)
|
||||||
|
if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil and ADCpBridge.courseHasHeadlands(ADCpBridge.getCourse(vehicle)) then
|
||||||
|
handle.reusedCourse = true
|
||||||
|
return startWithCourse()
|
||||||
|
end
|
||||||
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
||||||
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
||||||
if copied then
|
if copied then
|
||||||
@@ -424,12 +619,23 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
|||||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||||
end
|
end
|
||||||
setMultiTools(vehicle, opts.multiTools or 1)
|
setMultiTools(vehicle, opts.multiTools or 1)
|
||||||
|
-- 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)
|
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||||
|
handle.generating = false
|
||||||
if handle.cancelled then return end
|
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
|
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||||
startWithCourse()
|
startWithCourse()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
generate(ADCpBridge.HEADLANDS)
|
||||||
|
end
|
||||||
handle.onBoundary = function(_, success, message)
|
handle.onBoundary = function(_, success, message)
|
||||||
if handle.fired then return end
|
if handle.fired then return end
|
||||||
handle.fired = true
|
handle.fired = true
|
||||||
@@ -462,6 +668,21 @@ function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station
|
|||||||
if params ~= nil then
|
if params ~= nil then
|
||||||
if params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
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
|
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
|
if params.useGiantsUnload ~= nil then pcall(function() params.useGiantsUnload:setValue(true) end) end
|
||||||
end
|
end
|
||||||
pcall(function() job:setValues() end)
|
pcall(function() job:setValues() end)
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
-- AD CP Resume
|
||||||
|
-- Spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (AutoDrive gør selv). Ved hvert gem
|
||||||
|
-- skrives de maskiner hvor CP kører (uden AD) til ADSmartPickup_cpresume.xml; efter indlæsning startes netop
|
||||||
|
-- dem igen fra nærmeste waypoint med CP's egen HUD-start (samme job som spilleren sidst valgte i HUD'en).
|
||||||
|
|
||||||
|
ADCpResume = {}
|
||||||
|
ADCpResume.FILE_NAME = "ADSmartPickup_cpresume.xml"
|
||||||
|
ADCpResume.ROOT = "ADSmartPickupCpResume"
|
||||||
|
ADCpResume.LOG_PREFIX = "ADSmartPickup: CP-genstart: "
|
||||||
|
ADCpResume.pending = nil -- {[vehicleId] = true} fra seneste gem; nil = ingen fil (gammel regel bruges)
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- vehicles = {{id, cpActive, adActive}} -> id'er der skal huskes
|
||||||
|
function ADCpResume.activeIds(vehicles)
|
||||||
|
local ids = {}
|
||||||
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
|
if vehicle.cpActive and not vehicle.adActive then table.insert(ids, vehicle.id) end
|
||||||
|
end
|
||||||
|
return ids
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCpResume.fromRecords(ids)
|
||||||
|
local set = {}
|
||||||
|
for _, id in ipairs(ids or {}) do
|
||||||
|
if type(id) == "string" and id ~= "" then set[id] = true end
|
||||||
|
end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vehicles = {{id, aiActive, controlled, fieldWork}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste).
|
||||||
|
-- fieldWork = false: CP's HUD-job er ikke markarbejde (fx bunkersilo) — HUD-start ville starte det forkerte job
|
||||||
|
-- (in-game 29/9: "CP: No silo found"); nil = ukendt (ældre CP) -> startes som før.
|
||||||
|
function ADCpResume.toStart(pending, vehicles)
|
||||||
|
if pending == nil then return nil end
|
||||||
|
local ids = {}
|
||||||
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
|
if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled and vehicle.fieldWork ~= false then
|
||||||
|
table.insert(ids, vehicle.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ids
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
-- true/false fra CP's HUD; nil når CP-versionen ikke har funktionen
|
||||||
|
local function hudFieldWork(vehicle)
|
||||||
|
if vehicle.cpIsHudFieldWorkJobSelected == nil then return nil end
|
||||||
|
return vehicle:cpIsHudFieldWorkJobSelected() == true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehicleState(adEnv, vehicle)
|
||||||
|
local ad = vehicle.ad
|
||||||
|
return {
|
||||||
|
id = ADRunsController.getVehicleId(vehicle),
|
||||||
|
cpActive = vehicle.getIsCpActive ~= nil and vehicle:getIsCpActive() == true,
|
||||||
|
adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(),
|
||||||
|
aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true,
|
||||||
|
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true,
|
||||||
|
fieldWork = hudFieldWork(vehicle),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function farmVehicles(adEnv)
|
||||||
|
local list, byId = {}, {}
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local ok, state = pcall(vehicleState, adEnv, vehicle)
|
||||||
|
if ok and state.id ~= nil then
|
||||||
|
table.insert(list, state)
|
||||||
|
byId[state.id] = vehicle
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list, byId
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Starter CP fra nærmeste waypoint (HUD-startpunktet sættes som spillerens "start ved nærmeste"-tast).
|
||||||
|
function ADCpResume.startNearest(vehicle)
|
||||||
|
if vehicle.cpStartStopDriver == nil or (vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive()) then return false end
|
||||||
|
if vehicle.getCpStartingPointSetting ~= nil and CpFieldWorkJobParameters ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse() then
|
||||||
|
local setting = vehicle:getCpStartingPointSetting()
|
||||||
|
if setting ~= nil then setting:setValue(CpFieldWorkJobParameters.START_AT_NEAREST_POINT) end
|
||||||
|
end
|
||||||
|
vehicle:cpStartStopDriver(true)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Id'er der skal startes nu (nil = ingen gemt liste). Listen bruges kun én gang.
|
||||||
|
function ADCpResume.takeToStart(adEnv)
|
||||||
|
local list, byId = farmVehicles(adEnv)
|
||||||
|
local ids = ADCpResume.toStart(ADCpResume.pending, list)
|
||||||
|
ADCpResume.pending = nil
|
||||||
|
return ids, byId
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getPath()
|
||||||
|
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||||
|
if path == nil then return nil end
|
||||||
|
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADCpResume.FILE_NAME))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCpResume.save(adEnv)
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil or adEnv == nil then return false end
|
||||||
|
local ids = ADCpResume.activeIds((farmVehicles(adEnv)))
|
||||||
|
local xmlFile = XMLFile.create("ADSmartPickupCpResume", path, ADCpResume.ROOT)
|
||||||
|
if xmlFile == nil then return false end
|
||||||
|
for index, id in ipairs(ids) do
|
||||||
|
xmlFile:setString(string.format("%s.vehicle(%d)#id", ADCpResume.ROOT, index - 1), id)
|
||||||
|
end
|
||||||
|
xmlFile:save()
|
||||||
|
xmlFile:delete()
|
||||||
|
-- gemmes ved hver runs-gem (flere gange i minuttet): log kun når listen ændrer sig
|
||||||
|
local signature = table.concat(ids, ",")
|
||||||
|
if signature ~= ADCpResume.lastLogged then
|
||||||
|
ADCpResume.lastLogged = signature
|
||||||
|
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCpResume.load()
|
||||||
|
ADCpResume.pending = nil
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil or not fileExists(path) then return end
|
||||||
|
local xmlFile = XMLFile.load("ADSmartPickupCpResume", path)
|
||||||
|
if xmlFile == nil then return end
|
||||||
|
local ids = {}
|
||||||
|
xmlFile:iterate(ADCpResume.ROOT .. ".vehicle", function(_, key) table.insert(ids, xmlFile:getString(key .. "#id")) end)
|
||||||
|
xmlFile:delete()
|
||||||
|
ADCpResume.pending = ADCpResume.fromRecords(ids)
|
||||||
|
Logging.info(ADCpResume.LOG_PREFIX .. "%d maskine(r) kørte Courseplay ved sidste gem", #ids)
|
||||||
|
end
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- 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).
|
||||||
|
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed)
|
||||||
|
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 = 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
|
||||||
@@ -18,8 +18,9 @@ ADFieldFleet.ROLE_SPECS = {
|
|||||||
{role = "weeder", spec = "spec_weeder"},
|
{role = "weeder", spec = "spec_weeder"},
|
||||||
{role = "sprayRole"},
|
{role = "sprayRole"},
|
||||||
{role = "mulcher", spec = "spec_mulcher"},
|
{role = "mulcher", spec = "spec_mulcher"},
|
||||||
{role = "cultivator", spec = "spec_cultivator"},
|
-- plov før kultivator: FS25-plove har også spec_cultivator (in-game 28/9: POV 5 XL blev "kultivator")
|
||||||
{role = "plow", spec = "spec_plow"},
|
{role = "plow", spec = "spec_plow"},
|
||||||
|
{role = "cultivator", spec = "spec_cultivator"},
|
||||||
{role = "baleCollector", spec = "canBaleCollect"},
|
{role = "baleCollector", spec = "canBaleCollect"},
|
||||||
{role = "unloader", spec = "canUnload"},
|
{role = "unloader", spec = "canUnload"},
|
||||||
}
|
}
|
||||||
@@ -35,12 +36,23 @@ function ADFieldFleet.roleFromSpecs(specs)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Ekstra opgaver en rolle kan tage: pløjning klargør jorden lige så godt som kultivering (ikke omvendt).
|
||||||
|
ADFieldFleet.EXTRA_ROLES = {plow = {plow = true, cultivator = true}}
|
||||||
|
|
||||||
|
function ADFieldFleet.rolesFor(role)
|
||||||
|
local roles = ADFieldFleet.EXTRA_ROLES[role]
|
||||||
|
if roles == nil then return nil end
|
||||||
|
local copy = {}
|
||||||
|
for key, value in pairs(roles) do copy[key] = value end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
-- Kandidater med rollen, sorteret nærmest først, delt i brugbare og afviste (med første grund).
|
-- 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 function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||||
local usable, sawRole, sawAccepted = {}, false, false
|
local usable, sawRole, sawAccepted = {}, false, false
|
||||||
local widthReason, widthArgs = nil, nil
|
local widthReason, widthArgs = nil, nil
|
||||||
for _, candidate in ipairs(candidates) do
|
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
|
sawRole = true
|
||||||
if accept == nil or accept(candidate) then
|
if accept == nil or accept(candidate) then
|
||||||
sawAccepted = true
|
sawAccepted = true
|
||||||
@@ -48,7 +60,12 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
|||||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
||||||
if ok then
|
if ok then
|
||||||
local dx, dz = candidate.x - x, candidate.z - z
|
local dx, dz = candidate.x - x, candidate.z - z
|
||||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz})
|
-- spreder med en anden vare på skal tømmes først: kun hvis ingen anden kan
|
||||||
|
local wrongFill = candidate.filledRole ~= nil and candidate.filledRole ~= role
|
||||||
|
-- fuld af en anden vare: bruges slet ikke til denne opgave (resten hældes kun ud når den er lille)
|
||||||
|
if not (wrongFill and candidate.dumpable == false) then
|
||||||
|
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz, wrongFill = wrongFill})
|
||||||
|
end
|
||||||
elseif widthReason == nil then
|
elseif widthReason == nil then
|
||||||
widthReason, widthArgs = reason, args
|
widthReason, widthArgs = reason, args
|
||||||
end
|
end
|
||||||
@@ -56,7 +73,10 @@ local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(usable, function(a, b) return a.distance < b.distance end)
|
table.sort(usable, function(a, b)
|
||||||
|
if a.wrongFill ~= b.wrongFill then return not a.wrongFill end
|
||||||
|
return a.distance < b.distance
|
||||||
|
end)
|
||||||
local reason, args = nil, nil
|
local reason, args = nil, nil
|
||||||
if not sawRole then
|
if not sawRole then
|
||||||
reason = "noRig"
|
reason = "noRig"
|
||||||
@@ -73,6 +93,43 @@ end
|
|||||||
-- Nærmeste brugbare sæt. Grund når intet passer: noRig (ingen med flueben og rollen), cannotDo (ingen kan
|
-- 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).
|
-- 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".
|
-- 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)
|
function ADFieldFleet.pick(candidates, role, x, z, requiredWidth, accept)
|
||||||
local usable, reason, args = sortCandidates(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
|
if usable[1] ~= nil then return usable[1].candidate end
|
||||||
@@ -103,3 +160,27 @@ function ADFieldFleet.toggle(enabledIds, id)
|
|||||||
copy[id] = (not copy[id]) or nil
|
copy[id] = (not copy[id]) or nil
|
||||||
return copy
|
return copy
|
||||||
end
|
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
|
||||||
@@ -7,29 +7,139 @@ ADFieldJobs = {}
|
|||||||
|
|
||||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||||
ADFieldJobs.TICK_MS = 1000
|
ADFieldJobs.TICK_MS = 1000
|
||||||
|
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
||||||
|
ADFieldJobs.PASSIVE_SLICES = 10
|
||||||
|
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
||||||
|
ADFieldJobs.SLOW_TICK_MS = 3
|
||||||
|
ADFieldJobs.tickCount = 0
|
||||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
||||||
ADFieldJobs.MAX_PASSES = 2
|
ADFieldJobs.MAX_PASSES = 2
|
||||||
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
||||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all"}
|
-- 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;
|
-- 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å)
|
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||||
ADFieldJobs.courses = {}
|
ADFieldJobs.courses = {}
|
||||||
ADFieldJobs.finished = {}
|
ADFieldJobs.finished = {}
|
||||||
ADFieldJobs.timer = 0
|
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
|
||||||
|
ADFieldJobs.timer = 500
|
||||||
ADFieldJobs.now = 0
|
ADFieldJobs.now = 0
|
||||||
ADFieldJobs.loaded = false
|
ADFieldJobs.loaded = false
|
||||||
|
|
||||||
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang)
|
||||||
|
ADFieldJobs.ACTIVE_PHASES = {fetching = true, driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true}
|
||||||
|
|
||||||
|
-- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente.
|
||||||
|
ADFieldJobs.REGROW_RATIO = 0.1
|
||||||
|
|
||||||
|
-- Trin efter høsten der ødelægger/ignorerer en stående afgrøde.
|
||||||
|
-- Klargøring der kan måles på jorden: trin -> info-felt (andel af marken). Kultivering er gjort når jorden er
|
||||||
|
-- klargjort (kultiveret/pløjet/såbed/stubbearbejdet); pløjning først når den er pløjet.
|
||||||
|
ADFieldJobs.GROUND_PREP_STAGES = {cultivating = "prepared", plowing = "plowed"}
|
||||||
|
ADFieldJobs.PREPARED_DONE_RATIO = 0.95
|
||||||
|
|
||||||
|
ADFieldJobs.PREP_STAGES = {strawCollecting = true, baling = true, baleCollecting = true, mulching = true, liming = true,
|
||||||
|
plowing = true, cultivating = true, sowing = true, rolling = true}
|
||||||
|
|
||||||
-- ---- ren beslutning ----
|
-- ---- ren beslutning ----
|
||||||
|
|
||||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar, eller under 5 % af HELE
|
||||||
|
-- marken står (rester i hjørnerne). readyRatio regner kun med den afgrøde der er tilbage, så en halvt høstet mark
|
||||||
|
-- lignede "væk" (in-game 27/9 F111 gik til kultivering med halvdelen stående).
|
||||||
|
ADFieldJobs.HARVEST_DONE_RATIO = 0.05
|
||||||
local function cropGone(stage, info)
|
local function cropGone(stage, info)
|
||||||
if info == nil then return false end
|
if info == nil then return false end
|
||||||
if stage == "harvesting" and info.kind == "bare" then return true 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
|
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
||||||
end
|
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
|
-- 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).
|
-- 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.
|
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
||||||
@@ -47,14 +157,20 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
|||||||
end
|
end
|
||||||
return {action = "start"}
|
return {action = "start"}
|
||||||
end
|
end
|
||||||
-- mål-afgrøde på en bar mark: klargør og så så snart målet kan sås
|
-- 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 info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
||||||
local plantable = env ~= nil and env.plantable ~= nil and env.plantable(field.plan.targetFruit) or false
|
if ADFieldModel.shouldStartBare(field, "bare") then
|
||||||
if ADFieldModel.shouldStartBare(field, "bare", plantable) then
|
|
||||||
if not hasMarker then return {action = "wait"} end
|
if not hasMarker then return {action = "wait"} end
|
||||||
return {action = "start"}
|
return {action = "start"}
|
||||||
end
|
end
|
||||||
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
|
if not ADFieldModel.isWorking(field) then
|
||||||
local care = ADFieldModel.careNeed(field, info)
|
local care = ADFieldModel.careNeed(field, info)
|
||||||
if care == nil then return {action = "none"} end
|
if care == nil then return {action = "none"} end
|
||||||
@@ -62,7 +178,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
|||||||
if not hasMarker then return {action = "wait", care = care} end
|
if not hasMarker then return {action = "wait", care = care} end
|
||||||
return {action = "care", stage = care}
|
return {action = "care", stage = care}
|
||||||
end
|
end
|
||||||
local active, finished, helpers = 0, 0, 0
|
local active, finished, helpers, awaiting = 0, 0, 0, 0
|
||||||
for _, assignment in ipairs(assignments) do
|
for _, assignment in ipairs(assignments) do
|
||||||
if assignment.stage == nil or assignment.stage == field.stage then
|
if assignment.stage == nil or assignment.stage == field.stage then
|
||||||
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||||
@@ -70,6 +186,7 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
|||||||
if isActive then helpers = helpers + 1 end
|
if isActive then helpers = helpers + 1 end
|
||||||
else
|
else
|
||||||
if isActive then active = active + 1 end
|
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
|
if assignment.phase == "finished" then finished = finished + 1 end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -78,10 +195,21 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
|||||||
if active == 0 then
|
if active == 0 then
|
||||||
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
-- 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
|
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
|
if finished > 0 then
|
||||||
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
||||||
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
||||||
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 not hasMarker then return {action = "wait"} end
|
||||||
if (field.retryAt or 0) > now then return {action = "none"} end
|
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||||
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
||||||
@@ -91,7 +219,8 @@ function ADFieldJobs.decide(field, info, assignments, now, env)
|
|||||||
end
|
end
|
||||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||||
local helperRole = ADFieldModel.helperFor(field.stage)
|
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||||
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
-- tømmevogne først når en høster faktisk er i gang (ikke mens spilleren kører den derud)
|
||||||
|
if helperRole ~= nil and active > awaiting and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||||
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
||||||
end
|
end
|
||||||
return {action = "none"}
|
return {action = "none"}
|
||||||
@@ -134,6 +263,9 @@ function ADFieldJobs.load()
|
|||||||
if ok then
|
if ok then
|
||||||
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
||||||
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
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
|
else
|
||||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
||||||
end
|
end
|
||||||
@@ -145,6 +277,30 @@ function ADFieldJobs.setPlan(id, changes)
|
|||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- "Start nu" på en mark hvor moden venter på spilleren (manuel transport): overtag uden at vente
|
||||||
|
ADFieldJobs.takeoverRequested = {}
|
||||||
|
function ADFieldJobs.requestTakeover(id)
|
||||||
|
ADFieldJobs.takeoverRequested[id] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Venter moden på at spilleren kører en høster til marken? (assignment eller nil)
|
||||||
|
function ADFieldJobs.awaitingPlayer(fieldId)
|
||||||
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||||
|
if assignment.fieldId == fieldId and assignment.phase == "awaitingPlayer" then return assignment end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Under global pause: kun denne mark genoptages (de andre venter på Start alle). nil = pause igen.
|
||||||
|
function ADFieldJobs.setFieldResumed(id, isOn)
|
||||||
|
ADFieldJobs.state.resumed = ADFieldJobs.state.resumed or {}
|
||||||
|
ADFieldJobs.state.resumed[id] = isOn ~= false or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldJobs.isFieldResumed(id)
|
||||||
|
return (ADFieldJobs.state.resumed or {})[id] == true
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldJobs.requestStart(id)
|
function ADFieldJobs.requestStart(id)
|
||||||
ADFieldJobs.note(id, {startRequested = true})
|
ADFieldJobs.note(id, {startRequested = true})
|
||||||
end
|
end
|
||||||
@@ -153,8 +309,8 @@ function ADFieldJobs.forceReady(id)
|
|||||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.setTarget(id, fruitName)
|
function ADFieldJobs.setTarget(id, fruitName, source)
|
||||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName, source))
|
||||||
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
end
|
||||||
@@ -164,6 +320,19 @@ function ADFieldJobs.setFleetMode(mode)
|
|||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
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)
|
function ADFieldJobs.toggleFleet(vehicleId)
|
||||||
ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
|
ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
@@ -226,12 +395,16 @@ end
|
|||||||
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||||
ADFieldJobs.lastDecision = {}
|
ADFieldJobs.lastDecision = {}
|
||||||
|
|
||||||
local function stepField(adEnv, info)
|
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
ADFieldJobs.waitingSince = {}
|
||||||
ADFieldJobs.setField(field)
|
|
||||||
|
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)
|
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment())
|
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
|
||||||
ADFieldJobs.lastDecision[info.id] = decision
|
ADFieldJobs.lastDecision[info.id] = decision
|
||||||
if decision.action == "start" then
|
if decision.action == "start" then
|
||||||
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
|
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
|
||||||
@@ -239,13 +412,26 @@ local function stepField(adEnv, info)
|
|||||||
ADFieldJobs.setField(started)
|
ADFieldJobs.setField(started)
|
||||||
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
||||||
ADFieldJobs.save()
|
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
|
elseif decision.action == "care" then
|
||||||
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
||||||
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
elseif decision.action == "advance" then
|
elseif decision.action == "advance" then
|
||||||
|
-- høsteren har stadig korn i tanken: CP genstartes og venter ved kursets ende, tømmevognene kommer
|
||||||
|
-- (marken bliver på høst til tanken er tom) — in-game 27/9 F15
|
||||||
|
if field.stage == "harvesting" and ADFieldWork.tryEmptyHarvester ~= nil then
|
||||||
|
local okEmpty, emptying = pcall(ADFieldWork.tryEmptyHarvester, adEnv, field, info)
|
||||||
|
if okEmpty and emptying then return end
|
||||||
|
end
|
||||||
local nextField = ADFieldModel.advance(field, info.needs)
|
local nextField = ADFieldModel.advance(field, info.needs)
|
||||||
ADFieldJobs.setField(nextField)
|
ADFieldJobs.setField(nextField)
|
||||||
|
ADFieldJobs.waitingSince[info.id] = nil
|
||||||
ADFieldJobs.forgetFinished(info.id)
|
ADFieldJobs.forgetFinished(info.id)
|
||||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||||
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||||
@@ -253,23 +439,58 @@ local function stepField(adEnv, info)
|
|||||||
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
||||||
end
|
end
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
elseif decision.action == "wait" and decision.reason == "rain" then
|
elseif decision.action == "wait" and decision.reason ~= nil then
|
||||||
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
local key = "spu_field_reason_" .. decision.reason
|
||||||
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
if field.reason == nil or field.reason.key ~= key then
|
||||||
|
ADFieldJobs.note(info.id, {reason = {key = key}})
|
||||||
end
|
end
|
||||||
elseif decision.action == "dispatch" then
|
elseif decision.action == "dispatch" then
|
||||||
|
-- én udsendelse ad gangen på tværs af markerne: marken prøver igen ved næste tick
|
||||||
|
if not ADFieldJobs.mayDispatch(ADFieldJobs.now, ADFieldJobs.lastDispatchAt) then return end
|
||||||
|
local before = ADFieldJobs.assignmentCount()
|
||||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
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
|
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)
|
function ADFieldJobs.tick(adEnv, dt)
|
||||||
ADFieldJobs.now = ADFieldJobs.now + dt
|
ADFieldJobs.now = ADFieldJobs.now + dt
|
||||||
ADFieldJobs.timer = ADFieldJobs.timer + 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
|
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
||||||
ADFieldJobs.timer = 0
|
ADFieldJobs.timer = 0
|
||||||
|
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
||||||
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
||||||
local farmId = g_currentMission:getFarmId()
|
local t0 = clockMs()
|
||||||
ADFields.scanNext(adEnv, farmId)
|
local t1 = t0
|
||||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||||
if not ok then
|
if not ok then
|
||||||
@@ -277,8 +498,34 @@ function ADFieldJobs.tick(adEnv, dt)
|
|||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, info in ipairs(ADFields.list()) do
|
local t2 = clockMs()
|
||||||
local ok, err = pcall(stepField, adEnv, info)
|
-- 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
|
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ local DEFAULT_PLAN = {
|
|||||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||||
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
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}
|
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)
|
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
||||||
@@ -61,6 +63,8 @@ local function copyPlan(plan)
|
|||||||
for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end
|
for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end
|
||||||
for key, value in pairs(plan or {}) 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)
|
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
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,6 +91,8 @@ end
|
|||||||
|
|
||||||
-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst.
|
-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst.
|
||||||
function ADFieldModel.product(plan, kind)
|
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
|
if plan.product ~= nil and plan.product ~= "auto" then return plan.product end
|
||||||
return kind == "mowable" and "silage" or "harvest"
|
return kind == "mowable" and "silage" or "harvest"
|
||||||
end
|
end
|
||||||
@@ -154,9 +160,12 @@ end
|
|||||||
|
|
||||||
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
|
-- 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.
|
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
|
||||||
function ADFieldModel.setTarget(field, fruitName)
|
function ADFieldModel.setTarget(field, fruitName, source)
|
||||||
local copy = copyField(field)
|
local copy = copyField(field)
|
||||||
copy.plan.targetFruit = fruitName or ""
|
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"
|
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
||||||
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
||||||
copy.stage = "idle"
|
copy.stage = "idle"
|
||||||
@@ -164,12 +173,14 @@ function ADFieldModel.setTarget(field, fruitName)
|
|||||||
return copy
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Bar mark med mål-afgrøde starter selv klargøring + såning, når målet kan sås nu.
|
-- Bar mark med mål-afgrøde starter selv klargøring + såning med det samme: klargøringen (kalk, plov,
|
||||||
function ADFieldModel.shouldStartBare(field, kind, plantableNow)
|
-- 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
|
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
|
-- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning
|
||||||
if kind ~= "bare" or field.stage ~= "idle" then return false end
|
if field.held and not field.startRequested then return false end
|
||||||
return plantableNow == true
|
return kind == "bare" and field.stage == "idle"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
|
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
|
||||||
@@ -223,17 +234,28 @@ function ADFieldModel.lanePositions(n)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os.
|
-- 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)
|
function ADFieldModel.onReadiness(field, ratio)
|
||||||
local copy = copyField(field)
|
|
||||||
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
||||||
|
local copy = copyField(field)
|
||||||
copy.stage = "ready"
|
copy.stage = "ready"
|
||||||
|
return copy
|
||||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
|
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)
|
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
|
||||||
|
local copy = copyField(field)
|
||||||
copy.stage = "idle"
|
copy.stage = "idle"
|
||||||
copy.startRequested = false
|
copy.startRequested = false
|
||||||
end
|
|
||||||
return copy
|
return copy
|
||||||
end
|
end
|
||||||
|
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)
|
function ADFieldModel.shouldStart(field)
|
||||||
return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true)
|
return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true)
|
||||||
@@ -263,6 +285,7 @@ end
|
|||||||
-- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den.
|
-- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den.
|
||||||
function ADFieldModel.start(field, kind, needs, currentFruit)
|
function ADFieldModel.start(field, kind, needs, currentFruit)
|
||||||
local copy = copyField(field)
|
local copy = copyField(field)
|
||||||
|
copy.held = nil
|
||||||
copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit)
|
copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit)
|
||||||
local stages = ADFieldModel.workStages(copy.plan, kind, needs)
|
local stages = ADFieldModel.workStages(copy.plan, kind, needs)
|
||||||
copy.stage = stages[1] or "done"
|
copy.stage = stages[1] or "done"
|
||||||
@@ -306,6 +329,8 @@ function ADFieldModel.stop(field)
|
|||||||
end
|
end
|
||||||
copy.startRequested = false
|
copy.startRequested = false
|
||||||
copy.reason = nil
|
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
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -349,7 +374,16 @@ function ADFieldModel.startCare(field, stage)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde.
|
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde.
|
||||||
|
-- Under dette er bredden ikke læst rigtigt (spreder uden AI-markører gav 0,5 m): Courseplay laver så et
|
||||||
|
-- kursus med rækker hver halve meter — på en 72 ha mark fryser spillet (in-game 26/9, F108).
|
||||||
|
ADFieldModel.MIN_WORK_WIDTH = 1.5
|
||||||
|
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
||||||
|
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
||||||
|
|
||||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||||
|
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
||||||
|
return false, "widthUnknown", {width}
|
||||||
|
end
|
||||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||||
end
|
end
|
||||||
@@ -428,10 +462,12 @@ function ADFieldModel.toRecord(field)
|
|||||||
return {
|
return {
|
||||||
id = field.id, stage = field.stage, mode = plan.mode, product = plan.product,
|
id = field.id, stage = field.stage, mode = plan.mode, product = plan.product,
|
||||||
rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "),
|
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),
|
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),
|
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||||
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
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, " "),
|
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||||
mowWidth = field.mowWidth or 0,
|
mowWidth = field.mowWidth or 0,
|
||||||
mowVehicle = field.mowVehicleId or "",
|
mowVehicle = field.mowVehicleId or "",
|
||||||
@@ -459,6 +495,7 @@ function ADFieldModel.fromRecord(record)
|
|||||||
plan.product = PRODUCTS[record.product] and record.product or "auto"
|
plan.product = PRODUCTS[record.product] and record.product or "auto"
|
||||||
plan.rake = readFlag(record.rake, false)
|
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.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.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||||
plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||||
plan.mulch = readFlag(record.mulch, false)
|
plan.mulch = readFlag(record.mulch, false)
|
||||||
@@ -471,6 +508,9 @@ function ADFieldModel.fromRecord(record)
|
|||||||
plan.weed = readFlag(record.weed, true)
|
plan.weed = readFlag(record.weed, true)
|
||||||
plan.targetFruit = record.targetFruit or ""
|
plan.targetFruit = record.targetFruit or ""
|
||||||
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
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"
|
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
||||||
field.kind = readText(record.kind)
|
field.kind = readText(record.kind)
|
||||||
field.lastFruit = readText(record.lastFruit)
|
field.lastFruit = readText(record.lastFruit)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,7 +12,14 @@ ADFieldStorage.FIELD_TYPES = {
|
|||||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||||
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
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",
|
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()
|
function ADFieldStorage.getPath()
|
||||||
@@ -36,6 +43,21 @@ function ADFieldStorage.readRecord(xmlFile, key)
|
|||||||
return record
|
return record
|
||||||
end
|
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)
|
function ADFieldStorage.save(state)
|
||||||
local path = ADFieldStorage.getPath()
|
local path = ADFieldStorage.getPath()
|
||||||
if path == nil then return false end
|
if path == nil then return false end
|
||||||
@@ -47,23 +69,35 @@ function ADFieldStorage.save(state)
|
|||||||
index = index + 1
|
index = index + 1
|
||||||
end
|
end
|
||||||
xmlFile:setString(ADFieldStorage.ROOT .. "#fleetMode", state.fleetMode or "all")
|
xmlFile:setString(ADFieldStorage.ROOT .. "#fleetMode", state.fleetMode or "all")
|
||||||
|
xmlFile:setInt(ADFieldStorage.ROOT .. "#paused", state.paused == true and 1 or 0)
|
||||||
local vehicleIndex = 0
|
local vehicleIndex = 0
|
||||||
for vehicleId in pairs(state.fleet) do
|
for vehicleId in pairs(state.fleet) do
|
||||||
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
|
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
|
||||||
vehicleIndex = vehicleIndex + 1
|
vehicleIndex = vehicleIndex + 1
|
||||||
end
|
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:save()
|
||||||
xmlFile:delete()
|
xmlFile:delete()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldStorage.load()
|
function ADFieldStorage.load()
|
||||||
local result = {fields = {}, fleet = {}, fleetMode = "all"}
|
local result = {fields = {}, fleet = {}, fleetMode = "all", courseRecords = {}, manual = {}, paused = false}
|
||||||
local path = ADFieldStorage.getPath()
|
local path = ADFieldStorage.getPath()
|
||||||
if path == nil or not fileExists(path) then return result end
|
if path == nil or not fileExists(path) then return result end
|
||||||
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
||||||
if xmlFile == nil then return result end
|
if xmlFile == nil then return result end
|
||||||
result.fleetMode = xmlFile:getString(ADFieldStorage.ROOT .. "#fleetMode") == "ticked" and "ticked" or "all"
|
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)
|
xmlFile:iterate(ADFieldStorage.ROOT .. ".field", function(_, key)
|
||||||
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
||||||
if field.id ~= nil then result.fields[field.id] = field end
|
if field.id ~= nil then result.fields[field.id] = field end
|
||||||
@@ -72,6 +106,14 @@ function ADFieldStorage.load()
|
|||||||
local id = xmlFile:getString(key .. "#id")
|
local id = xmlFile:getString(key .. "#id")
|
||||||
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
if id ~= nil and id ~= "" then result.fleet[id] = true end
|
||||||
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()
|
xmlFile:delete()
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,3 +37,75 @@ function ADFieldView.soilParts(info)
|
|||||||
if (info.weed or 0) >= ADFieldModel.WEED_MIN then table.insert(parts, "weed") end
|
if (info.weed or 0) >= ADFieldModel.WEED_MIN then table.insert(parts, "weed") end
|
||||||
return parts
|
return parts
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -15,11 +15,19 @@ ADFieldWork.PLAYER_DRIVING_KMH = 3
|
|||||||
ADFieldWork.WINDROW_MEASURE_MS = 10000
|
ADFieldWork.WINDROW_MEASURE_MS = 10000
|
||||||
-- mangler der sæt, prøves igen efter så lang tid (ikke hvert sekund over hele flåden)
|
-- mangler der sæt, prøves igen efter så lang tid (ikke hvert sekund over hele flåden)
|
||||||
ADFieldWork.NO_RIG_RETRY_MS = 30000
|
ADFieldWork.NO_RIG_RETRY_MS = 30000
|
||||||
|
-- AutoDrive der stopper før markøren startes igen så mange gange, før turen opgives
|
||||||
|
ADFieldWork.DRIVE_RETRIES = 2
|
||||||
|
-- længere fra AD-vejnettet end dette kan AutoDrive ikke finde en rute ("Could not calculate path", in-game 26/9:
|
||||||
|
-- kultivatoren stod 666 m fra nærmeste waypoint); sådan en maskine sendes ikke ud
|
||||||
|
ADFieldWork.MAX_OFF_NETWORK = 60
|
||||||
|
-- flyttede køretøjet sig mindre end dette før AD stoppede, fandt AD ingen rute — ingen genforsøg
|
||||||
|
ADFieldWork.NO_MOVE_DISTANCE = 5
|
||||||
ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true}
|
ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true}
|
||||||
|
|
||||||
local function log(...) ADFieldJobs.log(...) end
|
local function log(...) ADFieldJobs.log(...) end
|
||||||
local function note(...) ADFieldJobs.note(...) end
|
local function note(...) ADFieldJobs.note(...) end
|
||||||
local function now() return ADFieldJobs.now end
|
local function now() return ADFieldJobs.now end
|
||||||
|
local lastNoRig = {}
|
||||||
|
|
||||||
-- ---- AutoDrive ----
|
-- ---- AutoDrive ----
|
||||||
|
|
||||||
@@ -57,12 +65,18 @@ local function helperCp(adEnv)
|
|||||||
return adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
return adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local function startAd(adEnv, vehicle, mode, firstId, secondId)
|
-- fillType (valgfri, LOAD): varen AutoDrive skal læsse — uden den kører AD til påfyldningen og hjem tom
|
||||||
|
-- (in-game 26/9: sprøjten kom tom tilbage fra Buying Station Multifruit)
|
||||||
|
local function startAd(adEnv, vehicle, mode, firstId, secondId, fillType)
|
||||||
local module = stateModule(vehicle)
|
local module = stateModule(vehicle)
|
||||||
stopAd(vehicle)
|
stopAd(vehicle)
|
||||||
module:setMode(mode)
|
module:setMode(mode)
|
||||||
module:setFirstMarkerByWayPointId(firstId)
|
module:setFirstMarkerByWayPointId(firstId)
|
||||||
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
||||||
|
if fillType ~= nil then
|
||||||
|
module:setFillType(fillType)
|
||||||
|
module.selectedFillTypes = {fillType}
|
||||||
|
end
|
||||||
module:setStartHelper(false)
|
module:setStartHelper(false)
|
||||||
if module.setActualFarmId ~= nil then module:setActualFarmId(vehicle:getOwnerFarmId()) end
|
if module.setActualFarmId ~= nil then module:setActualFarmId(vehicle:getOwnerFarmId()) end
|
||||||
module:raiseDirtyFlag()
|
module:raiseDirtyFlag()
|
||||||
@@ -75,14 +89,27 @@ local function startAd(adEnv, vehicle, mode, firstId, secondId)
|
|||||||
wake(vehicle, true)
|
wake(vehicle, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADFieldWork.startAd = startAd
|
||||||
|
ADFieldWork.enableParkAtJobFinished = enableParkAtJobFinished
|
||||||
|
|
||||||
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
||||||
local function parkMarkerId(adEnv, vehicle)
|
local function parkMarkerId(adEnv, vehicle)
|
||||||
local module = stateModule(vehicle)
|
local module = stateModule(vehicle)
|
||||||
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
||||||
|
-- parkeringen kan ligge på vognen/redskabet (advd.parkDestination); AD kopierer den først til traktoren når
|
||||||
|
-- redskabet kobles på/vælges — efter indlæsning er kopien ofte tom (in-game 27/9: 20 af 25 "uden parkering")
|
||||||
|
if index == nil or index < 1 then
|
||||||
|
local ok, units = pcall(adEnv.AutoDrive.getAllUnits, vehicle)
|
||||||
|
for _, unit in ipairs(ok and units or {}) do
|
||||||
|
local own = unit.advd ~= nil and unit.advd.parkDestination or nil
|
||||||
|
if type(own) == "number" and own >= 1 then index = own break end
|
||||||
|
end
|
||||||
|
end
|
||||||
if index == nil or index < 1 then return nil end
|
if index == nil or index < 1 then return nil end
|
||||||
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
return marker ~= nil and marker.id or nil
|
return marker ~= nil and marker.id or nil
|
||||||
end
|
end
|
||||||
|
ADFieldWork.parkMarkerId = parkMarkerId
|
||||||
|
|
||||||
local function restLoad(adEnv, vehicle)
|
local function restLoad(adEnv, vehicle)
|
||||||
local ok, level = pcall(function()
|
local ok, level = pcall(function()
|
||||||
@@ -102,6 +129,16 @@ end
|
|||||||
|
|
||||||
-- ---- flåde ----
|
-- ---- flåde ----
|
||||||
|
|
||||||
|
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
||||||
|
-- eller på hjemtur fra puljen (sætter redskab af).
|
||||||
|
local function isBusyVehicle(vehicle, id)
|
||||||
|
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
||||||
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||||
|
or (ADFlowWagons ~= nil and ADFlowWagons.isEmptying(vehicle))
|
||||||
|
end
|
||||||
|
|
||||||
|
ADFieldWork.isBusyVehicle = isBusyVehicle
|
||||||
|
|
||||||
function ADFieldWork.getRigs(adEnv)
|
function ADFieldWork.getRigs(adEnv)
|
||||||
local rigs = {}
|
local rigs = {}
|
||||||
local state = ADFieldJobs.state
|
local state = ADFieldJobs.state
|
||||||
@@ -109,17 +146,27 @@ function ADFieldWork.getRigs(adEnv)
|
|||||||
local specs = ADCpBridge.getSpecs(vehicle)
|
local specs = ADCpBridge.getSpecs(vehicle)
|
||||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||||
local id = ADRunsController.getVehicleId(vehicle)
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
if role ~= nil and id ~= nil then
|
-- traktorer i et almindeligt flow (æg, gylle, foder ...) hører til den drift og er ikke markflåde
|
||||||
|
-- (et stoppet flows traktorer er ledige — maskinpulje/flow-vogne)
|
||||||
|
if role ~= nil and id ~= nil and (ADPool ~= nil and ADPool.isFreeFromFlows(vehicle) or ADRunsController.getRunForVehicle(vehicle) == nil) then
|
||||||
local x, z = vehiclePosition(vehicle)
|
local x, z = vehiclePosition(vehicle)
|
||||||
local rig = {
|
local rig = {
|
||||||
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
||||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
||||||
-- optaget: vores egen opgave, et Smart Pickup-flow, eller AutoDrive/Courseplay kører den allerede
|
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens
|
||||||
-- (fx spillerens egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
-- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
||||||
busy = state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil
|
busy = isBusyVehicle(vehicle, id),
|
||||||
or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
|
-- hvorfor optaget (til loggen, når ingen maskine kan sendes ud)
|
||||||
|
busyWhy = (state.assignments[id] ~= nil and ("opgave F" .. tostring(state.assignments[id].fieldId)))
|
||||||
|
or (isAdActive(vehicle) and "AutoDrive kører") or (ADCpBridge.isWorking(vehicle) and "Courseplay kører")
|
||||||
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle) and "lånt af marksilo-flow")
|
||||||
|
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
||||||
controlled = isControlledByPlayer(vehicle),
|
controlled = isControlledByPlayer(vehicle),
|
||||||
|
-- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord)
|
||||||
|
manual = (state.manual or {})[id] == true,
|
||||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||||
|
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
||||||
|
roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), filledRole = specs.filledRole, dumpable = specs.dumpable,
|
||||||
}
|
}
|
||||||
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
||||||
if role == "seeder" then
|
if role == "seeder" then
|
||||||
@@ -242,7 +289,12 @@ end
|
|||||||
function ADFieldWork.release(assignment, phase)
|
function ADFieldWork.release(assignment, phase)
|
||||||
restoreToolOffset(assignment)
|
restoreToolOffset(assignment)
|
||||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||||
|
-- henter stadig puljeredskabet: afbryd koblingen, så det ikke kobles på uden en opgave
|
||||||
|
if assignment.phase == "fetching" and assignment.hitch ~= nil and not assignment.hitch.done and ADHitch ~= nil then
|
||||||
|
ADHitch.cancel(assignment.vehicle)
|
||||||
|
end
|
||||||
assignment.phase = phase or "released"
|
assignment.phase = phase or "released"
|
||||||
|
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
||||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -253,10 +305,33 @@ local function fail(fieldId, text)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
||||||
|
-- Afstand fra (x, z) til nærmeste AD-waypoint (nil hvis vejnettet ikke kan læses).
|
||||||
|
function ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||||
|
-- gemmes 30 s pr. 10 m-celle (15.000 waypoints pr. opslag)
|
||||||
|
if ADCacheGet ~= nil then
|
||||||
|
local key = string.format("%d:%d", math.floor(x / 10), math.floor(z / 10))
|
||||||
|
return ADCacheGet("networkDistance", key, 30000, function() return ADFieldWork.distanceToNetworkUncached(adEnv, x, z) end)
|
||||||
|
end
|
||||||
|
return ADFieldWork.distanceToNetworkUncached(adEnv, x, z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.distanceToNetworkUncached(adEnv, x, z)
|
||||||
|
local ok, best = pcall(function()
|
||||||
|
local wayPoints = adEnv.ADGraphManager:getWayPoints()
|
||||||
|
local bestD = math.huge
|
||||||
|
for _, wp in pairs(wayPoints or {}) do
|
||||||
|
local d = (wp.x - x) ^ 2 + (wp.z - z) ^ 2
|
||||||
|
if d < bestD then bestD = d end
|
||||||
|
end
|
||||||
|
return math.sqrt(bestD)
|
||||||
|
end)
|
||||||
|
return ok and best ~= math.huge and best or nil
|
||||||
|
end
|
||||||
|
|
||||||
local function acceptFor(field, info, role)
|
local function acceptFor(field, info, role)
|
||||||
if role == "harvester" then
|
if role == "harvester" then
|
||||||
local fruit = info.fruitIndex
|
local fruit = info.fruitIndex
|
||||||
return function(candidate) return fruit ~= nil and candidate.fruits ~= nil and candidate.fruits[fruit] == true end
|
return function(candidate) return ADManualTransport.acceptsHarvester(candidate, fruit) end
|
||||||
elseif role == "seeder" then
|
elseif role == "seeder" then
|
||||||
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
||||||
return function(candidate) return candidate.hasSeeds and fruitIndex ~= nil and candidate.seedFruits[fruitIndex] == true end
|
return function(candidate) return candidate.hasSeeds and fruitIndex ~= nil and candidate.seedFruits[fruitIndex] == true end
|
||||||
@@ -286,6 +361,81 @@ local function preflight(field, role)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true}
|
||||||
|
|
||||||
|
local function fillTypeName(fillType)
|
||||||
|
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Før udsendelse: forkert vare af (kalk på, skal gøde), og er sættet tomt, findes påfyldningen.
|
||||||
|
-- Returnerer {fillType, empty, refillId} eller nil (sættet bruger intet fyld: strigle, plov ...).
|
||||||
|
local function refillPlan(adEnv, fieldId, vehicle, role, x, z)
|
||||||
|
if not ADFieldWork.REFILL_ROLES[role] then return nil end
|
||||||
|
local dumped, dumpedName = ADCpBridge.emptySprayerIfWrong(vehicle, role)
|
||||||
|
if dumped > 0 then
|
||||||
|
log(fieldId, "'%s' tømt for %.0f l %s (skal bruges til %s)", ADRuns.vehicleLabel(vehicle), dumped, tostring(dumpedName), tostring(role))
|
||||||
|
end
|
||||||
|
local fillType = ADCpBridge.getRefillFillType(vehicle, role)
|
||||||
|
if fillType == nil then return nil end
|
||||||
|
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
|
||||||
|
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
|
||||||
|
return {fillType = fillType, empty = empty, refillId = refillId}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Manuel transport: bed spilleren køre høsteren til marken (dialog første gang, ellers besked i hjørnet).
|
||||||
|
local function askPlayer(field, info, assignment, withDialog)
|
||||||
|
local text = string.format("F%s skal høstes: kør '%s' til %s, montér skærebordet og stig ud — så overtager Smart Pickup",
|
||||||
|
tostring(field.id), ADRuns.vehicleLabel(assignment.vehicle), tostring(info.markerName or ("F" .. tostring(field.id))))
|
||||||
|
assignment.remindedAt = now()
|
||||||
|
log(field.id, "venter på spilleren: %s", text)
|
||||||
|
if withDialog and InfoDialog ~= nil and InfoDialog.show ~= nil then
|
||||||
|
pcall(InfoDialog.show, "Smart Pickup: " .. text)
|
||||||
|
else
|
||||||
|
ADFieldJobs.notify(text, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
||||||
|
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
||||||
|
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept, decision)
|
||||||
|
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
||||||
|
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
||||||
|
end)
|
||||||
|
local accept = function(implement)
|
||||||
|
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
||||||
|
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
||||||
|
if role == "seeder" then
|
||||||
|
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
||||||
|
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
||||||
|
end
|
||||||
|
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
||||||
|
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
||||||
|
-- tømmevogn: skal kunne bære høstens vare
|
||||||
|
if role == "unloader" and not ADPool.canCarry(implement, info.harvestFillType) then return false end
|
||||||
|
local required = ADFieldModel.requiredWidth(field, role)
|
||||||
|
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
||||||
|
end
|
||||||
|
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
||||||
|
if pair == nil then return false, why, args end
|
||||||
|
local tractor, implement = pair.tractor, pair.implement
|
||||||
|
local assignment = {
|
||||||
|
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||||
|
helper = decision ~= nil and decision.helper == true, phase = "fetching", since = now(), width = implement.width,
|
||||||
|
groupSize = 1, leader = true, fillType = decision ~= nil and decision.helper and info.harvestFillType or nil,
|
||||||
|
startX = tractor.x, startZ = tractor.z,
|
||||||
|
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
||||||
|
hitch = {done = false},
|
||||||
|
}
|
||||||
|
ADFieldJobs.state.assignments[tractor.id] = assignment
|
||||||
|
ADPool.reserve(assignment, {implement.id, tractor.id})
|
||||||
|
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
||||||
|
assignment.hitch = {done = true, ok = ok, detail = detail}
|
||||||
|
end)
|
||||||
|
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
||||||
|
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldWork.dispatch(adEnv, field, info, decision)
|
function ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
local role = decision.role
|
local role = decision.role
|
||||||
local ok, reason = preflight(field, role)
|
local ok, reason = preflight(field, role)
|
||||||
@@ -293,9 +443,25 @@ function ADFieldWork.dispatch(adEnv, field, info, decision)
|
|||||||
note(field.id, {retryAt = now() + ADFieldModel.RETRY_MS, reason = reason})
|
note(field.id, {retryAt = now() + ADFieldModel.RETRY_MS, reason = reason})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local candidates = ADFieldWork.getRigs(adEnv)
|
-- faste maskiner: andre markers faste bruges ikke; har marken selv faste med rollen, kun dem
|
||||||
|
local rigIdsByField = {}
|
||||||
|
for fieldId, other in pairs(ADFieldJobs.state.fields) do rigIdsByField[fieldId] = other.plan ~= nil and other.plan.rigIds or nil end
|
||||||
|
local candidates = ADFieldFleet.filterDedicated(ADFieldWork.getRigs(adEnv), field.id, role, rigIdsByField)
|
||||||
local required = ADFieldModel.requiredWidth(field, role)
|
local required = ADFieldModel.requiredWidth(field, role)
|
||||||
local accept = acceptFor(field, info, role)
|
local roleAccept = acceptFor(field, info, role)
|
||||||
|
-- kun maskiner AutoDrive kan køre fra: tæt på vejnettet
|
||||||
|
local offNetwork = {}
|
||||||
|
local accept = function(candidate)
|
||||||
|
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
||||||
|
-- manuel transport: spilleren kører den — afstand til AutoDrive-vejnettet er ligegyldig
|
||||||
|
if role == "harvester" and candidate.manual then return true end
|
||||||
|
local d = candidate.x ~= nil and ADFieldWork.distanceToNetwork(adEnv, candidate.x, candidate.z) or nil
|
||||||
|
if d ~= nil and d > ADFieldWork.MAX_OFF_NETWORK then
|
||||||
|
table.insert(offNetwork, string.format("'%s' %.0f m", tostring(candidate.label), d))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
local rigs, why, args
|
local rigs, why, args
|
||||||
if (decision.count or 1) > 1 then
|
if (decision.count or 1) > 1 then
|
||||||
rigs, why, args = ADFieldFleet.pickMany(candidates, role, decision.count, info.x, info.z, required, accept)
|
rigs, why, args = ADFieldFleet.pickMany(candidates, role, decision.count, info.x, info.z, required, accept)
|
||||||
@@ -305,9 +471,51 @@ function ADFieldWork.dispatch(adEnv, field, info, decision)
|
|||||||
rigs = rig ~= nil and {rig} or nil
|
rigs = rig ~= nil and {rig} or nil
|
||||||
end
|
end
|
||||||
if rigs == nil then
|
if rigs == nil then
|
||||||
note(field.id, {reason = {key = "spu_field_reason_" .. tostring(why), role = role, args = args}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
||||||
|
-- marken har egne faste maskiner til rollen: den venter på dem (som uden puljen)
|
||||||
|
local ownIds, hasOwnForRole = {}, false
|
||||||
|
for _, id in ipairs(field.plan ~= nil and field.plan.rigIds or {}) do ownIds[id] = true end
|
||||||
|
for _, candidate in ipairs(candidates) do
|
||||||
|
if ownIds[candidate.id] and (candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])) then hasOwnForRole = true end
|
||||||
|
end
|
||||||
|
if ADPool ~= nil and ADPool.mayUsePool(decision, hasOwnForRole)
|
||||||
|
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
||||||
|
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept, decision)
|
||||||
|
if pooled then return end
|
||||||
|
if poolWhy == "tooWeak" then
|
||||||
|
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til '%s'",
|
||||||
|
(poolArgs or {})[1] or 0, tostring((poolArgs or {})[2] or role))}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if poolWhy == "noPoolTractor" and why ~= "allBusy" then
|
||||||
|
why, args = poolWhy, poolArgs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #offNetwork > 0 and (why == "cannotDo" or why == "noRig") then
|
||||||
|
-- de eneste med rollen står uden for AD-vejnettet
|
||||||
|
note(field.id, {reason = {text = string.format("ingen %s kan køre til %s — står for langt fra AutoDrive-vejnettet (%s)",
|
||||||
|
tostring(role), tostring(info.markerName), table.concat(offNetwork, ", "))}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
note(field.id, {reason = {key = "spu_field_reason_" .. tostring(why), role = role, args = args}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
||||||
|
-- også i loggen (én gang pr. ændring): før stod grunden kun i fanen, og loggen var tavs (in-game 27/9)
|
||||||
|
local noRigKey = tostring(role) .. ":" .. tostring(why)
|
||||||
|
if lastNoRig[field.id] ~= noRigKey then
|
||||||
|
lastNoRig[field.id] = noRigKey
|
||||||
|
-- hvem har rollen, og hvorfor kan de ikke bruges (in-game 27/9: "allBusy" uden at sige hvem)
|
||||||
|
local who = {}
|
||||||
|
for _, candidate in ipairs(candidates) do
|
||||||
|
if candidate.role == role or (candidate.roles ~= nil and candidate.roles[role]) then
|
||||||
|
local state = candidate.controlled and "spiller i førersædet" or candidate.busyWhy
|
||||||
|
or (not candidate.enabled and "ikke i flåden") or (roleAccept ~= nil and not roleAccept(candidate) and "kan ikke (afgrøde/frø)") or "ledig"
|
||||||
|
table.insert(who, string.format("'%s': %s", tostring(candidate.label), state))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
log(field.id, "ingen maskine sendt til %s: %s%s", tostring(role), tostring(why), #who > 0 and (" — " .. table.concat(who, "; ")) or "")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
lastNoRig[field.id] = nil
|
||||||
local count = #rigs
|
local count = #rigs
|
||||||
local lanes = ADFieldModel.lanePositions(count)
|
local lanes = ADFieldModel.lanePositions(count)
|
||||||
for index, rig in ipairs(rigs) do
|
for index, rig in ipairs(rigs) do
|
||||||
@@ -317,11 +525,36 @@ function ADFieldWork.dispatch(adEnv, field, info, decision)
|
|||||||
groupSize = decision.helper and 1 or count, lane = count > 1 and lanes[index] or nil, leader = index == 1,
|
groupSize = decision.helper and 1 or count, lane = count > 1 and lanes[index] or nil, leader = index == 1,
|
||||||
fillType = decision.helper and info.harvestFillType or nil,
|
fillType = decision.helper and info.harvestFillType or nil,
|
||||||
}
|
}
|
||||||
|
if role == "harvester" and rig.manual and not assignment.helper then
|
||||||
|
-- for bred til vejene: spilleren kører den selv til marken og monterer skærebordet
|
||||||
|
assignment.phase, assignment.manual = "awaitingPlayer", true
|
||||||
|
assignment.startX, assignment.startZ = rig.x, rig.z
|
||||||
ADFieldJobs.state.assignments[rig.id] = assignment
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||||
|
askPlayer(field, info, assignment, true)
|
||||||
|
else
|
||||||
|
local refill = refillPlan(adEnv, field.id, rig.vehicle, role, info.x, info.z)
|
||||||
|
if refill ~= nil and refill.empty and refill.refillId == nil then
|
||||||
|
-- tom og intet sted at fylde op: ud på marken ville den bare køre kurset uden at sprede
|
||||||
|
fail(field.id, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op", rig.label, fillTypeName(refill.fillType)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
assignment.startX, assignment.startZ = rig.x, rig.z
|
||||||
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||||
|
if refill ~= nil and refill.empty then
|
||||||
|
-- AutoDrive LOAD: påfyldningen (2. markør), fyld op, videre til F-markøren (1.) — så Courseplay
|
||||||
|
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType)
|
||||||
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType}
|
||||||
|
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refill.refillId)
|
||||||
|
log(field.id, "%s '%s' er tom — fyldes med %s ved %s og kører så til %s", role, rig.label, fillTypeName(refill.fillType),
|
||||||
|
tostring(marker ~= nil and marker.name or refill.refillId), tostring(info.markerName))
|
||||||
|
else
|
||||||
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||||
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
||||||
log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0,
|
log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0,
|
||||||
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
note(field.id, {reason = false})
|
note(field.id, {reason = false})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -350,26 +583,83 @@ end
|
|||||||
-- Nærmeste påfyldning af varen: en læssestation gården har adgang til, med varen på lager, en AutoDrive-
|
-- Nærmeste påfyldning af varen: en læssestation gården har adgang til, med varen på lager, en AutoDrive-
|
||||||
-- markør ved triggeren og vej fra traktoren. (AutoDrives eget "automatiske påfyldningsmål" er slået fra i
|
-- markør ved triggeren og vej fra traktoren. (AutoDrives eget "automatiske påfyldningsmål" er slået fra i
|
||||||
-- AutoDrive — AutoDrive.automaticPickupTarget = false — så vi finder stedet selv, som silovalget.)
|
-- AutoDrive — AutoDrive.automaticPickupTarget = false — så vi finder stedet selv, som silovalget.)
|
||||||
|
-- Sælger købestationen varen? (placeable.spec_buyingStation.buyingStation; FS25-felter varierer, så flere
|
||||||
|
-- opslag: udbudte varer, triggernes varer, AI-varer.)
|
||||||
|
function ADFieldWork.buyingSupports(station, fillType)
|
||||||
|
if station == nil or fillType == nil then return false end
|
||||||
|
local ok, result = pcall(function()
|
||||||
|
for _, key in ipairs({"providedFillTypes", "supportedFillTypes", "fillTypes"}) do
|
||||||
|
local set = station[key]
|
||||||
|
if type(set) == "table" and set[fillType] then return true end
|
||||||
|
end
|
||||||
|
for _, trigger in pairs(station.loadTriggers or {}) do
|
||||||
|
if type(trigger.fillTypes) == "table" and trigger.fillTypes[fillType] then return true end
|
||||||
|
end
|
||||||
|
if station.getAISupportedFillTypes ~= nil then
|
||||||
|
local set = station:getAISupportedFillTypes()
|
||||||
|
if type(set) == "table" and set[fillType] then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
return ok and result == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- AD-markøren ved stationen: AutoDrives eget opslag (30 m fra triggeren), ellers nærmeste ikke-Wait
|
||||||
|
-- markør inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren).
|
||||||
|
local function markerAtStation(adEnv, vehicle, station, placeable)
|
||||||
|
local okAd, wayPointId = pcall(function() return adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 30) end)
|
||||||
|
if okAd and wayPointId ~= nil and wayPointId > 0 then return wayPointId end
|
||||||
|
local node = placeable ~= nil and placeable.rootNode or nil
|
||||||
|
if node == nil then return nil end
|
||||||
|
local px, _, pz = getWorldTranslation(node)
|
||||||
|
local best, bestDistance = nil, 40 * 40
|
||||||
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
|
local isWait = ADSupplyPlanner ~= nil and ADSupplyPlanner.isWaitMarker ~= nil and ADSupplyPlanner.isWaitMarker(marker)
|
||||||
|
local wayPoint = not isWait and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
|
||||||
|
if wayPoint ~= nil then
|
||||||
|
local d = (wayPoint.x - px) ^ 2 + (wayPoint.z - pz) ^ 2
|
||||||
|
if d < bestDistance then best, bestDistance = marker.id, d end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nærmeste påfyldning af varen med AD-markør og vej: lagre/siloer med varen, og købestationer (købes
|
||||||
|
-- efter behov — ingen lagerbeholdning, så de fandtes ikke før; in-game 26/9 "Liquid Buy Station").
|
||||||
function ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z)
|
function ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z)
|
||||||
local farmId = vehicle:getOwnerFarmId()
|
local farmId = vehicle:getOwnerFarmId()
|
||||||
local best, bestDistance = nil, math.huge
|
local best, bestDistance = nil, math.huge
|
||||||
local ok = pcall(function()
|
local function consider(wayPointId)
|
||||||
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
|
|
||||||
local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {}
|
|
||||||
if g_currentMission.accessHandler:canFarmAccess(farmId, station) and supported[fillType]
|
|
||||||
and (station:getFillLevel(fillType, farmId) or 0) > 0 then
|
|
||||||
local wayPointId = adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 10)
|
|
||||||
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
|
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
|
||||||
if wayPoint ~= nil then
|
if wayPoint == nil then return end
|
||||||
local distance = (wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2
|
local distance = (wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2
|
||||||
if distance < bestDistance and ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId) then
|
if distance < bestDistance and ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId) then
|
||||||
best, bestDistance = wayPointId, distance
|
best, bestDistance = wayPointId, distance
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- gårdens egne lagre først (stalde/tanke med gylle, lager med gødning): købes kun når intet eget findes
|
||||||
|
-- (in-game 26/9: 7 mio. l gylle ved BGA'en, men påfyldningen ville købe ved købestationen)
|
||||||
|
pcall(function()
|
||||||
|
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
|
||||||
|
local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {}
|
||||||
|
local isShop = station.owningPlaceable ~= nil and station.owningPlaceable.spec_buyingStation ~= nil
|
||||||
|
if not isShop and g_currentMission.accessHandler:canFarmAccess(farmId, station) and supported[fillType]
|
||||||
|
and (station:getFillLevel(fillType, farmId) or 0) > 0 then
|
||||||
|
consider(markerAtStation(adEnv, vehicle, station, station.owningPlaceable))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
return ok and best or nil
|
if best ~= nil then return best end
|
||||||
|
pcall(function()
|
||||||
|
for _, placeable in pairs(g_currentMission.placeableSystem.placeables) do
|
||||||
|
local spec = placeable.spec_buyingStation
|
||||||
|
local station = spec ~= nil and spec.buyingStation or nil
|
||||||
|
if station ~= nil and ADFieldWork.buyingSupports(station, fillType) then
|
||||||
|
consider(markerAtStation(adEnv, vehicle, station, placeable))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return best
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Påfyldning undervejs (såmaskine/sprøjte/spreder): AutoDrive LOAD med hjælper = Courseplay. Når CP melder
|
-- Påfyldning undervejs (såmaskine/sprøjte/spreder): AutoDrive LOAD med hjælper = Courseplay. Når CP melder
|
||||||
@@ -377,7 +667,7 @@ end
|
|||||||
-- og genstarter CP. Findes ingen påfyldning, stopper sættet når det er tomt ("tom for …", se finish).
|
-- og genstarter CP. Findes ingen påfyldning, stopper sættet når det er tomt ("tom for …", se finish).
|
||||||
local function prepareRefill(adEnv, field, info, assignment)
|
local function prepareRefill(adEnv, field, info, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
local fillType = ADCpBridge.getRefillFillType(vehicle)
|
local fillType = ADCpBridge.getRefillFillType(vehicle, assignment.role)
|
||||||
if fillType == nil then return end
|
if fillType == nil then return end
|
||||||
assignment.refillFillType = fillType
|
assignment.refillFillType = fillType
|
||||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||||
@@ -411,13 +701,16 @@ local function beginUnloader(adEnv, field, info, assignment)
|
|||||||
local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, fillType)
|
local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, fillType)
|
||||||
if store == nil then
|
if store == nil then
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
fail(field.id, "intet lager til høsten: " .. tostring(why))
|
fail(field.id, "intet lager til høsten: " .. tostring(why))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
assignment.storeId = store.id
|
assignment.storeId = store.id
|
||||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, store.id, vehicle:getOwnerFarmId())
|
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, store.id, vehicle:getOwnerFarmId())
|
||||||
local markerPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
local markerPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||||
local point = info.points[1] or {x = info.x, z = info.z}
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
||||||
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
||||||
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) or {x = info.x, z = info.z}
|
||||||
assignment.phase = "starting"
|
assignment.phase = "starting"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
local handle, err = ADCpBridge.startUnloader(vehicle, point.x, point.z, markerPoint ~= nil and markerPoint.x or point.x,
|
local handle, err = ADCpBridge.startUnloader(vehicle, point.x, point.z, markerPoint ~= nil and markerPoint.x or point.x,
|
||||||
@@ -428,11 +721,13 @@ local function beginUnloader(adEnv, field, info, assignment)
|
|||||||
log(field.id, "aflæsningsvogn '%s' følger høsteren -> %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
log(field.id, "aflæsningsvogn '%s' følger høsteren -> %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||||
else
|
else
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
fail(field.id, "aflæsningsvogn: " .. tostring(reason))
|
fail(field.id, "aflæsningsvogn: " .. tostring(reason))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
if handle == nil then
|
if handle == nil then
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
fail(field.id, "aflæsningsvogn: " .. tostring(err))
|
fail(field.id, "aflæsningsvogn: " .. tostring(err))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -463,7 +758,9 @@ local function beginBaleCollector(adEnv, field, info, assignment)
|
|||||||
else
|
else
|
||||||
log(field.id, "intet ballelager med plads og vej fundet — ballesamleren stopper når den er fuld")
|
log(field.id, "intet ballelager med plads og vej fundet — ballesamleren stopper når den er fuld")
|
||||||
end
|
end
|
||||||
local point = info.points[1] or {x = info.x, z = info.z}
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
||||||
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
||||||
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) or {x = info.x, z = info.z}
|
||||||
assignment.phase = "starting"
|
assignment.phase = "starting"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
local handle, err = ADCpBridge.startBaleFinder(vehicle, point.x, point.z, function(ok, reason)
|
local handle, err = ADCpBridge.startBaleFinder(vehicle, point.x, point.z, function(ok, reason)
|
||||||
@@ -507,12 +804,31 @@ end
|
|||||||
|
|
||||||
local function beginWork(adEnv, field, info, assignment)
|
local function beginWork(adEnv, field, info, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
|
-- aldrig Courseplay med tom tank (den kører kurset uden at sprede/så)
|
||||||
|
if ADFieldWork.REFILL_ROLES[assignment.role] then
|
||||||
|
local fillType = ADCpBridge.getRefillFillType(vehicle, assignment.role)
|
||||||
|
if fillType ~= nil and ADCpBridge.getRefillLevel(vehicle, fillType) <= 0 then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(field.id, string.format("'%s' er stadig tom for %s ved marken — fyld den op", ADRuns.vehicleLabel(vehicle), fillTypeName(fillType)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
if assignment.role == "unloader" then return beginUnloader(adEnv, field, info, assignment) end
|
if assignment.role == "unloader" then return beginUnloader(adEnv, field, info, assignment) end
|
||||||
if assignment.role == "baleCollector" then return beginBaleCollector(adEnv, field, info, assignment) end
|
if assignment.role == "baleCollector" then return beginBaleCollector(adEnv, field, info, assignment) end
|
||||||
-- høsteren: halmskår efter planen (hakkes = fra; samles op/presses = skår)
|
-- høsteren: halmskår efter planen (hakkes = fra; samles op/presses = skår)
|
||||||
if assignment.role == "harvester" then
|
if assignment.role == "harvester" then
|
||||||
local keepStraw = field.plan.straw == "collect" or field.plan.straw == "bale"
|
local keepStraw = field.plan.straw == "collect" or field.plan.straw == "bale"
|
||||||
ADCpBridge.setStrawSwath(vehicle, keepStraw)
|
ADCpBridge.setStrawSwath(vehicle, keepStraw)
|
||||||
|
-- høsteren sendes IKKE selv til lager med resten (in-game 27/9: den er for stor til at komme til lageret).
|
||||||
|
-- Courseplay venter selv ved kursets ende til tømmevognene har tømt tanken (WAITING_FOR_UNLOAD_AFTER_
|
||||||
|
-- FIELDWORK_ENDED), og tømmevognene bliver på marken, så længe høsterens CP-job kører.
|
||||||
|
end
|
||||||
|
-- vinmark: kurset i gangen (ikke på vinen), ellers kører maskinen ind i stolperne
|
||||||
|
local vineOffset = ADCpBridge.vineOffsetFor(info.fruitName, assignment.role)
|
||||||
|
if vineOffset ~= nil and ADCpBridge.setVineOffset(vehicle, vineOffset) then
|
||||||
|
local skip = ADCpBridge.vineSkipFor(info.fruitName, assignment.role) or 0
|
||||||
|
ADCpBridge.setVineSkip(vehicle, skip)
|
||||||
|
log(field.id, "vinmark: Courseplay-kurset lægges %s, springer %d række(r) over", vineOffset == 0 and "på vinen (høster)" or "til venstre for vinen (i gangen)", skip)
|
||||||
end
|
end
|
||||||
local opts, source = courseOptions(adEnv, field, assignment)
|
local opts, source = courseOptions(adEnv, field, assignment)
|
||||||
if opts == nil then
|
if opts == nil then
|
||||||
@@ -546,13 +862,25 @@ local function beginWork(adEnv, field, info, assignment)
|
|||||||
math.abs(field.windrowOffset), field.windrowOffset > 0 and "til venstre" or "til højre", target, own)
|
math.abs(field.windrowOffset), field.windrowOffset > 0 and "til venstre" or "til højre", target, own)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local point = info.points[1] or {x = info.x, z = info.z}
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
||||||
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
||||||
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) or {x = info.x, z = info.z}
|
||||||
|
-- genbrug af Courseplay-kurser: maskinens eget / en andens matchende kursus frem for ny beregning
|
||||||
|
local cacheContext
|
||||||
|
opts, cacheContext = ADCourseCache.prepare(adEnv, field, info, assignment, opts)
|
||||||
|
-- påbegyndt mark: fortsæt/start nærmest i stedet for forfra (CP's standard)
|
||||||
|
opts = opts or {}
|
||||||
|
local remembered = vehicle.getCpLastRememberedWaypointIx ~= nil and vehicle:getCpLastRememberedWaypointIx() ~= nil
|
||||||
|
opts.startAt = ADFieldJobs.startAtFor({reuse = opts.reuse == true, remembered = remembered, stage = field.stage,
|
||||||
|
standingRatio = info.standingRatio})
|
||||||
|
if opts.startAt ~= "first" then log(field.id, "'%s' starter kurset ved %s punkt (marken er påbegyndt)", ADRuns.vehicleLabel(vehicle), opts.startAt == "last" and "sidste" or "nærmeste") end
|
||||||
assignment.phase = "starting"
|
assignment.phase = "starting"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
||||||
if ok then
|
if ok then
|
||||||
assignment.phase = "working"
|
assignment.phase = "working"
|
||||||
assignment.since = now()
|
assignment.since = now()
|
||||||
|
ADCourseCache.afterStart(vehicle, assignment.vehicleId, cacheContext)
|
||||||
if assignment.role == "mower" then
|
if assignment.role == "mower" then
|
||||||
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
||||||
rememberCourse(field, "mower", vehicle)
|
rememberCourse(field, "mower", vehicle)
|
||||||
@@ -567,7 +895,7 @@ local function beginWork(adEnv, field, info, assignment)
|
|||||||
rememberCourse(field, field.stage, vehicle)
|
rememberCourse(field, field.stage, vehicle)
|
||||||
end
|
end
|
||||||
log(field.id, "Courseplay startet (%s%s%s)", assignment.role,
|
log(field.id, "Courseplay startet (%s%s%s)", assignment.role,
|
||||||
reason == "copied" and (", " .. tostring(source) .. "-kursus kopieret") or "",
|
reason == "copied" and source ~= nil and (", " .. tostring(source) .. "-kursus kopieret") or "",
|
||||||
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||||
else
|
else
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
@@ -585,23 +913,77 @@ end
|
|||||||
-- Sættet er færdigt: rest til lager (læsse-/aflæsningsvogn), ellers parkering.
|
-- Sættet er færdigt: rest til lager (læsse-/aflæsningsvogn), ellers parkering.
|
||||||
local function finishAssignment(adEnv, assignment)
|
local function finishAssignment(adEnv, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
|
-- høster der stadig har korn efter rest-turen: AutoDrive kunne ikke tømme den — sig det tydeligt
|
||||||
|
if assignment.role == "harvester" and restLoad(adEnv, vehicle) > 0 then
|
||||||
|
local text = string.format("'%s' har stadig korn i tanken (%.0f l) — tøm den (AutoDrive kunne ikke aflæsse den)",
|
||||||
|
ADRuns.vehicleLabel(vehicle), restLoad(adEnv, vehicle))
|
||||||
|
log(assignment.fieldId, "%s", text)
|
||||||
|
ADFieldJobs.notify(string.format("F%s: %s", tostring(assignment.fieldId), text), true)
|
||||||
|
end
|
||||||
-- tom såmaskine/sprøjte der ikke blev fyldt op: arbejdet er IKKE færdigt — marken må ikke gå videre
|
-- tom såmaskine/sprøjte der ikke blev fyldt op: arbejdet er IKKE færdigt — marken må ikke gå videre
|
||||||
if assignment.refillFillType ~= nil and ADCpBridge.getRefillLevel(vehicle, assignment.refillFillType) <= 0 then
|
if assignment.refillFillType ~= nil and ADCpBridge.getRefillLevel(vehicle, assignment.refillFillType) <= 0 then
|
||||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(assignment.refillFillType) or "?"
|
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(assignment.refillFillType) or "?"
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
||||||
fail(assignment.fieldId, string.format("'%s' er tom for %s — fyld op, så fortsætter marken", ADRuns.vehicleLabel(vehicle), tostring(name)))
|
fail(assignment.fieldId, string.format("'%s' er tom for %s — fyld op, så fortsætter marken", ADRuns.vehicleLabel(vehicle), tostring(name)))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||||
-- læssevogn/påfyldte sæt parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
-- læssevogn/påfyldte sæt parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
||||||
local park = not assignment.adHandlesEnd and parkMarkerId(adEnv, vehicle) or nil
|
-- manuel transport: ALDRIG ud på vejen med AutoDrive (skærebordet sidder på) — spilleren henter den
|
||||||
|
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
||||||
|
-- bag, så det virker også efter genindlæsning); ellers som før
|
||||||
|
local pooled = ADPool ~= nil and ADPool.shouldReturnHome(assignment) and ADHitch ~= nil
|
||||||
|
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
||||||
|
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
||||||
|
end)
|
||||||
|
local park = not pooled and not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
if pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
||||||
|
if assignment.manual then
|
||||||
|
ADFieldJobs.notify(string.format("F%s høstet — afmontér skærebordet og kør '%s' hjem", tostring(assignment.fieldId), ADRuns.vehicleLabel(vehicle)), true)
|
||||||
|
end
|
||||||
restoreToolOffset(assignment)
|
restoreToolOffset(assignment)
|
||||||
assignment.phase = "finished"
|
assignment.phase = "finished"
|
||||||
table.insert(ADFieldJobs.finished, assignment)
|
table.insert(ADFieldJobs.finished, assignment)
|
||||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local MANUAL_REASON_TEXT = {
|
||||||
|
notThere = "kør den til marken",
|
||||||
|
noHeader = "montér skærebordet",
|
||||||
|
wrongHeader = "skærebordet kan ikke høste afgrøden — montér det rigtige",
|
||||||
|
playerInside = "stig ud, så overtager Smart Pickup",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Manuel transport: overtag når høsteren står på marken med passende skærebord og spilleren er steget ud
|
||||||
|
-- (eller når spilleren trykker "Start nu"); påmind hvert 5. minut indtil da.
|
||||||
|
local function followManual(adEnv, field, info, assignment)
|
||||||
|
if field == nil or info == nil then return ADFieldWork.release(assignment) end
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
local x, z = vehiclePosition(vehicle)
|
||||||
|
local atField = #(info.verts or {}) >= 3 and ADFieldJobs.insidePolygon(x, z, info.verts)
|
||||||
|
if not atField and info.markerId ~= nil then
|
||||||
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||||
|
atField = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) <= ADFieldWork.ARRIVE_DISTANCE
|
||||||
|
end
|
||||||
|
local forced = ADFieldJobs.takeoverRequested[field.id] == true
|
||||||
|
ADFieldJobs.takeoverRequested[field.id] = nil
|
||||||
|
local ok, why = ADManualTransport.readiness({atField = atField, fruits = ADCpBridge.getHarvestFruits(vehicle) or {},
|
||||||
|
fruit = info.fruitIndex, controlled = isControlledByPlayer(vehicle), forced = forced})
|
||||||
|
if ok then
|
||||||
|
log(field.id, "overtager '%s'%s", ADRuns.vehicleLabel(vehicle), forced and " (Start nu)" or "")
|
||||||
|
return beginWork(adEnv, field, info, assignment)
|
||||||
|
end
|
||||||
|
local reason = MANUAL_REASON_TEXT[why] or tostring(why)
|
||||||
|
note(field.id, {reason = {text = string.format("venter på dig: '%s' — %s", ADRuns.vehicleLabel(vehicle), reason)}})
|
||||||
|
if forced then
|
||||||
|
ADFieldJobs.notify(string.format("F%s: kan ikke starte '%s' endnu — %s", tostring(field.id), ADRuns.vehicleLabel(vehicle), reason), true)
|
||||||
|
elseif ADManualTransport.shouldRemind(assignment.remindedAt, now()) then
|
||||||
|
askPlayer(field, info, assignment, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function followWorking(adEnv, field, assignment)
|
local function followWorking(adEnv, field, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
if assignment.role == "rake" and field.windrowOffset == nil and now() - assignment.since >= ADFieldWork.WINDROW_MEASURE_MS
|
if assignment.role == "rake" and field.windrowOffset == nil and now() - assignment.since >= ADFieldWork.WINDROW_MEASURE_MS
|
||||||
@@ -612,6 +994,15 @@ local function followWorking(adEnv, field, assignment)
|
|||||||
end
|
end
|
||||||
if ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) then
|
if ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) then
|
||||||
assignment.idleSince = nil
|
assignment.idleSince = nil
|
||||||
|
assignment.seenWorking = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- genoptaget efter indlæsning: CP starter asynkront — vent til den kører, før "står stille" betyder færdig
|
||||||
|
if assignment.resumed and not assignment.seenWorking then
|
||||||
|
if now() - assignment.since > ADFieldWork.START_TIMEOUT_MS then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(assignment.fieldId, string.format("'%s': Courseplay fortsatte ikke efter genindlæsning", ADRuns.vehicleLabel(vehicle)))
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
assignment.idleSince = assignment.idleSince or now()
|
assignment.idleSince = assignment.idleSince or now()
|
||||||
@@ -628,52 +1019,289 @@ local function followWorking(adEnv, field, assignment)
|
|||||||
finishAssignment(adEnv, assignment)
|
finishAssignment(adEnv, assignment)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Efter genindlæsning: spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (in-game 27/9).
|
||||||
|
-- En maskine med CP-kursus og et husket waypoint (CP nulstiller det når kurset er kørt færdigt) var midt i et job:
|
||||||
|
-- start den fra sidste waypoint med CP's eget "fortsæt"-job (med gemt liste fra ADCpResume: netop de maskiner
|
||||||
|
-- der kørte CP ved gem, fra nærmeste waypoint) (HUD'ens eget startpunkt røres ikke). Står den på en
|
||||||
|
-- mark hvis aktuelle trin er maskinens rolle, meldes den ind på marken igen, så den følges og trinnet afsluttes.
|
||||||
|
function ADFieldWork.resumeAfterLoad(adEnv)
|
||||||
|
-- gemt liste over maskiner hvor CP kørte (ADCpResume): den afgør alene hvem der startes, fra nærmeste waypoint
|
||||||
|
local toStart, cpVehicles = nil, {}
|
||||||
|
if ADCpResume ~= nil then toStart, cpVehicles = ADCpResume.takeToStart(adEnv) end
|
||||||
|
local startSet, handled = {}, {}
|
||||||
|
for _, id in ipairs(toStart or {}) do startSet[id] = true end
|
||||||
|
local fields, stageById = {}, {}
|
||||||
|
for _, info in ipairs(ADFields.list()) do
|
||||||
|
local field = ADFieldJobs.getField(info.id)
|
||||||
|
if field ~= nil and ADFieldModel.isWorking(field) then
|
||||||
|
table.insert(fields, {id = info.id, stage = field.stage, verts = info.verts})
|
||||||
|
stageById[info.id] = field.stage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
|
||||||
|
local vehicle = rig.vehicle
|
||||||
|
local wasWorking = vehicle.startCpAtLastWp ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse()
|
||||||
|
and vehicle.getCpLastRememberedWaypointIx ~= nil and vehicle:getCpLastRememberedWaypointIx() ~= nil
|
||||||
|
if toStart ~= nil then wasWorking = startSet[rig.id] == true end
|
||||||
|
handled[rig.id] = true
|
||||||
|
local fieldId, distance = nil, nil
|
||||||
|
if wasWorking and toStart ~= nil then
|
||||||
|
fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields)
|
||||||
|
elseif wasWorking then
|
||||||
|
fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields)
|
||||||
|
local course = ADCpBridge.getCourse(vehicle)
|
||||||
|
local okWp, wp = pcall(function() return course ~= nil and course:getWaypoint(vehicle:getCpLastRememberedWaypointIx()) or nil end)
|
||||||
|
if okWp and wp ~= nil and wp.x ~= nil then distance = math.sqrt((wp.x - rig.x) ^ 2 + (wp.z - rig.z) ^ 2) end
|
||||||
|
end
|
||||||
|
if wasWorking and toStart == nil and not rig.busy and not rig.controlled and not ADFieldJobs.shouldResume({fieldId = fieldId, distance = distance}) then
|
||||||
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartes ikke: husket waypoint fra et gammelt job (%s væk, ikke på en mark i gang)",
|
||||||
|
tostring(rig.label), distance ~= nil and string.format("%.0f m", distance) or "ukendt afstand")
|
||||||
|
elseif wasWorking and not rig.busy and not rig.controlled then
|
||||||
|
local ok, started
|
||||||
|
if toStart ~= nil then ok, started = pcall(ADCpResume.startNearest, vehicle)
|
||||||
|
else ok, started = pcall(vehicle.startCpAtLastWp, vehicle) end
|
||||||
|
if not ok then
|
||||||
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(rig.label), tostring(started))
|
||||||
|
elseif started then
|
||||||
|
if fieldId ~= nil then
|
||||||
|
ADFieldJobs.state.assignments[rig.id] = {vehicleId = rig.id, vehicle = vehicle, fieldId = fieldId, role = rig.role,
|
||||||
|
stage = stageById[fieldId], helper = false, phase = "working", since = now(), width = rig.width,
|
||||||
|
groupSize = 1, leader = true, resumed = true, manual = rig.manual == true}
|
||||||
|
-- lager til læssevognens rest, som ved en normal start
|
||||||
|
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||||
|
local field, info = ADFieldJobs.getField(fieldId), ADFields.getInfo(fieldId)
|
||||||
|
local fillType = nil
|
||||||
|
-- kun læssevognen kører selv hjem med læsset; høsteren tømmes af tømmevognene på marken
|
||||||
|
if rig.role == "collector" then fillType = ADFieldWork.storeFillType(field, info) end
|
||||||
|
if fillType ~= nil then
|
||||||
|
local okStore, store = pcall(ADFieldWork.chooseStore, adEnv, field, info, vehicle, fillType)
|
||||||
|
if okStore and store ~= nil then assignment.storeId = store.id end
|
||||||
|
end
|
||||||
|
log(fieldId, "'%s' fortsætter efter genindlæsning (Courseplay fra sidste waypoint)%s", tostring(rig.label),
|
||||||
|
assignment.storeId ~= nil and "" or (fillType ~= nil and " — intet lager til resten fundet" or ""))
|
||||||
|
else
|
||||||
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra %s waypoint efter genindlæsning", tostring(rig.label),
|
||||||
|
toStart ~= nil and "nærmeste" or "sidste")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- CP-maskiner uden markrolle (fx balleopsamler, tømmevogn i CP-job): start dem også
|
||||||
|
for _, id in ipairs(toStart or {}) do
|
||||||
|
local vehicle = cpVehicles[id]
|
||||||
|
if not handled[id] and vehicle ~= nil then
|
||||||
|
local ok, started = pcall(ADCpResume.startNearest, vehicle)
|
||||||
|
if ok and started then
|
||||||
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra nærmeste waypoint efter genindlæsning", tostring(ADRuns.vehicleLabel(vehicle)))
|
||||||
|
elseif not ok then
|
||||||
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(started))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markmaskiner der stadig hedder modelnavnet ("Unrealistic Engine 3") får et navn efter rollen ("Roeoptager 2").
|
||||||
|
-- Navne spilleren/moden har givet røres aldrig; flow-traktorer navngives af deres flow (ikke i markflåden).
|
||||||
|
local BEET_FRUITS = {SUGARBEET = true, POTATO = true, CARROT = true, PARSNIP = true, BEETROOT = true}
|
||||||
|
local function harvesterSubtype(rig)
|
||||||
|
for fruit in pairs(rig.fruits or {}) do
|
||||||
|
local desc = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruit) or nil
|
||||||
|
if desc ~= nil and BEET_FRUITS[desc.name] then return "beet" end
|
||||||
|
end
|
||||||
|
local chaff = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeIndexByName("CHAFF") or nil
|
||||||
|
local ok, isForage = pcall(function()
|
||||||
|
local vehicle = rig.vehicle
|
||||||
|
for index in ipairs(vehicle.getFillUnits ~= nil and vehicle:getFillUnits() or {}) do
|
||||||
|
if chaff ~= nil and vehicle:getFillUnitSupportsFillType(index, chaff) then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
return ok and isForage and "forage" or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.autoNameFleet(adEnv)
|
||||||
|
local names = {}
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do table.insert(names, ADRuns.vehicleLabel(vehicle)) end
|
||||||
|
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
|
||||||
|
local vehicle = rig.vehicle
|
||||||
|
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||||
|
local model = vehicle.getName ~= nil and tostring(vehicle:getName()) or nil
|
||||||
|
-- puljetraktor med et lånt redskab: ikke opkaldt efter redskabet (in-game 28/9: Challenger blev "Kultivator 1")
|
||||||
|
local pooled = ADHitch ~= nil and ADPool ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
||||||
|
if stateModule ~= nil and stateModule.setName ~= nil and model ~= nil and ADRuns.vehicleLabel(vehicle) == model and not rig.controlled and not pooled then
|
||||||
|
local name = ADNaming.fleetName(rig.role, rig.role == "harvester" and harvesterSubtype(rig) or nil, names)
|
||||||
|
if name ~= nil then
|
||||||
|
stateModule:setName(name)
|
||||||
|
if stateModule.raiseDirtyFlag ~= nil then stateModule:raiseDirtyFlag() end
|
||||||
|
table.insert(names, name)
|
||||||
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' navngivet '%s' (markflåde, %s)", model, name, tostring(rig.role))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Høsten er færdig, men den høster der høstede marken har korn i tanken og står ved marken: start dens CP-job igen
|
||||||
|
-- (sidste/nærmeste punkt), så CP venter ved kursets ende til tømmevognene har tømt den. Én gang pr. mark.
|
||||||
|
function ADFieldWork.tryEmptyHarvester(adEnv, field, info)
|
||||||
|
local vehicleId = field.harvestVehicleId
|
||||||
|
if vehicleId == nil or info == nil then return false end
|
||||||
|
local rig = nil
|
||||||
|
for _, candidate in ipairs(ADFieldWork.getRigs(adEnv)) do
|
||||||
|
if candidate.id == vehicleId then rig = candidate break end
|
||||||
|
end
|
||||||
|
if rig == nil then return false end
|
||||||
|
local fill = restLoad(adEnv, rig.vehicle)
|
||||||
|
local near = #(info.verts or {}) >= 3 and ADFieldJobs.insidePolygon(rig.x, rig.z, info.verts)
|
||||||
|
if not near and info.markerId ~= nil then
|
||||||
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||||
|
near = wayPoint ~= nil and math.sqrt((wayPoint.x - rig.x) ^ 2 + (wayPoint.z - rig.z) ^ 2) <= 100
|
||||||
|
end
|
||||||
|
if not ADFieldJobs.shouldEmptyHarvester({fill = fill, near = near, busy = rig.busy, controlled = rig.controlled,
|
||||||
|
tried = ADFieldJobs.emptyAttempts[field.id] == true}) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
ADFieldJobs.emptyAttempts[field.id] = true
|
||||||
|
local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = "harvester", stage = field.stage,
|
||||||
|
helper = false, phase = "driving", since = now(), width = rig.width, groupSize = 1, leader = true,
|
||||||
|
manual = rig.manual == true, emptying = true}
|
||||||
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||||
|
log(field.id, "høsten er færdig, men '%s' har %.0f l i tanken — tømmes på marken, før marken går videre", tostring(rig.label), fill)
|
||||||
|
beginWork(adEnv, field, info, assignment)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldWork.follow(adEnv, assignment)
|
function ADFieldWork.follow(adEnv, assignment)
|
||||||
local vehicle = assignment.vehicle
|
local vehicle = assignment.vehicle
|
||||||
-- Kun når spilleren selv KØRER (bevæger sig uden Courseplay/AutoDrive) slippes sættet — at sidde i
|
-- Kun når spilleren selv KØRER (bevæger sig uden Courseplay/AutoDrive) slippes sættet — at sidde i
|
||||||
-- førersædet og kigge må ikke slippe det (in-game 25/9).
|
-- førersædet og kigge må ikke slippe det (in-game 25/9).
|
||||||
local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting"
|
local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting"
|
||||||
local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0
|
local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0
|
||||||
if isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then
|
if assignment.phase ~= "awaitingPlayer" and isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then
|
||||||
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
||||||
return ADFieldWork.release(assignment)
|
return ADFieldWork.release(assignment)
|
||||||
end
|
end
|
||||||
local field = ADFieldJobs.getField(assignment.fieldId)
|
local field = ADFieldJobs.getField(assignment.fieldId)
|
||||||
local info = ADFields.getInfo(assignment.fieldId)
|
local info = ADFields.getInfo(assignment.fieldId)
|
||||||
wake(vehicle)
|
wake(vehicle)
|
||||||
if assignment.phase == "driving" then
|
if assignment.phase == "fetching" then
|
||||||
|
local hitch = assignment.hitch
|
||||||
|
if not hitch.done then return end
|
||||||
|
ADPool.release(assignment)
|
||||||
|
if info == nil or field == nil then
|
||||||
|
-- marken findes ikke længere (fx solgt): redskabet tilbage, traktoren hjem
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
if hitch.ok then ADHitch.returnHome(adEnv, vehicle) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not hitch.ok then
|
||||||
|
-- koblingen fejlede: den skyldige i karantæne (kom traktoren ikke frem, er det traktoren), hjem, prøv igen
|
||||||
|
local blamed = ADPool.failureBlames(hitch.detail) == "tractor" and assignment.vehicleId or assignment.pool.implementId
|
||||||
|
ADPool.quarantine(blamed, now())
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
note(assignment.fieldId, {retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS),
|
||||||
|
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
||||||
|
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- koblet på: sammenfoldet til vejen, bredden med redskabet på (for bred -> hjem igen)
|
||||||
|
pcall(ADHitch.foldForTransport, vehicle)
|
||||||
|
local width = ADCpBridge.getWorkWidth(vehicle)
|
||||||
|
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
||||||
|
if not okWidth then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- som almindelig udsendelse: tom såmaskine/sprøjte fyldes op på vejen, uden påfyldning køres den hjem igen
|
||||||
|
local refill = refillPlan(adEnv, assignment.fieldId, vehicle, assignment.role, info.x, info.z)
|
||||||
|
local nextDrive = ADPool.nextDrive(refill)
|
||||||
|
if nextDrive == "empty" then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
|
fail(assignment.fieldId, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op",
|
||||||
|
tostring(assignment.pool.implementName), fillTypeName(refill.fillType)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
||||||
|
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
||||||
|
if nextDrive == "load" then
|
||||||
|
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType)
|
||||||
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType}
|
||||||
|
else
|
||||||
|
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||||
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
||||||
|
end
|
||||||
|
note(assignment.fieldId, {reason = false})
|
||||||
|
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
||||||
|
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
||||||
|
elseif assignment.phase == "driving" then
|
||||||
if not isAdActive(vehicle) and now() - assignment.since > 2000 then
|
if not isAdActive(vehicle) and now() - assignment.since > 2000 then
|
||||||
local x, z = vehiclePosition(vehicle)
|
local x, z = vehiclePosition(vehicle)
|
||||||
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
|
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
|
||||||
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
||||||
if distance <= ADFieldWork.ARRIVE_DISTANCE then
|
if distance <= ADFieldWork.ARRIVE_DISTANCE then
|
||||||
beginWork(adEnv, field, info, assignment)
|
beginWork(adEnv, field, info, assignment)
|
||||||
|
elseif assignment.startX ~= nil and math.sqrt((x - assignment.startX) ^ 2 + (z - assignment.startZ) ^ 2) < ADFieldWork.NO_MOVE_DISTANCE
|
||||||
|
and (assignment.driveRetries or 0) >= 1 then
|
||||||
|
-- AD startede og stoppede uden at flytte sig (to gange): ingen rute — genforsøg hjælper ikke
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(assignment.fieldId, string.format("AutoDrive fandt ingen rute fra '%s' til %s — står den uden for vejnettet?", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?")))
|
||||||
|
elseif assignment.drive ~= nil and (assignment.driveRetries or 0) < ADFieldWork.DRIVE_RETRIES then
|
||||||
|
-- AutoDrive stoppede undervejs (spilleren byggede, trafik, ...): kør videre i stedet for at opgive
|
||||||
|
assignment.driveRetries = (assignment.driveRetries or 0) + 1
|
||||||
|
assignment.since = now()
|
||||||
|
local d = assignment.drive
|
||||||
|
startAd(adEnv, vehicle, d.mode, d.first, d.second, d.fillType)
|
||||||
|
log(assignment.fieldId, "'%s' stoppede %.0f m fra %s — AutoDrive startes igen (%d/%d)", ADRuns.vehicleLabel(vehicle), distance,
|
||||||
|
tostring(info ~= nil and info.markerName or "?"), assignment.driveRetries, ADFieldWork.DRIVE_RETRIES)
|
||||||
else
|
else
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
fail(assignment.fieldId, string.format("%s nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?"), distance))
|
fail(assignment.fieldId, string.format("%s nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?"), distance))
|
||||||
|
-- ikke efterladt midt på vejen/ved en mark: hjem til parkeringen (puljeredskab sættes af først)
|
||||||
|
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif assignment.phase == "waitingCourse" then
|
elseif assignment.phase == "waitingCourse" then
|
||||||
if now() - assignment.since > ADFieldWork.COURSE_WAIT_MS then
|
if now() - assignment.since > ADFieldWork.COURSE_WAIT_MS then
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
fail(assignment.fieldId, "multi-tool: lederens kursus kom ikke (3 min)")
|
fail(assignment.fieldId, "multi-tool: lederens kursus kom ikke (3 min)")
|
||||||
elseif coursesOf(field.id)[field.stage] ~= nil then
|
elseif coursesOf(field.id)[field.stage] ~= nil then
|
||||||
beginWork(adEnv, field, info, assignment)
|
beginWork(adEnv, field, info, assignment)
|
||||||
end
|
end
|
||||||
elseif assignment.phase == "starting" then
|
elseif assignment.phase == "starting" then
|
||||||
if now() - assignment.since > ADFieldWork.START_TIMEOUT_MS then
|
local generating = assignment.handle ~= nil and assignment.handle.generating == true
|
||||||
|
if ADFieldJobs.startTimedOut(now() - assignment.since, generating) then
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
fail(assignment.fieldId, "Courseplay svarede ikke (60 s)")
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
|
fail(assignment.fieldId, generating and "Courseplay blev ikke færdig med kurset (5 min)" or "Courseplay svarede ikke (60 s)")
|
||||||
end
|
end
|
||||||
elseif assignment.phase == "working" then
|
elseif assignment.phase == "working" then
|
||||||
followWorking(adEnv, field, assignment)
|
followWorking(adEnv, field, assignment)
|
||||||
|
elseif assignment.phase == "awaitingPlayer" then
|
||||||
|
followManual(adEnv, field, info, assignment)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Høsten er færdig: aflæsningsvognene stoppes; har de rest, kører den til lager, ellers parkering.
|
-- Høsten er færdig: aflæsningsvognene stoppes; har de rest, kører den til lager, ellers parkering.
|
||||||
function ADFieldWork.windDownHelpers(adEnv, fieldId, stage)
|
function ADFieldWork.windDownHelpers(adEnv, fieldId, stage)
|
||||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||||
if assignment.helper and assignment.stage == stage then
|
if assignment.helper and assignment.stage == stage and assignment.phase == "fetching" then
|
||||||
|
-- høsten sluttede mens puljevognen blev hentet: afbryd, vognen tilbage hvis den nåede at komme på
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
if not ADHitch.returnHome(adEnv, vehicle) then
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
end
|
||||||
|
log(fieldId, "tømmevogn '%s' afbrudt under hentning — høsten er færdig", ADRuns.vehicleLabel(vehicle))
|
||||||
|
elseif assignment.helper and assignment.stage == stage then
|
||||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||||
ADCpBridge.stop(assignment.vehicle)
|
ADCpBridge.stop(assignment.vehicle)
|
||||||
assignment.phase = "working"
|
assignment.phase = "working"
|
||||||
@@ -683,10 +1311,35 @@ function ADFieldWork.windDownHelpers(adEnv, fieldId, stage)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Stop alle markarbejdets opgaver (Stop alt og parkér). Returnerer {{vehicle, manual}}; markernes trin bevares,
|
||||||
|
-- så arbejdet fortsætter efter pausen.
|
||||||
|
function ADFieldWork.stopAllAssignments(adEnv)
|
||||||
|
local stopped = {}
|
||||||
|
local list = {}
|
||||||
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do table.insert(list, assignment) end
|
||||||
|
for _, assignment in ipairs(list) do
|
||||||
|
pcall(stopAd, assignment.vehicle)
|
||||||
|
pcall(ADCpBridge.stop, assignment.vehicle)
|
||||||
|
ADHitch.stow(assignment.vehicle, "stop alt")
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
-- puljeredskab: sættes af på sin plads og traktoren kører hjem (i stedet for almindelig parkering)
|
||||||
|
local okPool, pooled = pcall(ADHitch.returnHome, adEnv, assignment.vehicle)
|
||||||
|
table.insert(stopped, {vehicle = assignment.vehicle, manual = assignment.manual == true, pooled = okPool and pooled == true})
|
||||||
|
end
|
||||||
|
return stopped
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldWork.stopAll(adEnv, fieldId)
|
function ADFieldWork.stopAll(adEnv, fieldId)
|
||||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||||
pcall(stopAd, assignment.vehicle)
|
pcall(stopAd, assignment.vehicle)
|
||||||
ADCpBridge.stop(assignment.vehicle)
|
ADCpBridge.stop(assignment.vehicle)
|
||||||
|
ADHitch.stow(assignment.vehicle, "marken stoppet")
|
||||||
|
local wasFetching = assignment.phase == "fetching"
|
||||||
ADFieldWork.release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
|
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, assignment.vehicle)
|
||||||
|
if wasFetching and not (okHome and pooled) then
|
||||||
|
local park = parkMarkerId(adEnv, assignment.vehicle)
|
||||||
|
if park ~= nil then pcall(startAd, adEnv, assignment.vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+443
-119
@@ -10,7 +10,15 @@ ADFields.MARKER_MAX_DISTANCE = 400
|
|||||||
ADFields.cache = {}
|
ADFields.cache = {}
|
||||||
ADFields.order = {}
|
ADFields.order = {}
|
||||||
ADFields.scanIndex = 0
|
ADFields.scanIndex = 0
|
||||||
|
-- {[markId] = true}: marker hvis scanning er afsluttet (med resultat) eller fejlede — til isScanComplete
|
||||||
|
ADFields.scanFailed = {}
|
||||||
|
ADFields.ownedDone = false
|
||||||
ADFields.mowable = nil
|
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)
|
function ADFields.parseMarkerName(name)
|
||||||
if type(name) ~= "string" then return nil end
|
if type(name) ~= "string" then return nil end
|
||||||
@@ -45,6 +53,7 @@ end
|
|||||||
function ADFields.growthPhase(state, fruit)
|
function ADFields.growthPhase(state, fruit)
|
||||||
if state == nil or fruit == nil then return nil end
|
if state == nil or fruit == nil then return nil end
|
||||||
if state == 0 then return "none" 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.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
|
if fruit.witheredState ~= nil and state == fruit.witheredState then return "withered" end
|
||||||
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
|
||||||
@@ -54,6 +63,33 @@ function ADFields.growthPhase(state, fruit)
|
|||||||
return "cut"
|
return "cut"
|
||||||
end
|
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.
|
-- Vokser marken? Fasen vinder; ukendt fase -> der er voksende afgrøde (getFruitArea) og den er ikke klar.
|
||||||
function ADFields.isGrowing(phase, anyPixels, readyRatio)
|
function ADFields.isGrowing(phase, anyPixels, readyRatio)
|
||||||
if phase ~= nil then return phase == "growing" end
|
if phase ~= nil then return phase == "growing" end
|
||||||
@@ -69,6 +105,16 @@ function ADFields.soilNeeds(levels, settings)
|
|||||||
}
|
}
|
||||||
end
|
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()
|
function ADFields.getMowable()
|
||||||
if ADFields.mowable ~= nil then return ADFields.mowable end
|
if ADFields.mowable ~= nil then return ADFields.mowable end
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
@@ -122,15 +168,6 @@ local function readWeed(x, z)
|
|||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
local function average(points, reader, channel)
|
|
||||||
local sum, count = 0, 0
|
|
||||||
for _, p in ipairs(points) do
|
|
||||||
local value = reader(channel, p.x, p.z)
|
|
||||||
if value ~= nil then sum, count = sum + value, count + 1 end
|
|
||||||
end
|
|
||||||
return count > 0 and sum / count or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function missionSettings()
|
local function missionSettings()
|
||||||
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
||||||
local function flag(value) return value == nil or value == true end
|
local function flag(value) return value == nil or value == true end
|
||||||
@@ -141,22 +178,6 @@ local function missionSettings()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Jord og pleje: gødning (og max), kalk, pløjning, ukrudt — gennemsnit over prøvepunkterne.
|
|
||||||
local function readSoil(info)
|
|
||||||
if type(FieldDensityMap) ~= "table" or #info.points == 0 then return end
|
|
||||||
local settings = missionSettings()
|
|
||||||
info.fert = average(info.points, readDensity, FieldDensityMap.SPRAY_LEVEL)
|
|
||||||
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
|
|
||||||
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
|
|
||||||
info.lime = average(info.points, readDensity, FieldDensityMap.LIME_LEVEL)
|
|
||||||
info.plowLevel = average(info.points, readDensity, FieldDensityMap.PLOW_LEVEL)
|
|
||||||
if settings.weedsEnabled then
|
|
||||||
info.weed = average(info.points, function(_, x, z) return readWeed(x, z) end, nil)
|
|
||||||
end
|
|
||||||
info.needs = ADFields.soilNeeds(info, settings)
|
|
||||||
if not settings.limeRequired then info.lime = nil end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function fieldOwner(field)
|
local function fieldOwner(field)
|
||||||
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
||||||
if farmland == nil then return nil end
|
if farmland == nil then return nil end
|
||||||
@@ -171,82 +192,152 @@ local function isOnField(x, z)
|
|||||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Centroid + halvvejs mod op til 8 hjørner; kun punkter der stadig er mark (bygninger fjerner areal).
|
ADFields.SAMPLE_DIRECTIONS = 16
|
||||||
local function samplePoints(field)
|
|
||||||
|
-- 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 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
|
for _, node in ipairs(field.polygonPoints or {}) do
|
||||||
if type(node) == "number" then
|
if type(node) == "number" then
|
||||||
local x, _, z = getWorldTranslation(node)
|
local x, _, z = getWorldTranslation(node)
|
||||||
table.insert(verts, {x = x, z = z})
|
table.insert(verts, {x = x, z = z})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local cx, cz = field.posX, field.posZ
|
return verts
|
||||||
if (cx == nil or cz == nil) and #verts > 0 then
|
end
|
||||||
cx, cz = 0, 0
|
|
||||||
|
-- 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
|
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
|
||||||
cx, cz = cx / #verts, cz / #verts
|
return 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function fruitAt(points)
|
-- Afstand fra (x, z) til omridsets nærmeste kant; nil uden omrids.
|
||||||
local votes, best, bestVotes = {}, nil, 0
|
function ADFields.distanceToPolygon(verts, x, z)
|
||||||
for _, p in ipairs(points) do
|
if verts == nil or #verts == 0 then return nil end
|
||||||
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, p.x, p.z)
|
local best = math.huge
|
||||||
if ok and type(index) == "number" and index > 0 then
|
for index = 1, #verts do
|
||||||
votes[index] = (votes[index] or 0) + 1
|
local a, b = verts[index], verts[index % #verts + 1]
|
||||||
if votes[index] > bestVotes then best, bestVotes = index, votes[index] end
|
local dx, dz = b.x - a.x, b.z - a.z
|
||||||
end
|
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
|
end
|
||||||
return best
|
return best
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Andel af prøvefladen i høstvinduet (samme kald som Arbejdsseddel: any = true,true; klar = false,false).
|
-- Markøren "F<nr>" nærmest marken: afstand til omridset (markører står ved kanten; midten kan
|
||||||
local function readyRatio(fruitIndex, points)
|
-- ligge 900 m væk på store kort), ellers til midten.
|
||||||
local anySum, readySum = 0, 0
|
local function findMarker(adEnv, fieldId, x, z, verts)
|
||||||
for _, p in ipairs(points) do
|
|
||||||
local okA, _, anyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, true, true)
|
|
||||||
if okA and anyPixels then anySum = anySum + anyPixels end
|
|
||||||
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, false, false)
|
|
||||||
if okR and readyPixels then readySum = readySum + readyPixels end
|
|
||||||
end
|
|
||||||
return anySum > 0 and readySum / anySum or 0, anySum
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Vækststadiet på prøvepunkterne (flertal): getFieldDataAtWorldPosition's anden returværdi.
|
|
||||||
local function growthStateAt(points)
|
|
||||||
local votes, best, bestVotes = {}, nil, 0
|
|
||||||
for _, p in ipairs(points) do
|
|
||||||
local ok, _, state = pcall(function()
|
|
||||||
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, p.x, 1, p.z)
|
|
||||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(p.x, y, p.z)
|
|
||||||
end)
|
|
||||||
if ok and type(state) == "number" then
|
|
||||||
votes[state] = (votes[state] or 0) + 1
|
|
||||||
if votes[state] > bestVotes then best, bestVotes = state, votes[state] end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return best
|
|
||||||
end
|
|
||||||
|
|
||||||
local function findMarker(adEnv, fieldId, x, z)
|
|
||||||
local best, bestDistance = nil, math.huge
|
local best, bestDistance = nil, math.huge
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
if ADFields.parseMarkerName(marker.name) == fieldId then
|
if ADFields.parseMarkerName(marker.name) == fieldId then
|
||||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||||
if wayPoint ~= nil then
|
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
|
if distance < bestDistance then best, bestDistance = marker, distance end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -257,84 +348,317 @@ local function findMarker(adEnv, fieldId, x, z)
|
|||||||
return nil, best ~= nil and bestDistance or nil
|
return nil, best ~= nil and bestDistance or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scanField(adEnv, field, fieldId)
|
-- ---- scanning som arbejdskø ----
|
||||||
local points, verts = samplePoints(field)
|
-- En marks scanning er en kø af små opgaver (ét prøvepunkt pr. opgave), og scanStep kører én opgave pr.
|
||||||
local info = {id = fieldId, field = field, points = points, verts = verts, mowable = false, readyRatio = 0}
|
-- frame. In-game 26/9 gav en hel mark i ét frame et hak (flere opslag gennemgår alle kortets ~40
|
||||||
info.x, info.z = field.posX, field.posZ
|
-- afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem), derfor en kø.
|
||||||
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
|
-- (eksplicit hale: # er udefineret på en tabel med huller, og udførte opgaver nulstilles)
|
||||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
local function push(scan, task)
|
||||||
if info.fruitIndex ~= nil then
|
scan.tail = (scan.tail or 0) + 1
|
||||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
scan.queue[scan.tail] = task
|
||||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
|
||||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
|
||||||
end
|
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
|
if mowable ~= nil then
|
||||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||||
end
|
end
|
||||||
local fruit = nil
|
local fruit = nil
|
||||||
if info.fruitIndex ~= nil then
|
if info.fruitIndex ~= nil then
|
||||||
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
|
info.readyRatio = scan.anySum > 0 and scan.readySum / scan.anySum or 0
|
||||||
|
info.immatureRatio = scan.anySum > 0 and scan.immatureSum / scan.anySum or 0
|
||||||
|
info.anyPixels = scan.anySum
|
||||||
|
-- andel af hele arealet der stadig står i høsthøjde (fremdrift på fanen Måned)
|
||||||
|
info.standingRatio = scan.totalSum > 0 and scan.readySum / scan.totalSum or nil
|
||||||
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||||
info.fruitName = fruit ~= nil and fruit.name or nil
|
info.fruitName = fruit ~= nil and fruit.name or nil
|
||||||
info.harvestFillType = fruit ~= nil and fruit.fillType ~= nil and fruit.fillType.index 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
|
end
|
||||||
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
||||||
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), fruit) or nil
|
info.phase = fruit ~= nil and ADFields.growthPhase(info.growthState, fruit) or nil
|
||||||
info.growing = info.fruitIndex ~= nil and ADFields.isGrowing(info.phase, info.anyPixels, info.readyRatio)
|
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)
|
-- 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
|
if info.kind == "harvest" and (info.phase == "cut" or info.phase == "withered" or info.phase == "none") then
|
||||||
info.kind = "bare"
|
info.kind = "bare"
|
||||||
|
info.stubbleOf = info.phase == "cut" and info.cropTitle or nil
|
||||||
end
|
end
|
||||||
readSoil(info)
|
local settings = scan.settings or missionSettings()
|
||||||
if info.x ~= nil then
|
for key, sum in pairs(scan.soil) do
|
||||||
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
|
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.markerId = marker ~= nil and marker.id or nil
|
||||||
info.markerName = marker ~= nil and marker.name or nil
|
info.markerName = marker ~= nil and marker.name or nil
|
||||||
info.markerDistance = distance
|
info.markerDistance = distance
|
||||||
|
info.markerCheckedAt = ADFields.now
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
local function ownedFields(farmId)
|
-- Marklisten (ejede marker) i portioner af 25 marker pr. frame.
|
||||||
local result = {}
|
local function newOwnedScan(farmId)
|
||||||
local all = g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}
|
local all = {}
|
||||||
for _, field in pairs(all) do
|
for _, field in pairs(g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}) do
|
||||||
|
table.insert(all, field)
|
||||||
|
end
|
||||||
|
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
|
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||||
if id ~= nil and fieldOwner(field) == farmId then
|
if id ~= nil and fieldOwner(field) == farmId then
|
||||||
table.insert(result, {id = id, field = field})
|
table.insert(scan.result, {id = id, field = field})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(result, function(a, b) return a.id < b.id end)
|
end)
|
||||||
return result
|
end
|
||||||
|
return scan
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
|
local function finishOwnedScan(scan)
|
||||||
function ADFields.scanNext(adEnv, farmId)
|
table.sort(scan.result, function(a, b) return a.id < b.id end)
|
||||||
local owned = ownedFields(farmId)
|
|
||||||
local ids = {}
|
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
|
for id in pairs(ADFields.cache) do
|
||||||
if not ids[id] then ADFields.cache[id] = nil end
|
if not ids[id] then ADFields.cache[id] = nil end
|
||||||
end
|
end
|
||||||
ADFields.order = owned
|
ADFields.order = scan.result
|
||||||
if #owned == 0 then return end
|
ADFields.ownedDone = true
|
||||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
ADFields.listCache = nil
|
||||||
local entry = owned[ADFields.scanIndex]
|
end
|
||||||
local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
|
|
||||||
if ok then
|
-- Har scanningen nået alle ejede marker mindst én gang (resultat i cachen, eller scanningen fejlede)?
|
||||||
ADFields.cache[entry.id] = info
|
-- 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
|
else
|
||||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
|
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
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||||
function ADFields.list()
|
function ADFields.list()
|
||||||
|
if ADFields.listCache ~= nil then return ADFields.listCache end
|
||||||
local infos = {}
|
local infos = {}
|
||||||
for _, entry in ipairs(ADFields.order) do
|
for _, entry in ipairs(ADFields.order) do
|
||||||
local info = ADFields.cache[entry.id]
|
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
|
end
|
||||||
|
ADFields.listCache = infos
|
||||||
return infos
|
return infos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
-- AD Fleet Names
|
||||||
|
-- Konsol "spuNameFleet": traktorer/lastbiler uden for flows får spillets butiksnavn (nummereret når der er flere
|
||||||
|
-- af samme model), og deres egen AD-parkering hedder "P <navn>". Flow-traktorer beholder flowets navn (mod'en
|
||||||
|
-- sætter det selv); selvkørende maskiner (mejetærsker, roeoptager …) beholder deres navn.
|
||||||
|
|
||||||
|
ADFleetNames = {}
|
||||||
|
ADFleetNames.LOG_PREFIX = "ADSmartPickup: navne: "
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- vehicles = {{id, model, name, inRun, selfPropelled}} -> {{id, name, parkName, rename}}
|
||||||
|
-- parkUse = {[parkIndex] = antal køretøjer (også redskaber) med den parkering}: delte parkeringer røres ikke.
|
||||||
|
function ADFleetNames.plan(vehicles, parkUse)
|
||||||
|
local byModel = {}
|
||||||
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
|
if not vehicle.inRun and not vehicle.selfPropelled then
|
||||||
|
byModel[vehicle.model] = byModel[vehicle.model] or {}
|
||||||
|
table.insert(byModel[vehicle.model], vehicle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local newNames = {}
|
||||||
|
for model, list in pairs(byModel) do
|
||||||
|
table.sort(list, function(a, b) return tostring(a.id) < tostring(b.id) end)
|
||||||
|
for index, vehicle in ipairs(list) do
|
||||||
|
newNames[vehicle.id] = #list > 1 and string.format("%s %d", model, index) or model
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local result = {}
|
||||||
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
|
local name = newNames[vehicle.id] or vehicle.name or vehicle.model
|
||||||
|
local ownPark = vehicle.park == nil or ((parkUse or {})[vehicle.park] or 1) <= 1
|
||||||
|
table.insert(result, {id = vehicle.id, name = name, parkName = "P " .. name,
|
||||||
|
rename = newNames[vehicle.id] ~= nil and name ~= vehicle.name,
|
||||||
|
renamePark = newNames[vehicle.id] ~= nil and ownPark})
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
function ADFleetNames.apply(adEnv)
|
||||||
|
local vehicles, byId, parkUse = {}, {}, {}
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||||
|
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||||
|
if type(index) == "number" and index >= 1 and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then
|
||||||
|
parkUse[index] = (parkUse[index] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
if id ~= nil and vehicle.getName ~= nil then
|
||||||
|
local ownIndex = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||||
|
table.insert(vehicles, {id = id, model = tostring(vehicle:getName()), name = ADRuns.vehicleLabel(vehicle),
|
||||||
|
park = type(ownIndex) == "number" and ownIndex >= 1 and ownIndex or nil,
|
||||||
|
inRun = ADRunsController.getRunForVehicle(vehicle) ~= nil,
|
||||||
|
selfPropelled = not ADPool.canPull(ADPool.ownRole(vehicle))})
|
||||||
|
byId[id] = vehicle
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local renamed, parks = 0, 0
|
||||||
|
for _, entry in ipairs(ADFleetNames.plan(vehicles, parkUse)) do
|
||||||
|
local vehicle = byId[entry.id]
|
||||||
|
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||||
|
if entry.rename and module ~= nil and module.setName ~= nil then
|
||||||
|
module:setName(entry.name)
|
||||||
|
if module.raiseDirtyFlag ~= nil then module:raiseDirtyFlag() end
|
||||||
|
renamed = renamed + 1
|
||||||
|
end
|
||||||
|
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||||
|
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||||
|
if entry.renamePark and marker ~= nil and marker.name ~= entry.parkName then
|
||||||
|
adEnv.ADGraphManager:renameMapMarker(entry.parkName, index)
|
||||||
|
parks = parks + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Logging.info(ADFleetNames.LOG_PREFIX .. "%d maskiner omdøbt, %d parkeringer omdøbt", renamed, parks)
|
||||||
|
return renamed, parks
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFleetNames:consoleNameFleet()
|
||||||
|
local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil
|
||||||
|
if adEnv == nil then return "spuNameFleet: AutoDrive er ikke klar endnu" end
|
||||||
|
local ok, renamed, parks = pcall(ADFleetNames.apply, adEnv)
|
||||||
|
if not ok then return "spuNameFleet: fejl: " .. tostring(renamed) end
|
||||||
|
return string.format("spuNameFleet: %d maskiner og %d parkeringer omdøbt — gem spillet", renamed, parks)
|
||||||
|
end
|
||||||
|
|
||||||
|
if addConsoleCommand ~= nil then
|
||||||
|
addConsoleCommand("spuNameFleet", "Giver traktorer uden for flows butiksnavne og deres parkeringer 'P <navn>'", "consoleNameFleet", ADFleetNames)
|
||||||
|
end
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
-- AD Flow Guide
|
||||||
|
-- "Opsæt flow": flowguidens trin oven på ADGuide (ren Lua, ingen spil-API) — samme trin-skærm og knapper
|
||||||
|
-- som markguiden. Kladden er en kopi af flowets felter, så intet ændres før [Gem]/[Start flow]. Det
|
||||||
|
-- spil-afhængige kommer i `context` (GUI'et henter det ud fra kladden, se previewRun):
|
||||||
|
-- {buildings = {{id, name, label, hasMarker, text}} -- kandidater til Fra (udkørsel) / Til (forsyning)
|
||||||
|
-- fillTypes = {{name, title}} -- varer målet tager (forsyning, Vælg selv)
|
||||||
|
-- products = {{name, title}} -- varer kilderne har (udkørsel)
|
||||||
|
-- storeBuildings = {{id, name, text}} -- lagre der kan tage flowets lagervarer
|
||||||
|
-- waitPools = {{group, count}}
|
||||||
|
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked, solo}} -- label = kort navn (resumé)
|
||||||
|
-- wagons = {{id, label}}} -- ledige vogne til solo-traktorer (bedste først)
|
||||||
|
-- Solo-traktor (ingen vogn bagpå) får en ledig vogn fra context.wagons (draft.wagons = {[traktorId] = vognId}),
|
||||||
|
-- som den henter ved "Connect <vognens parkering>" — uden at vognens parkering skal omdøbes.
|
||||||
|
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
|
||||||
|
|
||||||
|
ADFlowGuide = {}
|
||||||
|
|
||||||
|
ADFlowGuide.ORDER = {"name", "direction", "buildings", "goods", "store", "rules", "wait", "tractors", "summary"}
|
||||||
|
ADFlowGuide.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||||
|
ADFlowGuide.MAX_NAMED_TRACTORS = 3
|
||||||
|
ADFlowGuide.MAX_NAMED_GOODS = 3
|
||||||
|
|
||||||
|
local function L(key, args) return {key = key, args = args} end
|
||||||
|
local function T(text) return {text = text} end
|
||||||
|
|
||||||
|
local function isOut(guide) return guide.draft.kind == "outbound" end
|
||||||
|
|
||||||
|
local function hasStorePolicy(draft)
|
||||||
|
for _, product in ipairs(draft.products or {}) do
|
||||||
|
if product.policy == "store" or product.policy == "sellElseStore" then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local VISIBLE = {
|
||||||
|
name = function(guide) return guide.draft.isNew == true end,
|
||||||
|
store = function(guide) return isOut(guide) and hasStorePolicy(guide.draft) end,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ADFlowGuide.isVisible(step, guide)
|
||||||
|
local rule = VISIBLE[step]
|
||||||
|
if rule ~= nil then return rule(guide) == true end
|
||||||
|
for _, id in ipairs(ADFlowGuide.ORDER) do
|
||||||
|
if id == step then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
ADFlowGuide.DEF = {order = ADFlowGuide.ORDER, isVisible = ADFlowGuide.isVisible}
|
||||||
|
|
||||||
|
-- Overskrift pr. trin; "Fra"/"Til" følger retningen.
|
||||||
|
function ADFlowGuide.stepKey(step, guide)
|
||||||
|
if step == "buildings" then
|
||||||
|
return (guide ~= nil and isOut(guide)) and "spu_guide_flow_step_from" or "spu_guide_flow_step_to"
|
||||||
|
end
|
||||||
|
return "spu_guide_flow_step_" .. tostring(step)
|
||||||
|
end
|
||||||
|
|
||||||
|
local KIND_HINTS = {buildings = true, goods = true, rules = true}
|
||||||
|
function ADFlowGuide.hintKey(step, guide)
|
||||||
|
if KIND_HINTS[step] then
|
||||||
|
return "spu_guide_flow_hint_" .. step .. ((guide ~= nil and isOut(guide)) and "_out" or "_in")
|
||||||
|
end
|
||||||
|
return "spu_guide_flow_hint_" .. tostring(step)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- kladde ----
|
||||||
|
|
||||||
|
local RUN_FIELDS = {
|
||||||
|
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
||||||
|
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
||||||
|
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons", "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 table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
||||||
|
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||||
|
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||||
|
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
local loops = {{label = L("spu_loops_infinite"), value = 0}}
|
||||||
|
for count = 1, ADRuns.MAX_LOOPS do table.insert(loops, {label = T(tostring(count)), value = count}) end
|
||||||
|
return {
|
||||||
|
choice("loops", L("spu_loops"), loops, draft.loops),
|
||||||
|
choice("returnBelowPercent", L("spu_returnBelow"), percentOptions(ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10), draft.returnBelowPercent),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rowsWait(guide)
|
||||||
|
local options = {{label = L("spu_waitPool_all"), value = ""}}
|
||||||
|
for _, pool in ipairs(guide.context.waitPools or {}) do
|
||||||
|
table.insert(options, {label = T(string.format("%s (%d)", tostring(pool.group), pool.count or 0)), value = pool.group})
|
||||||
|
end
|
||||||
|
return {choice("waitPoolGroup", L("spu_waitPool"), options, guide.draft.waitPoolGroup or "")}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vognvalg under en valgt solo-traktor: dens egen vogn + ledige vogne ingen anden traktor i flowet har.
|
||||||
|
local function rowWagon(guide, tractorId)
|
||||||
|
local wagons = guide.draft.wagons or {}
|
||||||
|
local usedByOthers = {}
|
||||||
|
for otherId, wagonId in pairs(wagons) do
|
||||||
|
if otherId ~= tractorId then usedByOthers[wagonId] = true end
|
||||||
|
end
|
||||||
|
local options = {}
|
||||||
|
for _, wagon in ipairs(guide.context.wagons or {}) do
|
||||||
|
if not usedByOthers[wagon.id] then table.insert(options, {label = T(tostring(wagon.label)), value = wagon.id}) end
|
||||||
|
end
|
||||||
|
if #options == 0 or wagons[tractorId] == nil then return text("spu_guide_flow_noFreeWagon") end
|
||||||
|
return choice("wagon", L("spu_guide_flow_wagon"), options, wagons[tractorId], tractorId)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktorer: flowets egne først, så de ledige. Traktorer i andre flows og blokerede (vognen passer ikke)
|
||||||
|
-- skjules med en linje hver.
|
||||||
|
local function rowsTractors(guide)
|
||||||
|
local draft, rows = guide.draft, {}
|
||||||
|
local inRun = chosenSet(draft.vehicleIds)
|
||||||
|
local entries = {}
|
||||||
|
local other, blocked = 0, 0
|
||||||
|
local blockedTexts = {}
|
||||||
|
for _, vehicle in ipairs(guide.context.vehicles or {}) do
|
||||||
|
local state = "free"
|
||||||
|
if inRun[vehicle.id] then
|
||||||
|
state = "in"
|
||||||
|
elseif vehicle.otherRunId ~= nil and vehicle.otherRunId ~= draft.id then
|
||||||
|
state = "other"
|
||||||
|
other = other + 1
|
||||||
|
elseif vehicle.blocked then
|
||||||
|
state = "blocked"
|
||||||
|
blocked = blocked + 1
|
||||||
|
table.insert(blockedTexts, tostring(vehicle.text))
|
||||||
|
end
|
||||||
|
table.insert(entries, {id = vehicle.id, label = tostring(vehicle.text), state = state})
|
||||||
|
end
|
||||||
|
local solo = soloIds(guide.context)
|
||||||
|
for _, entry in ipairs(ADFlowView.vehicleRows(entries, false)) do
|
||||||
|
table.insert(rows, check("vehicleIds", entry.id, entry.label, entry.state == "in"))
|
||||||
|
if entry.state == "in" and solo[entry.id] then
|
||||||
|
local wagonRow = rowWagon(guide, entry.id)
|
||||||
|
if wagonRow ~= nil then table.insert(rows, wagonRow) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #rows == 0 then table.insert(rows, text("spu_guide_flow_noTractors")) end
|
||||||
|
if other > 0 then table.insert(rows, text("spu_guide_flow_otherFlows", {other})) end
|
||||||
|
if blocked > 0 then
|
||||||
|
-- foldet sammen som standard; linjen med antallet folder navn + grund ud/ind
|
||||||
|
local key = guide.showBlocked and "spu_guide_flow_blockedHide" or "spu_guide_flow_blockedShow"
|
||||||
|
table.insert(rows, {type = "link", id = "showBlocked", value = true, label = L(key, {blocked})})
|
||||||
|
if guide.showBlocked then
|
||||||
|
table.sort(blockedTexts)
|
||||||
|
for _, line in ipairs(blockedTexts) do table.insert(rows, {type = "text", label = T(" " .. line)}) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rowsSummary(guide)
|
||||||
|
local rows = {}
|
||||||
|
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||||
|
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
|
||||||
|
end
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local ROWS = {
|
||||||
|
name = rowsName, direction = rowsDirection, buildings = rowsBuildings, goods = rowsGoods, store = rowsStore,
|
||||||
|
rules = rowsRules, wait = rowsWait, tractors = rowsTractors, summary = rowsSummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ADFlowGuide.rows(guide)
|
||||||
|
local builder = ROWS[guide.step]
|
||||||
|
return builder ~= nil and builder(guide) or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- resumé ----
|
||||||
|
|
||||||
|
-- Korte navne til resuméet (field, ellers navn, ellers rækkens tekst).
|
||||||
|
local function namesFor(ids, entries, field)
|
||||||
|
local byId = {}
|
||||||
|
for _, entry in ipairs(entries or {}) do byId[entry.id or entry.name] = entry[field] or entry.name or entry.text end
|
||||||
|
local names = {}
|
||||||
|
for _, id in ipairs(ids or {}) do table.insert(names, tostring(byId[id] or id)) end
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
local SUMMARY = {
|
||||||
|
name = function(guide) return {T(guide.draft.name or "")} end,
|
||||||
|
direction = function(guide) return {L(isOut(guide) and "spu_direction_out" or "spu_direction_in")} end,
|
||||||
|
buildings = function(guide)
|
||||||
|
local names = namesFor(buildingIds(guide.draft), guide.context.buildings, "name")
|
||||||
|
if #names == 0 then return {L("spu_guide_flow_none")} end
|
||||||
|
return {T(ADFlowView.joinCounted(names, " + ", ""))}
|
||||||
|
end,
|
||||||
|
goods = function(guide)
|
||||||
|
local draft = guide.draft
|
||||||
|
if isOut(guide) then
|
||||||
|
local parts = {}
|
||||||
|
local titles = {}
|
||||||
|
for _, product in ipairs(guide.context.products or {}) do titles[product.name] = product.title end
|
||||||
|
for _, product in ipairs(draft.products or {}) do
|
||||||
|
table.insert(parts, {key = "spu_policy_" .. product.policy, prefix = tostring(titles[product.fillType] or product.fillType) .. ": "})
|
||||||
|
end
|
||||||
|
if #parts == 0 then return {L("spu_guide_flow_none")} end
|
||||||
|
return parts
|
||||||
|
end
|
||||||
|
if draft.ingredientMode ~= "manual" then return {L("spu_guide_flow_goodsAuto")} end
|
||||||
|
if #draft.ingredients == 0 then return {L("spu_guide_flow_none")} end
|
||||||
|
if #draft.ingredients > ADFlowGuide.MAX_NAMED_GOODS then return {L("spu_guide_flow_sumGoods", {#draft.ingredients})} end
|
||||||
|
local byName = {}
|
||||||
|
for _, fillType in ipairs(guide.context.fillTypes or {}) do byName[fillType.name] = fillType.title end
|
||||||
|
local names = {}
|
||||||
|
for _, name in ipairs(draft.ingredients) do table.insert(names, tostring(byName[name] or name)) end
|
||||||
|
return {T(table.concat(names, " + "))}
|
||||||
|
end,
|
||||||
|
store = function(guide)
|
||||||
|
local draft = guide.draft
|
||||||
|
if draft.storeAuto or #draft.storeBuildingIds == 0 then return {L("spu_guide_sum_auto")} end
|
||||||
|
if #draft.storeBuildingIds > 2 then return {L("spu_guide_sum_stores", {#draft.storeBuildingIds})} end
|
||||||
|
return {T(table.concat(namesFor(draft.storeBuildingIds, guide.context.storeBuildings, "name"), " + "))}
|
||||||
|
end,
|
||||||
|
rules = function(guide)
|
||||||
|
local draft = guide.draft
|
||||||
|
if isOut(guide) then
|
||||||
|
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||||
|
local parts = {}
|
||||||
|
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||||
|
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||||
|
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||||
|
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||||
|
return parts
|
||||||
|
end
|
||||||
|
local loops = (draft.loops or 0) > 0 and L("spu_guide_flow_sumLoops", {draft.loops}) or L("spu_guide_flow_sumLoopsInf")
|
||||||
|
return {loops, L("spu_guide_flow_sumReturn", {draft.returnBelowPercent})}
|
||||||
|
end,
|
||||||
|
wait = function(guide)
|
||||||
|
local group = guide.draft.waitPoolGroup or ""
|
||||||
|
return {group == "" and L("spu_waitPool_all") or T(group)}
|
||||||
|
end,
|
||||||
|
tractors = function(guide)
|
||||||
|
local ids = guide.draft.vehicleIds
|
||||||
|
if #ids == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||||
|
if #ids > ADFlowGuide.MAX_NAMED_TRACTORS then return {L("spu_guide_flow_sumTractors", {#ids})} end
|
||||||
|
-- traktorer der ikke findes (solgt, eller ikke indlæst endnu) vises ikke som rå id
|
||||||
|
local known = {}
|
||||||
|
for _, vehicle in ipairs(guide.context.vehicles or {}) do known[vehicle.id] = true end
|
||||||
|
local present = {}
|
||||||
|
for _, id in ipairs(ids) do if known[id] then table.insert(present, id) end end
|
||||||
|
if #present == 0 then return {L("spu_guide_flow_sumNoTractors")} end
|
||||||
|
return {T(table.concat(namesFor(present, guide.context.vehicles, "label"), ", "))}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Resumé-linjer {step, title, parts}: Navn (altid) + de synlige trin.
|
||||||
|
function ADFlowGuide.summary(guide)
|
||||||
|
local lines = {}
|
||||||
|
for _, step in ipairs(ADFlowGuide.ORDER) do
|
||||||
|
local build = SUMMARY[step]
|
||||||
|
if build ~= nil and (step == "name" or ADFlowGuide.isVisible(step, guide)) then
|
||||||
|
table.insert(lines, {step = step, title = L(ADFlowGuide.stepKey(step, guide)), parts = build(guide)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- gem ----
|
||||||
|
|
||||||
|
function ADFlowGuide.result(guide)
|
||||||
|
local draft = guide.draft
|
||||||
|
local storeIds = ADGuide.deepCopy(draft.storeBuildingIds)
|
||||||
|
if ADFlowGuide.isVisible("store", guide) and draft.storeAuto then storeIds = {} end
|
||||||
|
local changes = {
|
||||||
|
kind = draft.kind, sourceBuildingIds = ADGuide.deepCopy(draft.sourceBuildingIds),
|
||||||
|
targetBuildingIds = ADGuide.deepCopy(draft.targetBuildingIds), storeBuildingIds = storeIds,
|
||||||
|
products = ADGuide.deepCopy(draft.products), ingredientMode = draft.ingredientMode,
|
||||||
|
ingredients = ADGuide.deepCopy(draft.ingredients), sellAtPercent = draft.sellAtPercent,
|
||||||
|
storeAbovePercent = draft.storeAbovePercent, sellAbovePercent = draft.sellAbovePercent or 0,
|
||||||
|
collectRound = draft.collectRound, loops = draft.loops,
|
||||||
|
returnBelowPercent = draft.returnBelowPercent, waitPoolGroup = draft.waitPoolGroup or "",
|
||||||
|
}
|
||||||
|
if draft.name ~= nil and draft.name:match("%S") then changes.name = draft.name end
|
||||||
|
return {isNew = draft.isNew == true, runId = draft.id, name = draft.name, kind = draft.kind, changes = changes,
|
||||||
|
vehicleIds = ADGuide.deepCopy(draft.vehicleIds), wagons = ADFlowGuide.resultWagons(draft)}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vogne for de valgte traktorer (til run.wagons ved gem).
|
||||||
|
function ADFlowGuide.resultWagons(draft)
|
||||||
|
local chosen, wagons = chosenSet(draft.vehicleIds), {}
|
||||||
|
for tractorId, wagonId in pairs(draft.wagons or {}) do
|
||||||
|
if chosen[tractorId] then wagons[tractorId] = wagonId end
|
||||||
|
end
|
||||||
|
return wagons
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktorer der skal tildeles/fjernes efter gem.
|
||||||
|
function ADFlowGuide.vehicleChanges(oldIds, newIds)
|
||||||
|
local old, new = chosenSet(oldIds), chosenSet(newIds)
|
||||||
|
local added, removed = {}, {}
|
||||||
|
for _, id in ipairs(newIds or {}) do if not old[id] then table.insert(added, id) end end
|
||||||
|
for _, id in ipairs(oldIds or {}) do if not new[id] then table.insert(removed, id) end end
|
||||||
|
return added, removed
|
||||||
|
end
|
||||||
@@ -101,18 +101,27 @@ end
|
|||||||
-- Bygninger: buildings = {{id, name, hasMarker}}, chosen = {[id] = true}. Valgte først; bygninger uden
|
-- 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).
|
-- 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)
|
function ADFlowView.buildingRows(buildings, chosen, showAll)
|
||||||
local picked, withMarker, withoutMarker = {}, {}, {}
|
local shown, withoutMarker = {}, {}
|
||||||
for _, building in ipairs(buildings or {}) do
|
for _, building in ipairs(buildings or {}) do
|
||||||
if chosen ~= nil and chosen[building.id] then
|
if (chosen ~= nil and chosen[building.id]) or building.hasMarker then
|
||||||
table.insert(picked, building)
|
table.insert(shown, building)
|
||||||
elseif building.hasMarker then
|
|
||||||
table.insert(withMarker, building)
|
|
||||||
elseif showAll then
|
elseif showAll then
|
||||||
table.insert(withoutMarker, building)
|
table.insert(withoutMarker, building)
|
||||||
end
|
end
|
||||||
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 = {}
|
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
|
for _, building in ipairs(list) do table.insert(rows, building) end
|
||||||
end
|
end
|
||||||
return rows
|
return rows
|
||||||
@@ -138,3 +147,53 @@ function ADFlowView.joinCounted(names, separator, empty)
|
|||||||
end
|
end
|
||||||
return table.concat(parts, separator)
|
return table.concat(parts, separator)
|
||||||
end
|
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
|
||||||
@@ -97,6 +97,19 @@ function ADLoadSwap.rankDumpCandidates(candidates, avoidMarkers)
|
|||||||
return ranked
|
return ranked
|
||||||
end
|
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)
|
function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables, avoidMarkers, allowFactories)
|
||||||
if vehicle.rootNode == nil then
|
if vehicle.rootNode == nil then
|
||||||
return nil, "køretøjet har ingen position"
|
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.
|
-- 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).
|
-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
|
||||||
function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
|
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
|
if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
|
||||||
return false
|
return false
|
||||||
end
|
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: "
|
ADOutbound.LOG_PREFIX = "ADSmartPickup: "
|
||||||
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
|
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
|
||||||
ADOutbound.CHECK_INTERVAL_MS = 5000
|
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.
|
-- "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
|
ADOutbound.NO_PROGRESS_MS = 15000
|
||||||
-- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl).
|
-- 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.
|
-- spawnet, forkert farm, ...): kilden udelukkes for varen i EMPTY_SOURCE_TTL_MS og turen afsluttes.
|
||||||
ADOutbound.EMPTY_SOURCE_MS = 30000
|
ADOutbound.EMPTY_SOURCE_MS = 30000
|
||||||
ADOutbound.EMPTY_SOURCE_TTL_MS = 300000
|
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
|
-- 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
|
-- 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.
|
-- 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 emptyUntil = {}
|
||||||
local isInstalled = false
|
local isInstalled = false
|
||||||
|
|
||||||
|
-- Glem alle "tom/hvilende kilde"-markeringer (tests; og når runs genindlæses).
|
||||||
|
function ADOutbound.resetEmptySources()
|
||||||
|
emptyUntil = {}
|
||||||
|
end
|
||||||
|
|
||||||
local function now()
|
local function now()
|
||||||
if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
|
if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
|
||||||
return g_currentMission.time
|
return g_currentMission.time
|
||||||
@@ -176,6 +186,12 @@ local function findSellQuote(adEnv, vehicle, product, sellMarkers, sellAtPercent
|
|||||||
if unreachableName ~= nil then
|
if unreachableName ~= nil then
|
||||||
return nil, unreachableName
|
return nil, unreachableName
|
||||||
end
|
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
|
return quotes[1], nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -241,7 +257,9 @@ local function findStoreMarker(adEnv, vehicle, product, capacity, sources, liter
|
|||||||
return {wayPointId = chosen.id, name = tostring(chosen.name), free = liters}
|
return {wayPointId = chosen.id, name = tostring(chosen.name), free = liters}
|
||||||
end
|
end
|
||||||
local avoid = ADRunsController ~= nil and ADRunsController.getPickupMarkers ~= nil and ADRunsController.getPickupMarkers() or nil
|
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
|
if marker == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -346,6 +364,7 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle)
|
|||||||
hasAutoload = hasAutoload,
|
hasAutoload = hasAutoload,
|
||||||
sellAtPercent = run.sellAtPercent,
|
sellAtPercent = run.sellAtPercent,
|
||||||
storeAbovePercent = run.storeAbovePercent,
|
storeAbovePercent = run.storeAbovePercent,
|
||||||
|
sellAbovePercent = run.sellAbovePercent,
|
||||||
collectRound = run.collectRound == true,
|
collectRound = run.collectRound == true,
|
||||||
products = products,
|
products = products,
|
||||||
}
|
}
|
||||||
@@ -474,7 +493,17 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
|||||||
local isReachable = function(candidate)
|
local isReachable = function(candidate)
|
||||||
return ADWaitPool.hasRouteTo(adEnv, vehicle, candidate.marker.id)
|
return ADWaitPool.hasRouteTo(adEnv, vehicle, candidate.marker.id)
|
||||||
end
|
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
|
local marker, source = nil, nil
|
||||||
if candidate ~= nil then
|
if candidate ~= nil then
|
||||||
ADWaitPool.claim(candidate.marker.id, vehicle)
|
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)))
|
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
|
end
|
||||||
local previous = holds[vehicle]
|
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, "; "))
|
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
|
if previous == nil or previous.lastLogged ~= message then
|
||||||
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
|
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
|
||||||
@@ -753,7 +783,12 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if state.noProgressMs > ADOutbound.NO_PROGRESS_MS and level > 0 and not spawnPending then
|
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
|
state.noProgressMs, state.completed = 0, true
|
||||||
task:finished()
|
task:finished()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
|||||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||||
percent = percent,
|
percent = percent,
|
||||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||||
|
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||||
|
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -123,7 +125,7 @@ local function collectCandidates(input)
|
|||||||
local e = ADOutboundPlanner.evaluate(product, input)
|
local e = ADOutboundPlanner.evaluate(product, input)
|
||||||
local added = false
|
local added = false
|
||||||
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
|
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})
|
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
|
||||||
added = true
|
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
|
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh or (input.feedMode and e.available >= ADOutboundPlanner.FEED_MIN_LITERS)) and product.store ~= nil then
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
-- AD Pool
|
||||||
|
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
||||||
|
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
||||||
|
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
||||||
|
|
||||||
|
ADPool = {}
|
||||||
|
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
||||||
|
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
||||||
|
-- tip/snegl er tømmevogn.
|
||||||
|
function ADPool.roleOf(specs)
|
||||||
|
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||||
|
if role ~= nil and role ~= "unloader" then return role end
|
||||||
|
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
||||||
|
return role
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||||
|
function ADPool.numberOf(value)
|
||||||
|
if type(value) == "number" then return value end
|
||||||
|
if type(value) ~= "table" then return nil end
|
||||||
|
local best = nil
|
||||||
|
for _, entry in pairs(value) do
|
||||||
|
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
||||||
|
function ADPool.powerOk(tractorHp, neededHp)
|
||||||
|
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
||||||
|
return tractorHp >= neededHp
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butikskategori: markredskaber kun bag traktorer (tractorsS/M/L …) — ikke lastbil/Unimog, læssere o.l.
|
||||||
|
-- Tømmevogne må alle trække. Ukendt kategori tillades (intet må gå i stå på manglende data).
|
||||||
|
ADPool.ANY_PULLER_ROLES = {unloader = true}
|
||||||
|
|
||||||
|
function ADPool.categoryOk(category, role)
|
||||||
|
if category == nil or ADPool.ANY_PULLER_ROLES[role] then return true end
|
||||||
|
return tostring(category):lower():find("^tractors") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function suits(tractor, implement, role)
|
||||||
|
return ADPool.powerOk(tractor.hp, implement.neededHp) and ADPool.categoryOk(tractor.category, role)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
||||||
|
function ADPool.connectFor(parkName, markers)
|
||||||
|
if type(parkName) ~= "string" then return nil end
|
||||||
|
local wanted = parkName:lower()
|
||||||
|
for _, marker in pairs(markers or {}) do
|
||||||
|
local action, target = ADHitch.parseMarker(marker.name)
|
||||||
|
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasRole(item, role)
|
||||||
|
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function distanceSq(ax, az, bx, bz)
|
||||||
|
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nearest(items, x, z, keep)
|
||||||
|
local best, bestDistance = nil, math.huge
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if keep == nil or keep(item) then
|
||||||
|
local distance = distanceSq(item.x, item.z, x, z)
|
||||||
|
if distance < bestDistance then best, bestDistance = item, distance end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Når opgaven slutter, sættes puljeredskabet af — også når AutoDrive ellers selv ville parkere (påfyldte
|
||||||
|
-- sæt): AD parkerer ved redskabets plads med redskabet på. Aldrig ved manuel transport.
|
||||||
|
-- Bedste pasform: den mindste traktor med hk nok; afstand afgør kun mellem traktorer inden for FIT_BAND af den
|
||||||
|
-- mindste. Over OVERSIZE_FACTOR x behovet kun når intet andet passer; ukendt traktor-hk efter de kendte.
|
||||||
|
-- Redskab uden hk-data: nærmeste (intet at matche på). tractors = allerede filtrerede kandidater.
|
||||||
|
ADPool.FIT_BAND = 1.15
|
||||||
|
ADPool.OVERSIZE_FACTOR = 2.5
|
||||||
|
|
||||||
|
function ADPool.bestFit(tractors, implement, role)
|
||||||
|
local fitting = {}
|
||||||
|
for _, tractor in ipairs(tractors or {}) do
|
||||||
|
if suits(tractor, implement, role) then table.insert(fitting, tractor) end
|
||||||
|
end
|
||||||
|
local needed = implement.neededHp
|
||||||
|
if needed == nil or needed <= 0 then return nearest(fitting, implement.x, implement.z) end
|
||||||
|
local sized, oversized, unknown = {}, {}, {}
|
||||||
|
for _, tractor in ipairs(fitting) do
|
||||||
|
if tractor.hp == nil or tractor.hp <= 0 then table.insert(unknown, tractor)
|
||||||
|
elseif tractor.hp > needed * ADPool.OVERSIZE_FACTOR then table.insert(oversized, tractor)
|
||||||
|
else table.insert(sized, tractor) end
|
||||||
|
end
|
||||||
|
local pool = #sized > 0 and sized or (#unknown > 0 and unknown or oversized)
|
||||||
|
if pool ~= unknown then
|
||||||
|
local smallest = math.huge
|
||||||
|
for _, tractor in ipairs(pool) do smallest = math.min(smallest, tractor.hp) end
|
||||||
|
local band = {}
|
||||||
|
for _, tractor in ipairs(pool) do
|
||||||
|
if tractor.hp <= smallest * ADPool.FIT_BAND then table.insert(band, tractor) end
|
||||||
|
end
|
||||||
|
pool = band
|
||||||
|
end
|
||||||
|
return nearest(pool, implement.x, implement.z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.shouldReturnHome(assignment)
|
||||||
|
return not assignment.manual
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kun maskiner uden egen arbejdsrolle (almindelige traktorer) henter puljeredskaber — ikke mejetærskere,
|
||||||
|
-- selvkørende sprøjter o.l., selv om de har koblingspunkter.
|
||||||
|
function ADPool.canPull(role)
|
||||||
|
return role == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvem fik koblingen til at fejle? Kom traktoren ikke frem, er det traktoren (ingen AD-rute), ellers redskabet.
|
||||||
|
function ADPool.failureBlames(reason)
|
||||||
|
if tostring(reason):find("^stoppedAway") ~= nil then return "tractor" end
|
||||||
|
return "implement"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Efter koblingen: refill = markarbejdets påfyldningsplan (nil = intet at fylde).
|
||||||
|
function ADPool.nextDrive(refill)
|
||||||
|
if refill == nil or not refill.empty then return "drive" end
|
||||||
|
if refill.refillId ~= nil then return "load" end
|
||||||
|
return "empty"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Puljen bruges til opgaver med ét sæt (også tømmevogne ved høst), ikke når marken har egne faste maskiner
|
||||||
|
-- til rollen.
|
||||||
|
function ADPool.mayUsePool(decision, hasOwnForRole)
|
||||||
|
return (decision.count or 1) == 1 and not hasOwnForRole
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kan puljevognen bære varen? (implement.carries = {[fillType] = true}; nil vare = intet krav)
|
||||||
|
function ADPool.canCarry(implement, fillType)
|
||||||
|
if fillType == nil then return true end
|
||||||
|
return implement.carries ~= nil and implement.carries[fillType] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Status til flådefanen. s = {parkName, hasConnect, attachedTo, quarantineMin, reserved} -> nøgle, argument
|
||||||
|
function ADPool.implementStatus(s)
|
||||||
|
if s.parkName == nil then return "noPark" end
|
||||||
|
if not s.hasConnect then return "noConnect", s.parkName end
|
||||||
|
if s.attachedTo ~= nil then return "attached", s.attachedTo end
|
||||||
|
if s.flowName ~= nil then return "flow", s.flowName end
|
||||||
|
if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end
|
||||||
|
if s.reserved then return "reserved" end
|
||||||
|
return "parked", s.parkName
|
||||||
|
end
|
||||||
|
|
||||||
|
-- s = {hasPark, enabled, controlled, returning, fieldId, busy, offNetwork, hitched} -> nøgle, argument
|
||||||
|
function ADPool.tractorStatus(s)
|
||||||
|
if not s.hasPark then return "noPark" end
|
||||||
|
if s.controlled then return "player" end
|
||||||
|
if s.returning then return "returning" end
|
||||||
|
if s.fieldId ~= nil then return "job", s.fieldId end
|
||||||
|
if s.busy then return "busy" end
|
||||||
|
if not s.enabled then return "notInFleet" end
|
||||||
|
if s.offNetwork then return "offNetwork" end
|
||||||
|
if s.hitched then return "hitched" end
|
||||||
|
return "free"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet.
|
||||||
|
-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool.
|
||||||
|
function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept)
|
||||||
|
blocked = blocked or {}
|
||||||
|
local free = {}
|
||||||
|
for _, tractor in ipairs(tractors or {}) do
|
||||||
|
if not blocked[tractor.id] and not tractor.offNetwork then table.insert(free, tractor) end
|
||||||
|
end
|
||||||
|
local candidates = {}
|
||||||
|
for _, implement in ipairs(implements or {}) do
|
||||||
|
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
||||||
|
table.insert(candidates, implement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #candidates == 0 then return nil, "noPoolImplement" end
|
||||||
|
if #free == 0 then return nil, "noPoolTractor" end
|
||||||
|
table.sort(candidates, function(a, b)
|
||||||
|
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
||||||
|
end)
|
||||||
|
local weakest = nil
|
||||||
|
for _, implement in ipairs(candidates) do
|
||||||
|
local tractor = ADPool.bestFit(free, implement, role)
|
||||||
|
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||||
|
if weakest == nil or (implement.neededHp or 0) < (weakest.neededHp or 0) then weakest = implement end
|
||||||
|
end
|
||||||
|
return nil, "tooWeak", {weakest ~= nil and weakest.neededHp or 0, weakest ~= nil and weakest.name or "?"}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle frie puljeredskaber med rollen, hver parret med den nærmeste endnu ubrugte traktor med nok hk
|
||||||
|
-- (marksilo-flowet planlægger over mange vogne på én gang).
|
||||||
|
function ADPool.pairAll(role, implements, tractors, blocked)
|
||||||
|
blocked = blocked or {}
|
||||||
|
local used, result = {}, {}
|
||||||
|
for _, implement in ipairs(implements or {}) do
|
||||||
|
if hasRole(implement, role) and not blocked[implement.id] then
|
||||||
|
local open = {}
|
||||||
|
for _, t in ipairs(tractors or {}) do
|
||||||
|
if not used[t.id] and not blocked[t.id] and not t.offNetwork then table.insert(open, t) end
|
||||||
|
end
|
||||||
|
local tractor = ADPool.bestFit(open, implement, role)
|
||||||
|
if tractor ~= nil then
|
||||||
|
used[tractor.id] = true
|
||||||
|
table.insert(result, {implement = implement, tractor = tractor})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ventende marker prøver igen på fælles taktslag (ikke hver sin 30 s fra eget forsøg): så udløber de i samme
|
||||||
|
-- tick, og køens rækkefølge (længst ventende først) afgør hvem der får den næste ledige traktor.
|
||||||
|
function ADPool.alignedRetry(nowMs, periodMs)
|
||||||
|
return (math.floor(nowMs / periodMs) + 1) * periodMs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
||||||
|
function ADPool.queueOrder(infos, waitingSince)
|
||||||
|
local waiting, rest = {}, {}
|
||||||
|
for _, info in ipairs(infos or {}) do
|
||||||
|
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
||||||
|
end
|
||||||
|
table.sort(waiting, function(a, b)
|
||||||
|
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
||||||
|
if wa ~= wb then return wa < wb end
|
||||||
|
return a.id < b.id
|
||||||
|
end)
|
||||||
|
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
||||||
|
return waiting
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
||||||
|
local reservations, quarantined = {}, {}
|
||||||
|
|
||||||
|
function ADPool.reserve(owner, ids)
|
||||||
|
reservations[owner] = ids
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.release(owner)
|
||||||
|
reservations[owner] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.quarantine(id, nowMs)
|
||||||
|
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.quarantineMinutes(id, nowMs)
|
||||||
|
local untilMs = quarantined[id]
|
||||||
|
if untilMs == nil or nowMs >= untilMs then return 0 end
|
||||||
|
return math.ceil((untilMs - nowMs) / 60000)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.isReserved(id)
|
||||||
|
for _, ids in pairs(reservations) do
|
||||||
|
for _, reserved in ipairs(ids) do if reserved == id then return true end end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.blocked(nowMs)
|
||||||
|
local result = {}
|
||||||
|
for _, ids in pairs(reservations) do
|
||||||
|
for _, id in ipairs(ids) do result[id] = true end
|
||||||
|
end
|
||||||
|
for id, untilMs in pairs(quarantined) do
|
||||||
|
if nowMs < untilMs then result[id] = true end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
||||||
|
local returning = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
function ADPool.setReturning(vehicle, isOn)
|
||||||
|
returning[vehicle] = isOn and true or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.isReturning(vehicle)
|
||||||
|
return returning[vehicle] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
local function parkMarker(adEnv, vehicle)
|
||||||
|
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||||
|
if type(index) ~= "number" or index < 1 then return nil end
|
||||||
|
return adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function positionOf(vehicle)
|
||||||
|
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||||
|
return x, z
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
||||||
|
local neededHpCache = {} -- configFileName -> hk eller false (butiksdata ændrer sig ikke i et spil)
|
||||||
|
|
||||||
|
local function neededHpOf(vehicle)
|
||||||
|
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||||
|
local cached = neededHpCache[vehicle.configFileName]
|
||||||
|
if cached ~= nil then return cached or nil end
|
||||||
|
local value = ADPool.readNeededHp(vehicle)
|
||||||
|
neededHpCache[vehicle.configFileName] = value or false
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.readNeededHp(vehicle)
|
||||||
|
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||||
|
if item == nil then return nil end
|
||||||
|
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
||||||
|
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
||||||
|
end
|
||||||
|
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local tractorHpCache = setmetatable({}, {__mode = "k"}) -- vehicle -> hk eller false
|
||||||
|
|
||||||
|
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. Gemmes pr. traktor.
|
||||||
|
local function tractorHpOf(vehicle)
|
||||||
|
local cached = tractorHpCache[vehicle]
|
||||||
|
if cached ~= nil then return cached or nil end
|
||||||
|
local value = ADPool.readTractorHp(vehicle)
|
||||||
|
tractorHpCache[vehicle] = value or false
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.readTractorHp(vehicle)
|
||||||
|
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
||||||
|
local kw = motor ~= nil and motor.peakMotorPower or nil
|
||||||
|
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
||||||
|
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||||
|
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||||
|
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butikskategori (fx "TRACTORSM", "TRUCKS") eller nil.
|
||||||
|
local function categoryOf(vehicle)
|
||||||
|
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||||
|
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||||
|
return item ~= nil and item.categoryName or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Varer og samlet kapacitet i redskabets fyldenheder (tømmevogne).
|
||||||
|
function ADPool.cargoOf(vehicle)
|
||||||
|
local carries, capacity = {}, 0
|
||||||
|
local units = vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits or {}
|
||||||
|
for index in ipairs(units) do
|
||||||
|
capacity = capacity + (vehicle:getFillUnitCapacity(index) or 0)
|
||||||
|
for fillType in pairs(vehicle:getFillUnitSupportedFillTypes(index) or {}) do carries[fillType] = true end
|
||||||
|
end
|
||||||
|
return carries, capacity
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ikke bundet af et kørende flow (traktorer i stoppede flows er ledige).
|
||||||
|
function ADPool.isFreeFromFlows(vehicle)
|
||||||
|
if ADFlowWagons ~= nil then return ADFlowWagons.isFreeTractor(vehicle) end
|
||||||
|
return ADRunsController.getRunForVehicle(vehicle) == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isAttached(vehicle)
|
||||||
|
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktorens egen rolle uden de redskaber der hænger på (mejetærsker/selvkørende har en, en traktor ikke).
|
||||||
|
function ADPool.ownRole(vehicle)
|
||||||
|
local specs = {}
|
||||||
|
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||||
|
if vehicle[name] ~= nil then specs[name] = true end
|
||||||
|
end
|
||||||
|
return ADFieldFleet.roleFromSpecs(specs)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer.
|
||||||
|
-- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur).
|
||||||
|
function ADPool.collect(adEnv, isBusy)
|
||||||
|
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
local implements, tractors = {}, {}
|
||||||
|
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
||||||
|
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||||
|
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||||
|
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then
|
||||||
|
local park = parkMarker(adEnv, vehicle)
|
||||||
|
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
||||||
|
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
||||||
|
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
||||||
|
local held = role ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(ADRunsController.getVehicleId(vehicle))
|
||||||
|
if role ~= nil and not held then
|
||||||
|
local x, z = positionOf(vehicle)
|
||||||
|
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||||
|
table.insert(implements, {carries = carries, capacity = capacity,
|
||||||
|
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||||
|
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||||
|
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local state = ADFieldJobs.state
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
local park = parkMarker(adEnv, vehicle)
|
||||||
|
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||||
|
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
||||||
|
and ADPool.isFreeFromFlows(vehicle) and not controlled and not isBusy(vehicle)
|
||||||
|
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)
|
||||||
|
and ADPool.canPull(ADPool.ownRole(vehicle)) then
|
||||||
|
local x, z = positionOf(vehicle)
|
||||||
|
-- uden for AD-vejnettet: den kan ikke køre ud efter redskabet (ville sende redskaber i karantæne)
|
||||||
|
local networkDistance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||||
|
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||||
|
hp = tractorHpOf(vehicle), category = categoryOf(vehicle), parkId = park.id,
|
||||||
|
offNetwork = networkDistance ~= nil and networkDistance > ADFieldWork.MAX_OFF_NETWORK})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return implements, tractors
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Oversigt til flådefanen: alle redskaber med en markrolle (også dem der mangler parkering/Connect, så
|
||||||
|
-- spilleren kan se hvorfor) og alle traktorer uden egen arbejdsrolle, der ikke hører til et flow.
|
||||||
|
function ADPool.overview(adEnv)
|
||||||
|
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
local nowMs = ADFieldJobs.now or 0
|
||||||
|
local implements, tractors = {}, {}
|
||||||
|
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||||
|
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||||
|
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil then
|
||||||
|
local role = ADPool.roleOf(ADCpBridge.getSpecs(vehicle))
|
||||||
|
if role ~= nil then
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
local park = parkMarker(adEnv, vehicle)
|
||||||
|
local attacher = isAttached(vehicle) and vehicle:getAttacherVehicle() or nil
|
||||||
|
local key, arg = ADPool.implementStatus({
|
||||||
|
parkName = park ~= nil and park.name or nil,
|
||||||
|
hasConnect = park ~= nil and ADPool.connectFor(park.name, markers) ~= nil,
|
||||||
|
attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil,
|
||||||
|
quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0,
|
||||||
|
reserved = id ~= nil and ADPool.isReserved(id),
|
||||||
|
flowName = id ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(id)
|
||||||
|
and tostring(ADFlowWagons.ownerOf(ADRunsController.store.runs, id).name) or nil,
|
||||||
|
})
|
||||||
|
table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local state = ADFieldJobs.state
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
-- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination
|
||||||
|
if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADPool.isFreeFromFlows(vehicle)
|
||||||
|
and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then
|
||||||
|
local x, z = positionOf(vehicle)
|
||||||
|
local distance = ADFieldWork.distanceToNetwork(adEnv, x, z)
|
||||||
|
local assignment = state.assignments[id]
|
||||||
|
local rear = ADHitch.rearImplementsOf(vehicle)
|
||||||
|
local key, arg = ADPool.tractorStatus({
|
||||||
|
hasPark = parkMarker(adEnv, vehicle) ~= nil,
|
||||||
|
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
||||||
|
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled(),
|
||||||
|
returning = ADPool.isReturning(vehicle),
|
||||||
|
fieldId = assignment ~= nil and assignment.fieldId or nil,
|
||||||
|
busy = ADFieldWork.isBusyVehicle(vehicle, id),
|
||||||
|
offNetwork = distance ~= nil and distance > ADFieldWork.MAX_OFF_NETWORK,
|
||||||
|
hitched = #rear > 0,
|
||||||
|
})
|
||||||
|
table.insert(tractors, {id = id, label = ADRuns.vehicleLabel(vehicle), hp = tractorHpOf(vehicle), status = key, arg = arg,
|
||||||
|
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return implements, tractors
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,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.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_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
|
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 function copyList(list)
|
||||||
local copy = {}
|
local copy = {}
|
||||||
@@ -39,6 +42,37 @@ local function clampRange(value, default, min, max)
|
|||||||
return math.max(min, math.min(max, math.floor(number)))
|
return math.max(min, math.min(max, math.floor(number)))
|
||||||
end
|
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)
|
local function copyRun(run)
|
||||||
return {
|
return {
|
||||||
id = run.id,
|
id = run.id,
|
||||||
@@ -51,6 +85,7 @@ local function copyRun(run)
|
|||||||
products = copyProducts(run.products),
|
products = copyProducts(run.products),
|
||||||
sellAtPercent = run.sellAtPercent,
|
sellAtPercent = run.sellAtPercent,
|
||||||
storeAbovePercent = run.storeAbovePercent,
|
storeAbovePercent = run.storeAbovePercent,
|
||||||
|
sellAbovePercent = run.sellAbovePercent,
|
||||||
collectRound = run.collectRound,
|
collectRound = run.collectRound,
|
||||||
targetWayPointId = run.targetWayPointId,
|
targetWayPointId = run.targetWayPointId,
|
||||||
targetWayPointIds = copyList(run.targetWayPointIds),
|
targetWayPointIds = copyList(run.targetWayPointIds),
|
||||||
@@ -60,6 +95,9 @@ local function copyRun(run)
|
|||||||
loops = run.loops,
|
loops = run.loops,
|
||||||
returnBelowPercent = run.returnBelowPercent,
|
returnBelowPercent = run.returnBelowPercent,
|
||||||
vehicleIds = copyList(run.vehicleIds),
|
vehicleIds = copyList(run.vehicleIds),
|
||||||
|
stopped = run.stopped == true,
|
||||||
|
wagons = ADRuns.copyWagons(run.wagons),
|
||||||
|
chain = run.chain or "",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,6 +136,7 @@ function ADRuns.createRun(store, name, kind)
|
|||||||
products = {},
|
products = {},
|
||||||
sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT,
|
sellAtPercent = ADRuns.DEFAULT_SELL_AT_PERCENT,
|
||||||
storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT,
|
storeAbovePercent = ADRuns.DEFAULT_STORE_ABOVE_PERCENT,
|
||||||
|
sellAbovePercent = ADRuns.DEFAULT_SELL_ABOVE_PERCENT,
|
||||||
collectRound = false,
|
collectRound = false,
|
||||||
targetWayPointId = nil,
|
targetWayPointId = nil,
|
||||||
targetWayPointIds = {},
|
targetWayPointIds = {},
|
||||||
@@ -107,6 +146,7 @@ function ADRuns.createRun(store, name, kind)
|
|||||||
loops = 0,
|
loops = 0,
|
||||||
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
||||||
vehicleIds = {},
|
vehicleIds = {},
|
||||||
|
chain = "",
|
||||||
}
|
}
|
||||||
new.nextId = new.nextId + 1
|
new.nextId = new.nextId + 1
|
||||||
table.insert(new.runs, run)
|
table.insert(new.runs, run)
|
||||||
@@ -164,7 +204,9 @@ function ADRuns.updateRun(store, runId, changes)
|
|||||||
if changes.products ~= nil then run.products = copyProducts(changes.products) 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
|
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||||
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
if changes.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.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
||||||
|
if changes.chain ~= nil then run.chain = tostring(changes.chain) end
|
||||||
return new
|
return new
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -308,10 +350,43 @@ function ADRuns.unassignVehicle(store, vehicleId)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
return new
|
return new
|
||||||
end
|
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).
|
-- 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)
|
function ADRuns.assignVehicle(store, runId, vehicleId)
|
||||||
local previous = ADRuns.findRunByVehicle(store, vehicleId)
|
local previous = ADRuns.findRunByVehicle(store, vehicleId)
|
||||||
@@ -364,7 +439,11 @@ function ADRuns.toRecords(store)
|
|||||||
products = ADRuns.serializeProducts(run.products),
|
products = ADRuns.serializeProducts(run.products),
|
||||||
sellAt = run.sellAtPercent,
|
sellAt = run.sellAtPercent,
|
||||||
storeAbove = run.storeAbovePercent,
|
storeAbove = run.storeAbovePercent,
|
||||||
|
sellAbove = run.sellAbovePercent or 0,
|
||||||
collectRound = run.collectRound and 1 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
|
end
|
||||||
return records
|
return records
|
||||||
@@ -407,6 +486,7 @@ function ADRuns.fromRecords(records)
|
|||||||
products = ADRuns.parseProducts(record.products),
|
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 = 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),
|
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,
|
collectRound = tonumber(record.collectRound) == 1,
|
||||||
targetWayPointId = targets[1],
|
targetWayPointId = targets[1],
|
||||||
targetWayPointIds = targets,
|
targetWayPointIds = targets,
|
||||||
@@ -416,6 +496,9 @@ function ADRuns.fromRecords(records)
|
|||||||
loops = clampLoops(record.loops),
|
loops = clampLoops(record.loops),
|
||||||
returnBelowPercent = clampPercent(record.returnBelow),
|
returnBelowPercent = clampPercent(record.returnBelow),
|
||||||
vehicleIds = copyList(record.vehicleIds),
|
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)
|
store.nextId = math.max(store.nextId, id + 1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ end
|
|||||||
|
|
||||||
function ADRunsController.deleteRun(runId, adEnv)
|
function ADRunsController.deleteRun(runId, adEnv)
|
||||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
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
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet")
|
ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet")
|
||||||
end
|
end
|
||||||
|
if ADFlowWagons ~= nil then pcall(ADFlowWagons.sendHome, adEnv, run) end
|
||||||
end
|
end
|
||||||
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
|
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
|
||||||
end
|
end
|
||||||
@@ -256,6 +257,11 @@ function ADRunsController.applyVehicleNamesThrottled(adEnv, dt)
|
|||||||
if nameTimerMs < ADRunsController.NAME_INTERVAL_MS then return end
|
if nameTimerMs < ADRunsController.NAME_INTERVAL_MS then return end
|
||||||
nameTimerMs = 0
|
nameTimerMs = 0
|
||||||
ADRunsController.applyVehicleNames(adEnv)
|
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
|
end
|
||||||
|
|
||||||
-- ---- varer ----
|
-- ---- varer ----
|
||||||
@@ -372,24 +378,49 @@ function ADRunsController.chooseTarget(adEnv, run, vehicle)
|
|||||||
end
|
end
|
||||||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||||||
local ingredients = ADRunsController.getRunIngredients(adEnv, run, 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
|
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
|
-- 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 pending = ADSmartPickup.getPending ~= nil and ADSmartPickup.getPending(entry.wayPointId, vehicle) or nil
|
||||||
local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId, pending)
|
local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId, pending)
|
||||||
local need = ranked[1]
|
local need = ranked[1]
|
||||||
if need ~= nil and need.freeSpace >= (ADSupplyPlanner.MIN_FREE_LITERS or 1000) then
|
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
|
local isBetter = bestNeed == nil
|
||||||
or (need.fits == true and bestNeed.fits ~= true)
|
or inbound < bestInbound
|
||||||
or (need.fits == bestNeed.fits and need.score > bestNeed.score)
|
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
|
if isBetter then
|
||||||
bestId, bestNeed = entry.wayPointId, need
|
bestId, bestNeed, bestInbound = entry.wayPointId, need, inbound
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return bestId or targets[1]
|
return bestId or targets[1]
|
||||||
end
|
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.
|
-- Fire traktorer kan hedde det samme; løbenummeret er pladsen i farmens AD-køretøjsliste.
|
||||||
function ADRunsController.getVehicleLabel(adEnv, vehicle)
|
function ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||||
local label, model = tostring(ADRuns.vehicleLabel(vehicle)), tostring(vehicle:getName())
|
local label, model = tostring(ADRuns.vehicleLabel(vehicle)), tostring(vehicle:getName())
|
||||||
@@ -812,7 +843,9 @@ function ADRunsController.validateVehicle(adEnv, run, vehicle)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
if not intersects(ADRunsController.getRunIngredients(adEnv, run, vehicle), carriable) then
|
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
|
return result
|
||||||
end
|
end
|
||||||
if isFarFromNetwork(adEnv, vehicle) then
|
if isFarFromNetwork(adEnv, vehicle) then
|
||||||
@@ -886,6 +919,7 @@ local function setSetting(adEnv, vehicle, settingName, value)
|
|||||||
end
|
end
|
||||||
end
|
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.
|
-- 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)
|
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 targetId = ADRunsController.chooseTarget(adEnv, run, vehicle) or run.targetWayPointId
|
||||||
local dump = ADRunsController.findWrongCargoDump(adEnv, run, vehicle, targetId)
|
local dump = ADRunsController.findWrongCargoDump(adEnv, run, vehicle, targetId)
|
||||||
stateModule:setSecondMarkerByWayPointId(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 dump ~= nil then
|
||||||
if ADUnloadWait ~= nil and ADUnloadWait.setDumpTarget ~= nil then
|
if ADUnloadWait ~= nil and ADUnloadWait.setDumpTarget ~= nil then
|
||||||
ADUnloadWait.setDumpTarget(vehicle, dump.marker.id)
|
ADUnloadWait.setDumpTarget(vehicle, dump.marker.id)
|
||||||
@@ -1035,41 +1073,174 @@ end
|
|||||||
|
|
||||||
-- Pr. tildelt traktor: validér, skriv AD-tilstand, start. Afviste traktorer springes over
|
-- Pr. tildelt traktor: validér, skriv AD-tilstand, start. Afviste traktorer springes over
|
||||||
-- og logges med årsag (samme nøgle som fanen viser).
|
-- 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)
|
function ADRunsController.startRun(adEnv, runId)
|
||||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
local results = {}
|
local results = {}
|
||||||
if run == nil then
|
if run == nil then
|
||||||
return results
|
return results
|
||||||
end
|
end
|
||||||
|
run.stopped = false
|
||||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||||
local started = 0
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
|
local vehicle = vehiclesById[vehicleId]
|
||||||
|
if vehicle ~= nil then
|
||||||
|
table.insert(results, ADRunsController.startVehicle(adEnv, run, vehicle))
|
||||||
|
end
|
||||||
|
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
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
local vehicle = vehiclesById[vehicleId]
|
local vehicle = vehiclesById[vehicleId]
|
||||||
if vehicle ~= nil then
|
if vehicle ~= nil then
|
||||||
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||||
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
|
-- solo-traktor med husket vogn: henter den ved start (valideringen ser den uden vogn)
|
||||||
if not validation.ok then
|
if validation.ok or (ADFlowWagons ~= nil and (validation.reason == "noTrailer" or ADFlowWagons.hasWagonFor(run, vehicleId))) then
|
||||||
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
|
table.insert(queue, {runId = run.id, vehicleId = vehicleId})
|
||||||
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
|
else
|
||||||
result.ok, result.reason = false, "error"
|
table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding})
|
||||||
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.insert(results, result)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if started > 0 then
|
|
||||||
ADRunsController.runningRuns[runId] = true
|
|
||||||
end
|
end
|
||||||
return results
|
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
|
end
|
||||||
|
|
||||||
-- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job —
|
-- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job —
|
||||||
@@ -1089,6 +1260,11 @@ function ADRunsController.stopRun(adEnv, runId)
|
|||||||
if run == nil then
|
if run == nil then
|
||||||
return stopped
|
return stopped
|
||||||
end
|
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)
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
local vehicle = vehiclesById[vehicleId]
|
local vehicle = vehiclesById[vehicleId]
|
||||||
@@ -1099,6 +1275,12 @@ function ADRunsController.stopRun(adEnv, runId)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
ADRunsController.runningRuns[runId] = nil
|
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
|
return stopped
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1113,11 +1295,15 @@ function ADRunsController.getRunStatus(adEnv, run)
|
|||||||
running = running + 1
|
running = running + 1
|
||||||
end
|
end
|
||||||
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
|
if ADRunsController.runningRuns[run.id] and running == 0 then
|
||||||
ADRunsController.runningRuns[run.id] = nil
|
ADRunsController.runningRuns[run.id] = nil
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Start alle (Oversigt): flows der allerede kører røres ikke, så et halvt kørende gårdbillede kan
|
-- 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 = {}}
|
local summary = {runsStarted = 0, alreadyRunning = 0, vehiclesStarted = 0, results = {}}
|
||||||
for _, run in ipairs(ADRunsController.store.runs) do
|
for _, run in ipairs(ADRunsController.store.runs) do
|
||||||
if #run.vehicleIds > 0 then
|
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
|
summary.alreadyRunning = summary.alreadyRunning + 1
|
||||||
|
if action == "resume" then run.stopped = false end
|
||||||
else
|
else
|
||||||
local startedHere = 0
|
local startedHere = 0
|
||||||
for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do
|
for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do
|
||||||
@@ -1287,6 +1475,14 @@ function ADRunsController.save()
|
|||||||
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||||
pcall(ADFieldJobs.save)
|
pcall(ADFieldJobs.save)
|
||||||
end
|
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
|
||||||
|
-- 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
|
return saved
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1298,7 +1494,11 @@ function ADRunsController.load(adEnv)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
ADRunsController.store = store
|
ADRunsController.store = store
|
||||||
|
if ADNames ~= nil then pcall(ADNames.load) end
|
||||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
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 ADCpResume ~= nil then pcall(ADCpResume.load) end
|
||||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||||
for _, run in ipairs(store.runs) do
|
for _, run in ipairs(store.runs) do
|
||||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
@@ -1309,6 +1509,27 @@ function ADRunsController.load(adEnv)
|
|||||||
end
|
end
|
||||||
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()
|
function ADRunsController.installSaveHook()
|
||||||
if ItemSystem == nil or ItemSystem.save == nil or Utils == nil then
|
if ItemSystem == nil or ItemSystem.save == nil or Utils == nil then
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ function ADRunsStorage.writeRecords(xmlFile, records)
|
|||||||
xmlFile:setString(key .. "#products", record.products or "")
|
xmlFile:setString(key .. "#products", record.products or "")
|
||||||
xmlFile:setInt(key .. "#sellAt", record.sellAt or ADRuns.DEFAULT_SELL_AT_PERCENT)
|
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 .. "#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)
|
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
|
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||||
end
|
end
|
||||||
@@ -74,7 +79,11 @@ function ADRunsStorage.readRecords(xmlFile)
|
|||||||
products = xmlFile:getString(key .. "#products"),
|
products = xmlFile:getString(key .. "#products"),
|
||||||
sellAt = xmlFile:getInt(key .. "#sellAt"),
|
sellAt = xmlFile:getInt(key .. "#sellAt"),
|
||||||
storeAbove = xmlFile:getInt(key .. "#storeAbove"),
|
storeAbove = xmlFile:getInt(key .. "#storeAbove"),
|
||||||
|
sellAbove = xmlFile:getInt(key .. "#sellAbove"),
|
||||||
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
||||||
|
stopped = xmlFile:getInt(key .. "#stopped"),
|
||||||
|
wagons = xmlFile:getString(key .. "#wagons"),
|
||||||
|
chain = xmlFile:getString(key .. "#chain"),
|
||||||
vehicleIds = {},
|
vehicleIds = {},
|
||||||
}
|
}
|
||||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||||
|
|||||||
@@ -10,14 +10,30 @@
|
|||||||
|
|
||||||
ADSmartPickup = {}
|
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
|
ADSmartPickup.dir = g_currentModDirectory
|
||||||
if ADSmartPickup.dir ~= nil and source ~= nil then
|
if ADSmartPickup.dir ~= nil and source ~= nil then
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
|
||||||
|
source(ADSmartPickup.dir .. "gui/SmartPickupStorageView.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.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
|
end
|
||||||
|
|
||||||
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
|
-- 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
|
end
|
||||||
|
|
||||||
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
|
-- 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)
|
function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||||
|
return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||||
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
|
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||||||
|
return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
|
||||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||||
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen".
|
-- 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
|
end
|
||||||
end
|
end
|
||||||
return best
|
return best, best ~= nil and bestDistance or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Siloens EGET indhold. Udvidelses-siloer (isExtension) deler lager med naboer
|
-- 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
|
-- 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
|
-- 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.
|
-- 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 prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes))
|
||||||
|
local ample = ADSupplyPlanner.ampleLevel(loadCapacity)
|
||||||
local best = {[true] = {}, [false] = {}}
|
local best = {[true] = {}, [false] = {}}
|
||||||
for _, entry in ipairs(markerStations) do
|
for _, entry in ipairs(markerStations) do
|
||||||
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
|
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
|
||||||
if level >= ADSmartPickup.MIN_USEFUL_LITERS and not isProtectedProductionInput(entry.station, fillTypes)
|
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}
|
best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn}
|
||||||
end
|
end
|
||||||
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
|
if choice.marker == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -392,7 +423,7 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)
|
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
|
end
|
||||||
|
|
||||||
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
|
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
|
||||||
@@ -452,7 +483,7 @@ end
|
|||||||
local factoryFallbackLogged = setmetatable({}, {__mode = "k"})
|
local factoryFallbackLogged = setmetatable({}, {__mode = "k"})
|
||||||
local function logFactoryFallbackOnce(driver, station)
|
local function logFactoryFallbackOnce(driver, station)
|
||||||
local placeable = station.owningPlaceable
|
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))
|
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
|
if factoryFallbackLogged[driver] ~= message then
|
||||||
factoryFallbackLogged[driver] = message
|
factoryFallbackLogged[driver] = message
|
||||||
@@ -639,6 +670,20 @@ function ADSmartPickup.setEnRoute(vehicle, wayPointId, fillType, liters)
|
|||||||
enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters}
|
enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters}
|
||||||
end
|
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)
|
function ADSmartPickup.clearEnRoute(vehicle)
|
||||||
enRoute[vehicle] = nil
|
enRoute[vehicle] = nil
|
||||||
end
|
end
|
||||||
@@ -658,6 +703,15 @@ function ADSmartPickup.getPending(wayPointId, excludeVehicle)
|
|||||||
return pending
|
return pending
|
||||||
end
|
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).
|
-- 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)
|
function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
||||||
local needs = getTargetNeeds(station, fillTypes, farmId, {})
|
local needs = getTargetNeeds(station, fillTypes, farmId, {})
|
||||||
@@ -676,7 +730,7 @@ function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den.
|
-- 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
|
if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
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)
|
-- 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
|
local bestFood = ADSupplyPlanner.bestFoodPriority ~= nil and ADSupplyPlanner.bestFoodPriority(ranked) or nil
|
||||||
for _, need in ipairs(ranked) do
|
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
|
if choice ~= nil then
|
||||||
return {
|
return {
|
||||||
markerIndex = choice.markerIndex,
|
markerIndex = choice.markerIndex,
|
||||||
@@ -1095,6 +1149,9 @@ local function installHook(adEnv)
|
|||||||
if ADOutbound ~= nil and ADOutbound.install ~= nil then
|
if ADOutbound ~= nil and ADOutbound.install ~= nil then
|
||||||
ADOutbound.install(adEnv)
|
ADOutbound.install(adEnv)
|
||||||
end
|
end
|
||||||
|
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
||||||
|
ADHitchCreep.install(adEnv)
|
||||||
|
end
|
||||||
|
|
||||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
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
|
end
|
||||||
@@ -1146,10 +1203,33 @@ local function insertInGameMenuPage(frame, pageName, iconFile, iconUVs, predicat
|
|||||||
end
|
end
|
||||||
inGameMenu[pageName] = frame
|
inGameMenu[pageName] = frame
|
||||||
inGameMenu.pagingElement:addElement(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:updateAbsolutePosition()
|
||||||
inGameMenu.pagingElement:updatePageMapping()
|
inGameMenu.pagingElement:updatePageMapping()
|
||||||
inGameMenu:registerPage(frame, position, predicate)
|
inGameMenu:registerPage(frame, position, predicate)
|
||||||
inGameMenu:addPageTab(frame, iconFile, iconUVs)
|
inGameMenu:addPageTab(frame, iconFile, iconUVs)
|
||||||
|
moveTo(inGameMenu.pageFrames, function(child) return child == frame end)
|
||||||
if frame.updateAbsolutePosition ~= nil then
|
if frame.updateAbsolutePosition ~= nil then
|
||||||
frame:updateAbsolutePosition()
|
frame:updateAbsolutePosition()
|
||||||
end
|
end
|
||||||
@@ -1184,11 +1264,17 @@ function ADSmartPickup:loadMap()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ADSmartPickup:update(dt)
|
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
|
if not isHooked then
|
||||||
local adEnv = getAutoDriveEnv()
|
local adEnv = getAutoDriveEnv()
|
||||||
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
|
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
|
||||||
installHook(adEnv)
|
installHook(adEnv)
|
||||||
isHooked = true
|
isHooked = true
|
||||||
|
if ADProfiler ~= nil then pcall(ADProfiler.install) end
|
||||||
|
if ADCache ~= nil then ADCache.enabled = true end
|
||||||
if ADRunsController ~= nil then
|
if ADRunsController ~= nil then
|
||||||
pcall(ADRunsController.load, adEnv)
|
pcall(ADRunsController.load, adEnv)
|
||||||
pcall(ADRunsController.installSaveHook)
|
pcall(ADRunsController.installSaveHook)
|
||||||
@@ -1206,6 +1292,9 @@ function ADSmartPickup:update(dt)
|
|||||||
if ADOutbound ~= nil and ADOutbound.releaseInactive ~= nil then
|
if ADOutbound ~= nil and ADOutbound.releaseInactive ~= nil then
|
||||||
pcall(ADOutbound.releaseInactive)
|
pcall(ADOutbound.releaseInactive)
|
||||||
end
|
end
|
||||||
|
if ADRunsController ~= nil and ADRunsController.processStartQueue ~= nil then
|
||||||
|
pcall(ADRunsController.processStartQueue, getAutoDriveEnv(), dt)
|
||||||
|
end
|
||||||
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
|
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
|
||||||
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
|
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
|
||||||
end
|
end
|
||||||
@@ -1215,11 +1304,47 @@ function ADSmartPickup:update(dt)
|
|||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
||||||
end
|
end
|
||||||
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)
|
local ok, err = pcall(restoreStoppedVehicles)
|
||||||
if not ok then
|
if not ok then
|
||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
||||||
supplyState = setmetatable({}, {__mode = "k"})
|
supplyState = setmetatable({}, {__mode = "k"})
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
addModEventListener(ADSmartPickup)
|
addModEventListener(ADSmartPickup)
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
|
|
||||||
ADSources = {}
|
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: "
|
ADSources.LOG_PREFIX = "ADSmartPickup: "
|
||||||
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
|
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
|
||||||
ADSources.MAX_DISTANCE = 40
|
ADSources.MAX_DISTANCE = 40
|
||||||
@@ -17,10 +24,6 @@ local function getHusbandryPalletSpawners(placeable)
|
|||||||
return spec ~= nil and spec.fillTypeIndexToPalletSpawner or nil
|
return spec ~= nil and spec.fillTypeIndexToPalletSpawner or nil
|
||||||
end
|
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 function distance2(ax, az, bx, bz)
|
||||||
local dx, dz = ax - bx, az - bz
|
local dx, dz = ax - bx, az - bz
|
||||||
@@ -36,6 +39,8 @@ local function getNodePosition(node)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function getPlaceableName(placeable, fallback)
|
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
|
if placeable ~= nil and placeable.getName ~= nil then
|
||||||
local ok, name = pcall(placeable.getName, placeable)
|
local ok, name = pcall(placeable.getName, placeable)
|
||||||
if ok and name ~= nil then
|
if ok and name ~= nil then
|
||||||
@@ -123,7 +128,8 @@ local function findPlaceableNear(wayPoint, farmId, predicate)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return best
|
-- (afstand i meter som 2. værdi)
|
||||||
|
return best, best ~= nil and math.sqrt(bestDistance) or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function isObjectStorage(placeable)
|
local function isObjectStorage(placeable)
|
||||||
@@ -136,6 +142,12 @@ end
|
|||||||
|
|
||||||
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
|
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
|
||||||
function ADSources.identify(adEnv, wayPointId, farmId)
|
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)
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||||||
if wayPoint == nil then
|
if wayPoint == nil then
|
||||||
return nil
|
return nil
|
||||||
@@ -164,9 +176,15 @@ function ADSources.identify(adEnv, wayPointId, farmId)
|
|||||||
end
|
end
|
||||||
-- Det strenge opslag (trigger bag næsen) passer til silorækker; en mælketank eller fabrik ligger
|
-- 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.
|
-- 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
|
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
|
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 loosePlaceable ~= nil and loosePlaceable.spec_silo == nil then
|
||||||
if getProductionPoint(loosePlaceable) ~= nil then
|
if getProductionPoint(loosePlaceable) ~= nil then
|
||||||
return {kind = "factory", placeable = loosePlaceable, station = loose, wayPointId = wayPointId, name = getPlaceableName(loosePlaceable, markerName)}
|
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
|
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
|
if palletBarn ~= nil then
|
||||||
return {kind = "husbandry", placeable = palletBarn, station = nil, wayPointId = wayPointId, name = getPlaceableName(palletBarn, markerName)}
|
return {kind = "husbandry", placeable = palletBarn, station = nil, wayPointId = wayPointId, name = getPlaceableName(palletBarn, markerName)}
|
||||||
end
|
end
|
||||||
@@ -241,29 +257,99 @@ local function getPalletContent(vehicle)
|
|||||||
return nil, 0
|
return nil, 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Liter af varen på paller der står ved fabrikkens spawner (pallespawneren tømmer lageret
|
-- Spawnerens område som segmenter {ax, az, bx, bz}: hver spawnPlace fra start til slut (startNode/endNode,
|
||||||
-- ud på jorden løbende, så det er dér varen fysisk er).
|
-- eller PlacementUtil-felterne startX/startZ + dirX/dirZ*width). Et hønsehus har to rækker; måles kun
|
||||||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawnerNode)
|
-- til første række, ligger den anden række tættere på naboens start end husets egen.
|
||||||
local productionPoint = getProductionPoint(placeable)
|
local function spawnerSegments(spawner)
|
||||||
local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil
|
local segments = {}
|
||||||
local node = spawnerNode or (spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode) or placeable.rootNode
|
for _, place in ipairs(spawner ~= nil and spawner.spawnPlaces or {}) do
|
||||||
local sx, sz = getNodePosition(node)
|
local ax, az = getNodePosition(place.startNode)
|
||||||
if sx == nil then
|
local bx, bz = getNodePosition(place.endNode)
|
||||||
return 0
|
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
|
||||||
|
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 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
|
local radius2 = ADSources.PALLET_RADIUS * ADSources.PALLET_RADIUS
|
||||||
for _, vehicle in pairs(vehicles) do
|
for _, vehicle in pairs(vehicles) do
|
||||||
if isPalletVehicle(vehicle) and (vehicle.getOwnerFarmId == nil or vehicle:getOwnerFarmId() == farmId) then
|
if isPalletVehicle(vehicle) and (vehicle.getOwnerFarmId == nil or vehicle:getOwnerFarmId() == farmId) then
|
||||||
local palletFillType, level = getPalletContent(vehicle)
|
local palletFillType, level = getPalletContent(vehicle)
|
||||||
local x, z = getNodePosition(vehicle.rootNode)
|
local x, z = getNodePosition(vehicle.rootNode)
|
||||||
if palletFillType == fillType and x ~= nil and distance2(sx, sz, x, z) <= radius2 then
|
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
|
total = total + level
|
||||||
count = count + 1
|
count = count + 1
|
||||||
|
else
|
||||||
|
lost = lost + 1
|
||||||
end
|
end
|
||||||
end
|
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
|
return total, count
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -395,7 +481,7 @@ local function getHusbandryPalletProducts(source, farmId)
|
|||||||
local products = {}
|
local products = {}
|
||||||
local spec = source.placeable.spec_husbandryPallets
|
local spec = source.placeable.spec_husbandryPallets
|
||||||
for fillType, spawner in pairs(getHusbandryPalletSpawners(source.placeable) or {}) do
|
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 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)
|
local product = newProduct(fillType, liters, liters, math.max(capacity or 0, liters), true)
|
||||||
product.palletLiters, product.palletCount = liters, count
|
product.palletLiters, product.palletCount = liters, count
|
||||||
@@ -552,6 +638,10 @@ end
|
|||||||
|
|
||||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||||||
function ADSources.getSellMarkers(adEnv, farmId)
|
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 = {}
|
local result = {}
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||||
@@ -852,16 +942,17 @@ local function addAnimalProduction(rates, placeable)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- {[fillType] = {consumption, production}} i liter pr. døgn for gårdens fabrikker og stalde.
|
-- {[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 placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||||
local rates = {}
|
local rates = {}
|
||||||
for _, placeable in pairs(placeables) do
|
for _, placeable in pairs(placeables) do
|
||||||
if isOwnedBy(placeable, farmId) then
|
if isOwnedBy(placeable, farmId) then
|
||||||
if getProductionPoint(placeable) ~= nil then
|
if getProductionPoint(placeable) ~= nil then
|
||||||
addFactoryRates(rates, placeable)
|
if not feedOnly then addFactoryRates(rates, placeable) end
|
||||||
elseif placeable.getHusbandryFillLevel ~= nil then
|
elseif placeable.getHusbandryFillLevel ~= nil then
|
||||||
addHusbandryRates(rates, placeable)
|
addHusbandryRates(rates, placeable)
|
||||||
pcall(addAnimalProduction, rates, placeable)
|
if not feedOnly then pcall(addAnimalProduction, rates, placeable) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -872,6 +963,31 @@ function ADSources.getFarmRates(adEnv, farmId)
|
|||||||
return rates
|
return rates
|
||||||
end
|
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):
|
-- 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.
|
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
||||||
function ADSources.getMarketQuote(fillType)
|
function ADSources.getMarketQuote(fillType)
|
||||||
@@ -999,6 +1115,10 @@ end
|
|||||||
-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
|
-- { {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.
|
-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
|
||||||
function ADSources.getFarmInventory(adEnv, farmId)
|
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 placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||||
local result = {}
|
local result = {}
|
||||||
for _, placeable in pairs(placeables) do
|
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
|
end
|
||||||
return result
|
return result
|
||||||
end
|
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,16 +29,33 @@ function ADSupplyPlanner.prefersFullestSource(fillTypeNames)
|
|||||||
return true
|
return true
|
||||||
end
|
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)?
|
-- 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
|
if bestLevel == nil then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if prefersFullest then
|
if prefersFullest then
|
||||||
return level > bestLevel
|
return level > bestLevel
|
||||||
end
|
end
|
||||||
|
if ample == nil then
|
||||||
return level < bestLevel
|
return level < bestLevel
|
||||||
end
|
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)
|
function ADSupplyPlanner.copyList(list)
|
||||||
local copy = {}
|
local copy = {}
|
||||||
@@ -345,3 +362,14 @@ function ADSupplyPlanner.parallelFoodShare(groups, fillLevels, capacity, fillTyp
|
|||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
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.
|
-- AD ser — vognens indhold, målets fri plads, AD's aflæsningstilstand og vognens dischargeState.
|
||||||
ADUnloadWait.DEBUG_INTERVAL_MS = 15000
|
ADUnloadWait.DEBUG_INTERVAL_MS = 15000
|
||||||
local debugTimers = setmetatable({}, {__mode = "k"})
|
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 function debugAtTarget(adEnv, task, dt)
|
||||||
local vehicle = task.vehicle
|
local vehicle = task.vehicle
|
||||||
local targetReached = vehicle.ad.drivePathModule:isTargetReached()
|
local targetReached = vehicle.ad.drivePathModule:isTargetReached()
|
||||||
@@ -268,8 +271,15 @@ local function debugAtTarget(adEnv, task, dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
|
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
|
-- ved målet MED plads, men læsset falder ikke (in-game 29/9: EDK 650 ved TMR Unload/Biogas Drop) — også det
|
||||||
if waits[vehicle] ~= nil or (free ~= nil and free >= ADUnloadWait.MIN_FREE_TO_UNLOAD) then
|
-- 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
|
return
|
||||||
end
|
end
|
||||||
local trailerModule = vehicle.ad.trailerModule
|
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
|
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle ~= nil and ADOutbound.isOutboundVehicle(task.vehicle) then
|
||||||
return
|
return
|
||||||
end
|
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)
|
pcall(debugAtTarget, adEnv, task, dt)
|
||||||
local wait = waits[task.vehicle]
|
local wait = waits[task.vehicle]
|
||||||
if wait ~= nil then
|
if wait ~= nil then
|
||||||
@@ -309,7 +325,7 @@ end
|
|||||||
-- stående for evigt. Her genoptages ventningen for aktive traktorer i et forsynings-run hvis
|
-- 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.
|
-- aflæsningsopgave peger på en Wait-markør: hjem = run'ets mål, og reservationen sættes igen.
|
||||||
-- Kaldes fra ADSmartPickup:update (throttlet).
|
-- Kaldes fra ADSmartPickup:update (throttlet).
|
||||||
ADUnloadWait.ADOPT_INTERVAL_MS = 5000
|
ADUnloadWait.ADOPT_INTERVAL_MS = 5300
|
||||||
local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS
|
local adoptTimerMs = ADUnloadWait.ADOPT_INTERVAL_MS
|
||||||
|
|
||||||
function ADUnloadWait.adoptOrphans(adEnv)
|
function ADUnloadWait.adoptOrphans(adEnv)
|
||||||
@@ -410,6 +426,9 @@ function ADUnloadWait.getClaims(adEnv)
|
|||||||
return ADWaitPool.getClaims(adEnv)
|
return ADWaitPool.getClaims(adEnv)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADUnloadWait.TIP_CHECK_MS = 2000
|
||||||
|
local tipChecked = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
function ADUnloadWait.install(adEnv)
|
function ADUnloadWait.install(adEnv)
|
||||||
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
||||||
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
|
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
|
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
|
||||||
|
|
||||||
task.update = function(self, dt)
|
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)
|
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
|
||||||
if not ok then
|
if not ok then
|
||||||
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
|
|
||||||
ADWaitPool = {}
|
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: "
|
ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
|
||||||
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
|
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
|
||||||
ADWaitPool.STATE_DRIVING = 2
|
ADWaitPool.STATE_DRIVING = 2
|
||||||
@@ -14,6 +21,10 @@ ADWaitPool.claims = setmetatable({}, {__mode = "v"})
|
|||||||
|
|
||||||
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
|
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
|
||||||
function ADWaitPool.getCandidates(adEnv, poolGroup)
|
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 = {}
|
local candidates = {}
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
|
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
|
-- 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).
|
-- 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 function otherVehiclePositions(vehicle)
|
||||||
local positions = {}
|
local positions = {}
|
||||||
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
|
||||||
local own = {}
|
local own = {}
|
||||||
if vehicle.getChildVehicles ~= nil then
|
if vehicle.getChildVehicles ~= nil then
|
||||||
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
||||||
for _, child in pairs(ok and children or {}) do own[child] = true end
|
for _, child in pairs(ok and children or {}) do own[child] = true end
|
||||||
end
|
end
|
||||||
own[vehicle] = true
|
own[vehicle] = true
|
||||||
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
for _, entry in ipairs(allVehiclePositions()) do
|
||||||
if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then
|
if not own[entry.vehicle] then table.insert(positions, entry) end
|
||||||
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
|
||||||
if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return positions
|
return positions
|
||||||
end
|
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)
|
local x, z = getVehiclePosition(vehicle)
|
||||||
|
if anchorX ~= nil and anchorZ ~= nil then x, z = anchorX, anchorZ end
|
||||||
if x == nil then
|
if x == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -124,11 +156,42 @@ local function graphReaches(adEnv, vehicle, targetId)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
-- Rutesvar gemmes ROUTE_CACHE_MS pr. mål og 10 m-område: holdende vogne spørger hvert 5. s for hver
|
||||||
local graph = adEnv.ADGraphManager
|
-- kandidat, og AD's stifinding (+ reserve-søgningen) gennem hele vejnettet gav et hak hvert 5. s
|
||||||
if graph.getPathTo == nil then
|
-- (in-game 26/9). Vejnettet ændrer sig ikke så hurtigt.
|
||||||
return true
|
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
|
||||||
|
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
|
if pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId) then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -142,13 +205,73 @@ function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
|||||||
return false
|
return false
|
||||||
end
|
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)
|
function ADWaitPool.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
|
||||||
local graph = adEnv.ADGraphManager
|
local graph = adEnv.ADGraphManager
|
||||||
if graph.getPathTo == nil or graph.pathFromTo == nil then
|
if graph.getPathTo == nil or graph.pathFromTo == nil then
|
||||||
return true
|
return true
|
||||||
end
|
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)
|
and pathReaches(graph:pathFromTo(toWayPointId, homeWayPointId), homeWayPointId)
|
||||||
|
if key ~= nil then remember(key, result, now) end
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Står AD midt i en (blokeret) af-/pålæsning, stoppes den pænt først — ellers kører
|
-- 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,115 @@
|
|||||||
|
-- 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 == "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å 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 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,16 +1,12 @@
|
|||||||
-- Smart Pickup — fanen Marker
|
-- Smart Pickup — fanen Marker
|
||||||
-- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række
|
-- Oversigt: tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en
|
||||||
-- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin,
|
-- række viser markens resumé (én linje pr. emne — klik på en linje for at rette netop det trin), hele
|
||||||
-- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben.
|
-- statusteksten og markøren. [Opsæt mark] åbner guiden (gui/SmartPickupFieldGuide.lua + SmartPickupGuide.lua),
|
||||||
-- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView.
|
-- [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).
|
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||||
|
|
||||||
SmartPickupFrame.FIELD_ROW_KINDS = {
|
-- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow.
|
||||||
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
|
SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true, fleetManual = true}
|
||||||
fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
|
|
||||||
fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
|
|
||||||
fieldTarget = true, fleetMode = true, fieldStraw = true,
|
|
||||||
}
|
|
||||||
SmartPickupFrame.ROLE_KEYS = {
|
SmartPickupFrame.ROLE_KEYS = {
|
||||||
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
|
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",
|
collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader",
|
||||||
@@ -19,15 +15,6 @@ SmartPickupFrame.ROLE_KEYS = {
|
|||||||
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
|
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
|
||||||
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
|
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
|
||||||
}
|
}
|
||||||
SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
|
||||||
-- simple Ja/Nej-valg på planen: rækketype -> plan-felt
|
|
||||||
SmartPickupFrame.PLAN_TOGGLES = {
|
|
||||||
fieldRake = "rake", fieldMulch = "mulch", fieldLime = "lime", fieldSow = "sow", fieldRoll = "roll",
|
|
||||||
fieldFertilize = "fertilize", fieldWeed = "weed",
|
|
||||||
}
|
|
||||||
-- valg med flere muligheder: rækketype -> plan-felt
|
|
||||||
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit", fieldStraw = "straw"}
|
|
||||||
SmartPickupFrame.STRAW_VALUES = {"chop", "collect", "bale"}
|
|
||||||
|
|
||||||
-- ---- tekster ----
|
-- ---- tekster ----
|
||||||
|
|
||||||
@@ -53,11 +40,32 @@ function SmartPickupFrame:soilText(info)
|
|||||||
return table.concat(texts, ", ")
|
return table.concat(texts, ", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:fieldStatusText(field, info)
|
-- Kort årsag til tabellens statuskolonne ("bredde ukendt" i stedet for hele forklaringen, som står i
|
||||||
|
-- markens detaljer). Fritekst-årsager (fx "… er tom for X") forkortes til "se detaljer".
|
||||||
|
function SmartPickupFrame:fieldReasonShort(reason)
|
||||||
|
if type(reason) ~= "table" then return nil end
|
||||||
|
if reason.text ~= nil then return self.i18n:getText("spu_field_reasonShort_text") end
|
||||||
|
local shortKey = reason.key ~= nil and string.gsub(reason.key, "^spu_field_reason_", "spu_field_reasonShort_") or nil
|
||||||
|
if shortKey ~= nil then
|
||||||
|
local okHas, has = pcall(function() return self.i18n.hasText == nil or self.i18n:hasText(shortKey) end)
|
||||||
|
if okHas and has then return self.i18n:getText(shortKey) end
|
||||||
|
end
|
||||||
|
return self:fieldReasonText(reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- short = tabellens korte udgave (trin + kort årsag); ellers hele teksten (markens detaljer).
|
||||||
|
function SmartPickupFrame:fieldStatusText(field, info, short)
|
||||||
local text = self.i18n:getText(ADFieldView.statusKey(field, info))
|
local text = self.i18n:getText(ADFieldView.statusKey(field, info))
|
||||||
if info.markerId == nil then
|
if info.markerId == nil then
|
||||||
return string.format(text, info.id)
|
return string.format(text, info.id)
|
||||||
end
|
end
|
||||||
|
if short then
|
||||||
|
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||||
|
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||||
|
end
|
||||||
|
local reason = self:fieldReasonShort(field.reason)
|
||||||
|
return reason ~= nil and (text .. ": " .. reason) or text
|
||||||
|
end
|
||||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
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))
|
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||||
end
|
end
|
||||||
@@ -74,18 +82,6 @@ function SmartPickupFrame:fieldStatusText(field, info)
|
|||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Hvad moden gør med marken frem mod mål-afgrøden (én linje under valget).
|
|
||||||
function SmartPickupFrame:targetSummary(field, info)
|
|
||||||
local target = self:fruitTitle(field.plan.targetFruit)
|
|
||||||
if info.kind == "bare" then
|
|
||||||
return string.format(self.i18n:getText("spu_field_target_bare"), target)
|
|
||||||
end
|
|
||||||
if info.fruitName == field.plan.targetFruit and info.kind == "mowable" then
|
|
||||||
return string.format(self.i18n:getText("spu_field_target_keep"), target)
|
|
||||||
end
|
|
||||||
return string.format(self.i18n:getText("spu_field_target_switch"), tostring(info.cropTitle or "-"), target)
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:fruitTitle(name)
|
function SmartPickupFrame:fruitTitle(name)
|
||||||
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
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)
|
return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
|
||||||
@@ -108,209 +104,135 @@ function SmartPickupFrame:fieldCells(row)
|
|||||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||||
end
|
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 = info.kind == "bare" and self.i18n:getText("spu_field_bare") or (info.cropTitle or "-")
|
local crop = ADFieldView.cropText(info, function(key) return self.i18n:getText(key) end)
|
||||||
return {
|
return {
|
||||||
name, crop, self:fieldPlanText(field, info),
|
name, crop, self:fieldPlanText(field, info),
|
||||||
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
||||||
self:fieldStatusText(field, info),
|
self:fieldStatusText(field, info, true) .. ((ADFieldJobs.state.paused and not ADFieldJobs.isFieldResumed(info.id)
|
||||||
|
and ADFieldModel.isWorking(field)) and (" " .. self.i18n:getText("spu_field_pausedSuffix")) or ""),
|
||||||
ADFlowView.joinCounted(names, ", ", "-"),
|
ADFlowView.joinCounted(names, ", ", "-"),
|
||||||
|
self:fieldActionText(info.id),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- rækker ----
|
|
||||||
|
|
||||||
-- Valg med flere muligheder (klon af productPrefab): values[i] er værdien for texts[i].
|
|
||||||
function SmartPickupFrame:addChoiceRow(kind, title, texts, values, current)
|
|
||||||
local row = self:addRow(self.productPrefab, {kind = kind, values = values})
|
|
||||||
local option = row:getDescendantByName("option")
|
|
||||||
option:setTexts(texts)
|
|
||||||
local index = 1
|
|
||||||
for position, value in ipairs(values) do
|
|
||||||
if value == current then index = position end
|
|
||||||
end
|
|
||||||
option:setState(index, false)
|
|
||||||
row:getDescendantByName("title"):setText(title)
|
|
||||||
return option
|
|
||||||
end
|
|
||||||
|
|
||||||
local function numberTexts(from, to)
|
|
||||||
local texts, values = {}, {}
|
|
||||||
for value = from, to do
|
|
||||||
table.insert(texts, tostring(value))
|
|
||||||
table.insert(values, value)
|
|
||||||
end
|
|
||||||
return texts, values
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Afgrøder der kan sås (første valg: samme som den høstede / ingen mål-afgrøde).
|
|
||||||
function SmartPickupFrame:sowFruitChoices(firstKey)
|
|
||||||
local texts, values = {self.i18n:getText(firstKey or "spu_field_sowSame")}, {""}
|
|
||||||
for _, fruit in ipairs(g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}) do
|
|
||||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
|
||||||
table.insert(texts, tostring(fruit.fillType.title or fruit.name))
|
|
||||||
table.insert(values, fruit.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return texts, values
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ---- opbygning ----
|
-- ---- opbygning ----
|
||||||
|
|
||||||
function SmartPickupFrame:fillFieldRows(adEnv)
|
function SmartPickupFrame:fillFieldRows(adEnv)
|
||||||
self.fieldRows = {}
|
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 i18n = self.i18n
|
||||||
local infos = ADFields.list()
|
local infos = ADFields.list()
|
||||||
if #infos == 0 then
|
if #infos == 0 then
|
||||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||||
else
|
else
|
||||||
self:addTextRow(i18n:getText("spu_field_hint"))
|
self:addTextRow(i18n:getText("spu_field_hint"))
|
||||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors")})
|
-- Stop alt-pausen gemmes med savegamet: sig det tydeligt her (in-game 29/9: "jeg har ikke trykket stop")
|
||||||
|
if ADFieldJobs.state.paused then self:addTextRow(i18n:getText("spu_field_pausedBanner")) end
|
||||||
|
-- markflåden på "kun med flueben" uden flueben: intet kan sendes ud (in-game 26/9 — alle marker "no machine")
|
||||||
|
if ADFieldJobs.state.fleetMode == "ticked" and next(ADFieldJobs.state.fleet or {}) == nil then
|
||||||
|
self:addTextRow(i18n:getText("spu_field_fleetEmpty"))
|
||||||
|
end
|
||||||
|
self:addCellRow(self.fieldListPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors"), ""})
|
||||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields)) do
|
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields)) do
|
||||||
local cells = self:addCellRow(self.fieldPrefab, self:fieldCells(row))
|
local cells = self:addCellRow(self.fieldListPrefab, self:fieldCells(row))
|
||||||
local fieldId = row.info.id
|
local fieldId = row.info.id
|
||||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||||
|
local actionCell = cells[7]
|
||||||
self:makeRowClickable(cells[1].parent, function(frame)
|
self:makeRowClickable(cells[1].parent, function(frame)
|
||||||
|
-- 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.selectedFieldId = fieldId
|
||||||
frame:requestRebuild()
|
frame:openEditPanel("field", fieldId)
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
table.insert(self.fieldRows, {id = fieldId, cells = cells})
|
table.insert(self.fieldRows, {id = fieldId, cells = cells})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
self:refreshFieldLevels()
|
||||||
if selected ~= nil then
|
|
||||||
self:fillFieldDetail(adEnv, selected)
|
|
||||||
end
|
|
||||||
self:fillFleetRows(adEnv)
|
|
||||||
end
|
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 i18n = self.i18n
|
||||||
local field = ADFieldJobs.getField(info.id)
|
local field = ADFieldJobs.getField(info.id)
|
||||||
local plan = field.plan
|
local crop = ADFieldView.cropText(info, function(key) return i18n:getText(key) end)
|
||||||
local yesNo = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
if not self.editPanelOpen then
|
||||||
local crop = info.kind == "bare" and i18n:getText("spu_field_bare") or tostring(info.cropTitle or "-")
|
|
||||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||||
local targetTexts, targetValues = self:sowFruitChoices("spu_field_target_none")
|
|
||||||
self:addChoiceRow("fieldTarget", i18n:getText("spu_field_target"), targetTexts, targetValues, plan.targetFruit or "")
|
|
||||||
local hasTarget = plan.targetFruit ~= nil and plan.targetFruit ~= ""
|
|
||||||
if hasTarget then
|
|
||||||
self:addTextRow(" " .. self:targetSummary(field, info))
|
|
||||||
else
|
|
||||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
|
|
||||||
end
|
end
|
||||||
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
|
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||||
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
|
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
|
||||||
-- græs-valgene (ensilage/hø, riv) også når målet er en slåafgrøde (fx græs på en kornmark)
|
local guide = ADFieldGuide.open(field, context)
|
||||||
local targetMowable = false
|
local getText = function(key) return i18n:getText(key) end
|
||||||
if hasTarget and ADFields ~= nil and g_fruitTypeManager ~= nil then
|
local fieldId = info.id
|
||||||
local targetFruit = g_fruitTypeManager:getFruitTypeByName(plan.targetFruit)
|
for _, line in ipairs(ADFieldGuide.summary(guide)) do
|
||||||
targetMowable = targetFruit ~= nil and ADFields.getMowable()[targetFruit.index] ~= nil
|
local step = line.step
|
||||||
|
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||||
|
frame:openFieldStep(fieldId, step)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
if info.kind == "mowable" or targetMowable then
|
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), self:fieldStatusText(field, info)))
|
||||||
local isHay = ADFieldModel.product(plan, "mowable") == "hay"
|
|
||||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
|
||||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
|
|
||||||
end
|
|
||||||
-- klargøring: høst-/bare marker, og græsmarker der skal skifte til en anden mål-afgrøde
|
|
||||||
if info.kind ~= "mowable" or (hasTarget and info.fruitName ~= plan.targetFruit) then
|
|
||||||
if info.kind == "harvest" then
|
|
||||||
local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
|
|
||||||
self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
|
|
||||||
local strawTexts = {}
|
|
||||||
for _, value in ipairs(SmartPickupFrame.STRAW_VALUES) do table.insert(strawTexts, i18n:getText("spu_field_straw_" .. value)) end
|
|
||||||
self:addChoiceRow("fieldStraw", i18n:getText("spu_field_straw"), strawTexts, SmartPickupFrame.STRAW_VALUES, plan.straw or "chop")
|
|
||||||
end
|
|
||||||
self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
|
|
||||||
self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
|
|
||||||
local soilTexts = {}
|
|
||||||
for _, value in ipairs(SmartPickupFrame.SOIL_VALUES) do table.insert(soilTexts, i18n:getText("spu_field_soil_" .. value)) end
|
|
||||||
self:addChoiceRow("fieldSoil", i18n:getText("spu_field_soil"), soilTexts, SmartPickupFrame.SOIL_VALUES, plan.soil or "auto")
|
|
||||||
if not hasTarget then
|
|
||||||
self:addToggleRow("fieldSow", i18n:getText("spu_field_sow"), yesNo, plan.sow == true)
|
|
||||||
if plan.sow then
|
|
||||||
local fruitTexts, fruitValues = self:sowFruitChoices()
|
|
||||||
self:addChoiceRow("fieldSowFruit", i18n:getText("spu_field_sowFruit"), fruitTexts, fruitValues, plan.sowFruit or "")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if hasTarget or plan.sow then
|
|
||||||
self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self:addTextRow(i18n:getText("spu_field_section_care"))
|
|
||||||
self:addToggleRow("fieldFertilize", i18n:getText("spu_field_fertilize"), yesNo, plan.fertilize ~= false)
|
|
||||||
self:addToggleRow("fieldLime", i18n:getText("spu_field_lime"), yesNo, plan.lime ~= false)
|
|
||||||
if info.kind ~= "mowable" then
|
|
||||||
self:addToggleRow("fieldWeed", i18n:getText("spu_field_weed"), yesNo, plan.weed ~= false)
|
|
||||||
end
|
|
||||||
local soil = self:soilText(info)
|
|
||||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_soilState"), soil ~= "" and soil or i18n:getText("spu_field_soilOk")))
|
|
||||||
if info.markerId ~= nil then
|
if info.markerId ~= nil then
|
||||||
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
|
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
|
||||||
else
|
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
|
|
||||||
if field.plan.rake and field.windrowOffset ~= nil then
|
|
||||||
self:addTextRow(string.format(i18n:getText("spu_field_windrowOffset"), math.abs(field.windrowOffset),
|
|
||||||
i18n:getText(math.abs(field.windrowOffset) <= 0.05 and "spu_field_side_center" or (field.windrowOffset > 0 and "spu_field_side_left" or "spu_field_side_right"))))
|
|
||||||
end
|
|
||||||
self:fillFieldStoreRows(adEnv, info, field)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer.
|
|
||||||
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
|
||||||
local i18n = self.i18n
|
|
||||||
local product = ADFieldModel.product(field.plan, info.kind)
|
|
||||||
local fillType
|
|
||||||
if info.kind == "mowable" then
|
|
||||||
fillType = product == "hay" and info.hay or info.windrow
|
|
||||||
else
|
|
||||||
fillType = info.harvestFillType
|
|
||||||
end
|
|
||||||
if fillType == nil then return end
|
|
||||||
self:addTextRow(i18n:getText("spu_field_section_store"))
|
|
||||||
local chosen = {}
|
|
||||||
for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end
|
|
||||||
local farmId = g_currentMission:getFarmId()
|
|
||||||
local fieldSilos = {}
|
|
||||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do fieldSilos[building.id] = building end
|
|
||||||
local fermenterIds = {}
|
|
||||||
if info.kind == "mowable" and product == "silage" then
|
|
||||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
|
||||||
end
|
|
||||||
local count = 0
|
|
||||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
|
||||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
|
||||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
|
||||||
count = count + 1
|
|
||||||
local row = self:addRow(self.ingredientPrefab, {kind = "fieldStore", buildingId = building.id})
|
|
||||||
local suffix = fieldSilos[building.id] ~= nil and (" (" .. i18n:getText("spu_field_fieldSilo") .. ")") or ""
|
|
||||||
if fermenterIds[building.id] then suffix = suffix .. " (" .. i18n:getText("spu_field_fermenter") .. ")" end
|
|
||||||
row:getDescendantByName("title"):setText(" " .. tostring(building.name) .. suffix)
|
|
||||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if count == 0 then
|
|
||||||
self:addTextRow(" " .. i18n:getText("spu_field_noStore"))
|
|
||||||
elseif #field.plan.storeBuildingIds == 0 then
|
|
||||||
self:addTextRow(" " .. i18n:getText((info.kind == "mowable" and product == "silage") and "spu_field_storeAutoSilage" or "spu_field_storeAuto"))
|
|
||||||
end
|
|
||||||
-- marksiloer skal tømmes hjem af et udkørselsflow
|
|
||||||
for id, building in pairs(fieldSilos) do
|
|
||||||
local emptied = false
|
|
||||||
for _, run in ipairs(ADRunsController.store.runs) do
|
|
||||||
for _, sourceId in ipairs(run.kind == "outbound" and run.sourceBuildingIds or {}) do
|
|
||||||
if sourceId == id then emptied = true end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not emptied then
|
|
||||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_fieldSiloNotEmptied"), tostring(building.name)))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
-- marksilo-advarsler vises ikke her: de gælder ikke marken, og Field Silos-fanen tømmer marksiloerne selv
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
||||||
@@ -326,18 +248,28 @@ function SmartPickupFrame:rigDetails(rig)
|
|||||||
elseif rig.role == "weeder" then
|
elseif rig.role == "weeder" then
|
||||||
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
||||||
end
|
end
|
||||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
-- samme grænse som udsendelsen (50,0 m med afrunding er ikke "over 50 m")
|
||||||
|
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||||
|
if (rig.width or 0) < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[rig.role] then table.insert(parts, i18n:getText("spu_field_widthUnknownShort")) end
|
||||||
|
-- fast på en mark (valgt under markens "Faste maskiner") og hvor den arbejder nu
|
||||||
|
for fieldId, field in pairs(ADFieldJobs.state.fields) do
|
||||||
|
for _, id in ipairs(field.plan ~= nil and field.plan.rigIds or {}) do
|
||||||
|
if id == rig.id then table.insert(parts, string.format(i18n:getText("spu_field_dedicatedElsewhere"), tostring(fieldId))) end
|
||||||
|
end
|
||||||
|
end
|
||||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||||
if assignment ~= nil then table.insert(parts, string.format("F%d", assignment.fieldId)) end
|
if assignment ~= nil then table.insert(parts, string.format(i18n:getText("spu_field_rig_working"), tostring(assignment.fieldId))) end
|
||||||
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
|
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Markflåden (egen visning): alle maskiner med flueben, "fast på F22", "i gang på F108", bredde-advarsler.
|
||||||
function SmartPickupFrame:fillFleetRows(adEnv)
|
function SmartPickupFrame:fillFleetRows(adEnv)
|
||||||
local i18n = self.i18n
|
local i18n = self.i18n
|
||||||
self:addTextRow(i18n:getText("spu_field_section_fleet"))
|
self:addTextRow(i18n:getText("spu_field_section_fleet"))
|
||||||
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
|
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
|
||||||
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
|
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
|
||||||
local rigs = ADFieldJobs.getRigs(adEnv)
|
local rigs = {}
|
||||||
|
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do table.insert(rigs, rig) end
|
||||||
table.sort(rigs, function(a, b)
|
table.sort(rigs, function(a, b)
|
||||||
if a.role ~= b.role then return a.role < b.role end
|
if a.role ~= b.role then return a.role < b.role end
|
||||||
return ADFlowView.naturalLess(a.label, b.label)
|
return ADFlowView.naturalLess(a.label, b.label)
|
||||||
@@ -347,15 +279,55 @@ function SmartPickupFrame:fillFleetRows(adEnv)
|
|||||||
local title = string.format(" %s — %s, %.1f m%s", tostring(rig.label), self:roleText(rig.role), rig.width or 0, self:rigDetails(rig))
|
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("title"):setText(title)
|
||||||
row:getDescendantByName("option"):setState(rig.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
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
|
end
|
||||||
if #rigs == 0 then
|
if #rigs == 0 then
|
||||||
self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
|
self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
|
||||||
end
|
end
|
||||||
|
if ADPool ~= nil and ADPool.overview ~= nil then self:fillPoolRows(adEnv) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Maskinpuljen: solo-traktorer (flueben som resten af flåden) og redskaber med status og hvorfor de ikke er med.
|
||||||
|
function SmartPickupFrame:fillPoolRows(adEnv)
|
||||||
|
local i18n = self.i18n
|
||||||
|
local ok, implements, tractors = pcall(ADPool.overview, adEnv)
|
||||||
|
if not ok then return end
|
||||||
|
local byLabel = function(a, b) return ADFlowView.naturalLess(a.label, b.label) end
|
||||||
|
table.sort(tractors, byLabel)
|
||||||
|
table.sort(implements, function(a, b)
|
||||||
|
if a.role ~= b.role then return a.role < b.role end
|
||||||
|
return byLabel(a, b)
|
||||||
|
end)
|
||||||
|
self:addTextRow(i18n:getText("spu_pool_section_tractors"))
|
||||||
|
for _, tractor in ipairs(tractors) do
|
||||||
|
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
||||||
|
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg))
|
||||||
|
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||||
|
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", tostring(tractor.label), hp, status))
|
||||||
|
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||||
|
end
|
||||||
|
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
||||||
|
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
||||||
|
for _, implement in ipairs(implements) do
|
||||||
|
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg))
|
||||||
|
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status))
|
||||||
|
end
|
||||||
|
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- live ----
|
-- ---- live ----
|
||||||
|
|
||||||
function SmartPickupFrame:refreshFieldRows(adEnv)
|
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 = {}
|
local rowsById = {}
|
||||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) 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
|
local working, total = 0, 0
|
||||||
@@ -364,47 +336,32 @@ function SmartPickupFrame:refreshFieldRows(adEnv)
|
|||||||
if row ~= nil then
|
if row ~= nil then
|
||||||
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
||||||
total = total + 1
|
total = total + 1
|
||||||
if ADFieldModel.isWorking(row.field) then working = working + 1 end
|
if self:fieldIsActive(entry.id, row.field) then working = working + 1 end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
self:refreshFieldLevels()
|
||||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_field_status"), working, total))
|
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 selected = self.selectedFieldId ~= nil and ADFieldJobs.getField(self.selectedFieldId) or nil
|
||||||
local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||||
local isWorking = selected ~= nil and ADFieldModel.isWorking(selected)
|
local isWorking = selected ~= nil and ADFieldModel.isWorking(selected)
|
||||||
self.fieldStartButtonInfo.text = self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start")
|
local awaiting = self.selectedFieldId ~= nil and ADFieldJobs.awaitingPlayer(self.selectedFieldId) ~= nil
|
||||||
|
self.fieldStartButtonInfo.text = self.i18n:getText(awaiting and "spu_field_button_takeover"
|
||||||
|
or (isWorking and "spu_field_button_stop" or "spu_field_button_start"))
|
||||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
|
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- klik ----
|
-- ---- klik (markflåden) ----
|
||||||
|
|
||||||
function SmartPickupFrame:onClickFieldRow(state, data)
|
function SmartPickupFrame:onClickFieldRow(state, data)
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
local isOn = state == SmartPickupFrame.STATE_ON
|
local isOn = state == SmartPickupFrame.STATE_ON
|
||||||
local id = frame.selectedFieldId
|
|
||||||
if data.kind == "fleetVehicle" then
|
if data.kind == "fleetVehicle" then
|
||||||
ADFieldJobs.toggleFleet(data.vehicleId)
|
ADFieldJobs.toggleFleet(data.vehicleId)
|
||||||
elseif data.kind == "fleetMode" then
|
elseif data.kind == "fleetMode" then
|
||||||
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
|
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
|
||||||
elseif id == nil then
|
elseif data.kind == "fleetManual" then
|
||||||
return
|
local wantManual = not isOn -- Ja = kører selv på vejen
|
||||||
elseif data.kind == "fieldTarget" then
|
if ((ADFieldJobs.state.manual or {})[data.vehicleId] == true) ~= wantManual then ADFieldJobs.toggleManual(data.vehicleId) end
|
||||||
ADFieldJobs.setTarget(id, data.values ~= nil and data.values[state] or "")
|
|
||||||
elseif SmartPickupFrame.PLAN_CHOICES[data.kind] ~= nil then
|
|
||||||
local value = data.values ~= nil and data.values[state] or nil
|
|
||||||
if value ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_CHOICES[data.kind]] = value}) end
|
|
||||||
elseif data.kind == "fieldProduct" then
|
|
||||||
ADFieldJobs.setPlan(id, {product = isOn and "hay" or "silage"})
|
|
||||||
elseif data.kind == "fieldMode" then
|
|
||||||
ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"})
|
|
||||||
elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then
|
|
||||||
ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn})
|
|
||||||
elseif data.kind == "fieldStore" then
|
|
||||||
local ids = {}
|
|
||||||
for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do
|
|
||||||
if buildingId ~= data.buildingId then table.insert(ids, buildingId) end
|
|
||||||
end
|
|
||||||
if isOn then table.insert(ids, data.buildingId) end
|
|
||||||
ADFieldJobs.setPlan(id, {storeBuildingIds = ids})
|
|
||||||
end
|
end
|
||||||
frame:requestRebuild()
|
frame:requestRebuild()
|
||||||
end)
|
end)
|
||||||
@@ -412,13 +369,29 @@ end
|
|||||||
|
|
||||||
-- Start nu: pleje hvis marken trænger (og vokser), ellers hele kæden (høst/slå, eller klargøring på bar mark).
|
-- 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()
|
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)
|
self:safe(function(frame)
|
||||||
local id = frame.selectedFieldId
|
if id == nil or not frame:fieldCanStart(id) then return end
|
||||||
if id == nil then return end
|
|
||||||
local field = ADFieldJobs.getField(id)
|
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.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
|
else
|
||||||
|
if ADFieldJobs.state.paused then ADFieldJobs.setFieldResumed(id, true) end
|
||||||
local decision = ADFieldJobs.lastDecision[id]
|
local decision = ADFieldJobs.lastDecision[id]
|
||||||
if decision == nil or decision.care == nil then
|
if decision == nil or decision.care == nil then
|
||||||
ADFieldJobs.forceReady(id)
|
ADFieldJobs.forceReady(id)
|
||||||
@@ -430,3 +403,25 @@ function SmartPickupFrame:onClickFieldStartStop()
|
|||||||
frame:requestRebuild()
|
frame:requestRebuild()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Redigeringsvinduet for en mark (gui/SmartPickupEditPanel.lua).
|
||||||
|
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||||
|
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
||||||
|
canDelete = false,
|
||||||
|
exists = function(frame, id) return ADFields.getInfo(id) ~= nil and ADFieldJobs.getField(id) ~= nil end,
|
||||||
|
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
|
||||||
|
fill = function(frame, adEnv, id)
|
||||||
|
frame:fillFieldOverview(adEnv, ADFields.getInfo(id))
|
||||||
|
frame:addLinkRow(" " .. frame.i18n:getText("spu_guide_field_open"), function(f)
|
||||||
|
f.selectedFieldId = id
|
||||||
|
f:onClickFieldSetup()
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
status = function(frame, adEnv, id)
|
||||||
|
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), true), level = frame:fieldLevel(id)}
|
||||||
|
end,
|
||||||
|
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id, true) end,
|
||||||
|
startStop = function(frame, id) frame:toggleField(id) end,
|
||||||
|
canStart = function(frame, id) return frame:fieldCanStart(id) end,
|
||||||
|
delete = function() end,
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
-- Smart Pickup — fanen Flows: tavle + resumé, og guiden "Opsæt flow"
|
||||||
|
-- Tavlen øverst (én række pr. flow), det valgte flows resumé (klik på en linje = ret ét trin; Navn = omdøb)
|
||||||
|
-- og status. [Opsæt flow] / [Nyt flow] åbner guiden på den fælles trin-skærm (gui/SmartPickupGuide.lua).
|
||||||
|
-- Henter det spil-afhængige som `context` til ADFlowGuide og gemmer via ADRunsController.
|
||||||
|
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||||
|
|
||||||
|
function SmartPickupFrame:initFlowButtons(i18n)
|
||||||
|
self.flowSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_flow_open"), callback = function() self:onClickFlowSetup() end}
|
||||||
|
self.flowNewButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_new"), callback = function() self:onClickFlowNew() end}
|
||||||
|
-- Flows-fanen: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; omdøb ligger på resuméets Navn-linje
|
||||||
|
-- v1.58: listen har Start/Stop pr. række; Opsæt/Slet ligger i redigeringsvinduet
|
||||||
|
self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- traktor-tekst (også til guidens traktorliste) ----
|
||||||
|
|
||||||
|
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — OK" / "… — X ingen vogn".
|
||||||
|
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
|
||||||
|
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||||
|
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
||||||
|
local parts = {label}
|
||||||
|
if validation.isAutoload and validation.capacity == nil then
|
||||||
|
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
|
||||||
|
elseif validation.capacity ~= nil then
|
||||||
|
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
|
||||||
|
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
|
||||||
|
if carriable ~= nil then
|
||||||
|
table.insert(parts, carriable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if validation.ok then
|
||||||
|
table.insert(parts, "OK")
|
||||||
|
else
|
||||||
|
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
|
||||||
|
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
|
||||||
|
reason = string.format(reason, tostring(validation.missingBuilding))
|
||||||
|
end
|
||||||
|
table.insert(parts, "X " .. reason)
|
||||||
|
end
|
||||||
|
return table.concat(parts, " — ")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
|
||||||
|
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
|
||||||
|
local canCarry = {}
|
||||||
|
for _, fillType in ipairs(carriable or {}) do
|
||||||
|
canCarry[fillType] = true
|
||||||
|
end
|
||||||
|
local titles = {}
|
||||||
|
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
|
||||||
|
if canCarry[fillType] then
|
||||||
|
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||||||
|
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #titles == 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- context til ADFlowGuide ----
|
||||||
|
|
||||||
|
local function trim(text)
|
||||||
|
return (string.gsub(tostring(text or ""), "^%s+", ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function idSet(list)
|
||||||
|
local set = {}
|
||||||
|
for _, id in ipairs(list or {}) do set[id] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:flowBuildingContext(adEnv, draft, farmId)
|
||||||
|
local isOut = draft.kind == "outbound"
|
||||||
|
local role = isOut and "load" or "unload"
|
||||||
|
local chosen = idSet(isOut and draft.sourceBuildingIds or draft.targetBuildingIds)
|
||||||
|
local buildings, found = {}, {}
|
||||||
|
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||||
|
local fits = (isOut and building.canBeSource) or (not isOut and building.canBeTarget)
|
||||||
|
if fits or chosen[building.id] then
|
||||||
|
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}
|
||||||
|
found[building.id] = true
|
||||||
|
table.insert(buildings, {id = building.id, name = tostring(building.name), label = markers[1] ~= nil and tostring(markers[1].name) or nil,
|
||||||
|
hasMarker = markers[1] ~= nil, text = trim(self:describeBuilding(building, role))})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- valgte bygninger der ikke længere findes (solgt): kan fravælges
|
||||||
|
for id in pairs(chosen) do
|
||||||
|
if not found[id] then
|
||||||
|
local gone = self.i18n:getText("spu_buildingGone")
|
||||||
|
table.insert(buildings, {id = id, name = gone, hasMarker = false, text = gone})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return buildings
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:flowStoreContext(adEnv, preview, farmId)
|
||||||
|
local goods = ADRunsController.getStoreGoods(adEnv, preview)
|
||||||
|
local chosen = idSet(preview.storeBuildingIds)
|
||||||
|
local result = {}
|
||||||
|
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||||
|
local isStoreKind = building.kind == "silo" or building.kind == "factory" or building.kind == "objectStorage"
|
||||||
|
if isStoreKind and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, goods)) then
|
||||||
|
table.insert(result, {id = building.id, name = tostring(building.name), text = trim(self:describeBuilding(building, "unload"))})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Varer (fillType-indeks) flowet kører med: udkørsel = valgte produkter, forsyning = valgte/målets varer.
|
||||||
|
-- Tom liste = ukendt endnu (ingen mål valgt) -> enhver vogn med last-enheder tæller.
|
||||||
|
local function flowFillTypes(adEnv, preview)
|
||||||
|
local names = {}
|
||||||
|
if preview.kind == "outbound" then
|
||||||
|
for _, product in ipairs(preview.products or {}) do table.insert(names, product.fillType) end
|
||||||
|
elseif preview.ingredientMode == "manual" then
|
||||||
|
names = preview.ingredients or {}
|
||||||
|
else
|
||||||
|
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do table.insert(names, fillType.name) end
|
||||||
|
end
|
||||||
|
local result = {}
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
local index = g_fillTypeManager:getFillTypeIndexByName(tostring(name))
|
||||||
|
if index ~= nil then table.insert(result, index) end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function carriesAny(carriable, wanted)
|
||||||
|
if #carriable == 0 then return false end
|
||||||
|
if #wanted == 0 then return true end
|
||||||
|
for _, a in ipairs(carriable) do
|
||||||
|
for _, b in ipairs(wanted) do if a == b then return true end end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ledige vogne til solo-traktorer: frakoblet, egen AD-parkering med "Connect"-markør, ikke holdt af et andet flow
|
||||||
|
-- og kan bære flowets varer. Bedste først (navnematch, så "Disponibel"). In-game 29/9: flowet kunne ikke oprettes
|
||||||
|
-- uden at omdøbe en vogns parkering til flowets navn.
|
||||||
|
function SmartPickupFrame:flowWagonContext(adEnv, preview)
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||||
|
local wanted = flowFillTypes(adEnv, preview)
|
||||||
|
local candidates = {}
|
||||||
|
for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do
|
||||||
|
local ok, entry = pcall(function()
|
||||||
|
local index = any.advd ~= nil and any.advd.parkDestination or nil
|
||||||
|
if any.spec_motorized ~= nil or any.spec_attachable == nil or type(index) ~= "number" or index < 1 then return nil end
|
||||||
|
if any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil then return nil end
|
||||||
|
if any.getOwnerFarmId == nil or any:getOwnerFarmId() ~= farmId then return nil end
|
||||||
|
local id = ADRunsController.getVehicleId(any)
|
||||||
|
local park = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
|
if id == nil or park == nil or ADPool.connectFor(park.name, markers) == nil then return nil end
|
||||||
|
local owner = ADFlowWagons.ownerOf(ADRunsController.store.runs, id)
|
||||||
|
if owner ~= nil and owner.id ~= preview.id then return nil end
|
||||||
|
if not carriesAny(ADRunsController.getCarriableFillTypes(adEnv, any), wanted) then return nil end
|
||||||
|
return {id = id, label = tostring(park.name)}
|
||||||
|
end)
|
||||||
|
if ok and entry ~= nil then table.insert(candidates, entry) end
|
||||||
|
end
|
||||||
|
return ADFlowWagons.rankWagons(preview.name, candidates)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:flowVehicleContext(adEnv, preview, wagons)
|
||||||
|
local result = {}
|
||||||
|
local hasWagons = wagons ~= nil and #wagons > 0
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local ok, entry = pcall(function()
|
||||||
|
local text = self:describeVehicle(adEnv, preview, vehicle, false)
|
||||||
|
local validation = ADRunsController.validateVehicle(adEnv, preview, vehicle)
|
||||||
|
local other = ADRunsController.getRunForVehicle(vehicle)
|
||||||
|
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
|
||||||
|
-- solo-traktor (ingen vogn bagpå, kan trække): valgbar når en ledig vogn findes — den hentes ved Connect
|
||||||
|
local solo = validation.reason == "noTrailer" and ADPool.canPull(ADPool.ownRole(vehicle))
|
||||||
|
if solo and hasWagons then text = string.format(self.i18n:getText("spu_guide_flow_soloText"), tostring(label)) end
|
||||||
|
local blocked = SmartPickupFrame.VEHICLE_BLOCKING_REASONS[validation.reason] == true and not (solo and hasWagons)
|
||||||
|
return {id = ADRunsController.getVehicleId(vehicle), text = text, label = label, otherRunId = other ~= nil and other.id or nil,
|
||||||
|
otherRunName = other ~= nil and other.name or nil, blocked = blocked, solo = solo}
|
||||||
|
end)
|
||||||
|
if ok and entry.id ~= nil then
|
||||||
|
table.insert(result, entry)
|
||||||
|
elseif not ok then
|
||||||
|
Logging.warning(SmartPickupFrame.LOG_PREFIX .. "traktor '%s' kunne ikke vises i flowets traktorliste: %s",
|
||||||
|
tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), tostring(entry))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draft: kladden (eller selve run'et — samme feltnavne).
|
||||||
|
function SmartPickupFrame:flowGuideContext(adEnv, draft)
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
local preview = ADFlowGuide.previewRun(draft, function(buildingId, role)
|
||||||
|
local building = ADBuildings.find(adEnv, farmId, buildingId)
|
||||||
|
local ids = {}
|
||||||
|
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role) or {}) do
|
||||||
|
table.insert(ids, marker.id)
|
||||||
|
end
|
||||||
|
return ids
|
||||||
|
end)
|
||||||
|
local context = {buildings = self:flowBuildingContext(adEnv, draft, farmId), fillTypes = {}, products = {}, storeBuildings = {},
|
||||||
|
waitPools = ADRunsController.getWaitPools(adEnv)}
|
||||||
|
context.wagons = self:flowWagonContext(adEnv, preview)
|
||||||
|
context.vehicles = self:flowVehicleContext(adEnv, preview, context.wagons)
|
||||||
|
if draft.kind == "outbound" then
|
||||||
|
for _, product in ipairs(ADRunsController.getRunProducts(adEnv, preview)) do
|
||||||
|
local title = tostring(product.title)
|
||||||
|
table.insert(context.products, {name = product.name, title = title,
|
||||||
|
text = string.format("%s — %s l", title, SmartPickupFrame.formatCompact(product.available or 0))})
|
||||||
|
end
|
||||||
|
context.storeBuildings = self:flowStoreContext(adEnv, preview, farmId)
|
||||||
|
else
|
||||||
|
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, preview)) do
|
||||||
|
table.insert(context.fillTypes, {name = fillType.name, title = tostring(fillType.title)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- fanen: tavle + resumé ----
|
||||||
|
|
||||||
|
-- Flows: det valgte flow øverst (indstillinger som "ret"-linjer + status), derunder en kort liste til at vælge
|
||||||
|
-- et andet flow. Tavlen med alle flows står i Oversigt (in-game 27/9: de to faner så ens ud).
|
||||||
|
function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||||
|
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
|
||||||
|
self.flowListRows = {}
|
||||||
|
local i18n = self.i18n
|
||||||
|
if #ADRunsController.store.runs == 0 then
|
||||||
|
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||||
|
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||||
|
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||||
|
for _, run in ipairs(group.runs) do
|
||||||
|
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||||
|
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||||
|
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||||
|
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
|
||||||
|
local runId = run.id
|
||||||
|
local actionCell = entry.cells[5]
|
||||||
|
self:makeRowClickable(row, function(frame)
|
||||||
|
-- klik på [Start]/[Stop]-feltet = handling (kun dette flow); ellers åbn redigeringsvinduet
|
||||||
|
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
||||||
|
frame:toggleRun(frame.getAdEnv(), runId)
|
||||||
|
frame:requestRebuild()
|
||||||
|
else
|
||||||
|
frame.selectedRunId = runId
|
||||||
|
frame:openEditPanel("flow", runId)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
table.insert(self.flowListRows, entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:refreshFlowListRows(adEnv)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Listens celler, prik og statusfarve (hvert sekund og efter opbygning).
|
||||||
|
function SmartPickupFrame:refreshFlowListRows(adEnv)
|
||||||
|
local unpackFn = table.unpack or unpack
|
||||||
|
for _, entry in ipairs(self.flowListRows or {}) do
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
|
||||||
|
if run ~= nil then
|
||||||
|
local cells = self:flowCells(adEnv, run)
|
||||||
|
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
||||||
|
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||||
|
entry.cells[1]:setText(tostring(run.name))
|
||||||
|
entry.cells[2]:setText(tostring(cells[2]))
|
||||||
|
entry.cells[3]:setText(tostring(cells[5]))
|
||||||
|
entry.cells[4]:setText(tostring(cells[6]))
|
||||||
|
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
|
||||||
|
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
|
||||||
|
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start/Stop for ét flow (rækkens felt og vinduets knap). Husker om sidste start blev helt afvist (rød).
|
||||||
|
function SmartPickupFrame:toggleRun(adEnv, runId)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
if adEnv == nil or run == nil then return end
|
||||||
|
self.lastStartFailed = self.lastStartFailed or {}
|
||||||
|
if ADRunsController.getRunStatus(adEnv, run).canStop then
|
||||||
|
ADRunsController.stopRun(adEnv, run.id)
|
||||||
|
self.lastStartFailed[run.id] = nil
|
||||||
|
self.statusOverride = nil
|
||||||
|
else
|
||||||
|
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
||||||
|
self.lastStartFailed[run.id] = started == 0 and skipped > 0
|
||||||
|
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
|
||||||
|
self:showOverride(text, reasons)
|
||||||
|
-- statuslinjen kan ligge uden for skærmen: sprunget over vises også som besked i højre side
|
||||||
|
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
||||||
|
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ADRunsController.save()
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:flowLevel(adEnv, run)
|
||||||
|
local row = ADRunsController.getFlowRow(adEnv, run)
|
||||||
|
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||||
|
-- en gammel afvisning gælder ikke længere når flowet kører (fx startet fra Oversigt bagefter)
|
||||||
|
if row.running > 0 and self.lastStartFailed ~= nil then self.lastStartFailed[run.id] = nil end
|
||||||
|
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
|
||||||
|
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
|
||||||
|
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:fillFlowSummary(adEnv, run)
|
||||||
|
local i18n = self.i18n
|
||||||
|
if not self.editPanelOpen then
|
||||||
|
self:addTextRow(string.format(i18n:getText("spu_guide_flow_section"), tostring(run.name)))
|
||||||
|
end
|
||||||
|
self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
|
||||||
|
local guide = ADFlowGuide.open(run, self:flowGuideContext(adEnv, run))
|
||||||
|
local getText = function(key) return i18n:getText(key) end
|
||||||
|
local runId = run.id
|
||||||
|
for _, line in ipairs(ADFlowGuide.summary(guide)) do
|
||||||
|
local step = line.step
|
||||||
|
self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
|
||||||
|
if step == "name" then frame:onClickRename() else frame:openFlowStep(runId, step) end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
local cells = self:flowCells(adEnv, run)
|
||||||
|
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), tostring(cells[6])))
|
||||||
|
self:addLinkRow(" " .. i18n:getText("spu_guide_flow_open"), function(frame)
|
||||||
|
frame.selectedRunId = runId
|
||||||
|
frame:onClickFlowSetup()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Redigeringsvinduet for et flow (gui/SmartPickupEditPanel.lua).
|
||||||
|
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
||||||
|
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
|
||||||
|
canDelete = true,
|
||||||
|
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
|
||||||
|
title = function(frame, id)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||||
|
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
|
||||||
|
end,
|
||||||
|
fill = function(frame, adEnv, id)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||||
|
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
|
||||||
|
end,
|
||||||
|
status = function(frame, adEnv, id)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||||
|
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
|
||||||
|
end,
|
||||||
|
startStopText = function(frame, adEnv, id)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, id)
|
||||||
|
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
|
||||||
|
end,
|
||||||
|
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
|
||||||
|
delete = function(frame, id)
|
||||||
|
frame.selectedRunId = id
|
||||||
|
frame:onClickDelete()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Hvert sekund: tavlens celler, statuslinje og knapper.
|
||||||
|
function SmartPickupFrame:refreshFlowTab(adEnv)
|
||||||
|
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
|
||||||
|
self:setStatusText(self.statusOverride or "")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:refreshFlowListRows(adEnv)
|
||||||
|
local running, total = 0, 0
|
||||||
|
for _, run in ipairs(ADRunsController.store.runs) do
|
||||||
|
total = total + 1
|
||||||
|
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
|
||||||
|
end
|
||||||
|
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- hooks til den fælles guide-skærm ----
|
||||||
|
|
||||||
|
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
|
||||||
|
SmartPickupFrame.GUIDE_HOOKS.flow = {
|
||||||
|
model = ADFlowGuide,
|
||||||
|
tab = SmartPickupFrame.TAB_FLOWS,
|
||||||
|
heading = function(frame, guide)
|
||||||
|
return string.format(frame.i18n:getText("spu_guide_flow_title"), tostring(guide.draft.name))
|
||||||
|
end,
|
||||||
|
context = function(frame, adEnv, guide)
|
||||||
|
-- flowet er slettet mens guiden var åben
|
||||||
|
if not guide.draft.isNew and ADRuns.getRun(ADRunsController.store, guide.draft.id) == nil then return nil end
|
||||||
|
return frame:flowGuideContext(adEnv, guide.draft)
|
||||||
|
end,
|
||||||
|
choose = function(frame, id)
|
||||||
|
if id ~= "rename" then return false end
|
||||||
|
TextInputDialog.show(SmartPickupFrame.onGuideNameResult, frame, frame.guide.draft.name, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
startKey = function() return "spu_guide_flow_start" end,
|
||||||
|
save = function(frame, guide, start) frame:saveFlowGuide(guide, start) end,
|
||||||
|
}
|
||||||
|
|
||||||
|
function SmartPickupFrame:onGuideNameResult(value, clickOk)
|
||||||
|
self:safe(function(frame)
|
||||||
|
if not clickOk or frame.guide == nil or frame.guideKind ~= "flow" then return end
|
||||||
|
frame.guide = ADFlowGuide.choose(frame.guide, "name", value)
|
||||||
|
frame:requestRebuild()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gem: nyt flow oprettes først nu; felterne via updateRun; traktorer tildeles/fjernes; markører findes.
|
||||||
|
-- start = true: flowet startes bagefter ([Start flow]).
|
||||||
|
function SmartPickupFrame:saveFlowGuide(guide, start)
|
||||||
|
local adEnv = self.getAdEnv()
|
||||||
|
local result = ADFlowGuide.result(guide)
|
||||||
|
local runId = result.runId
|
||||||
|
local oldIds = {}
|
||||||
|
if result.isNew then
|
||||||
|
runId = ADRunsController.createRun(result.name, result.kind).id
|
||||||
|
else
|
||||||
|
local existing = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
if existing == nil then return end
|
||||||
|
oldIds = existing.vehicleIds
|
||||||
|
end
|
||||||
|
ADRunsController.updateRun(runId, result.changes)
|
||||||
|
local added, removed = ADFlowGuide.vehicleChanges(oldIds, result.vehicleIds)
|
||||||
|
for _, id in ipairs(removed) do
|
||||||
|
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||||
|
if vehicle ~= nil then
|
||||||
|
ADRunsController.unassignVehicle(vehicle)
|
||||||
|
else
|
||||||
|
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, id in ipairs(added) do
|
||||||
|
local vehicle = adEnv ~= nil and ADRunsController.findVehicleById(adEnv, id) or nil
|
||||||
|
if vehicle ~= nil then ADRunsController.assignVehicle(runId, vehicle) end
|
||||||
|
end
|
||||||
|
-- solo-traktorernes vogne fra guiden: flowet husker dem straks og henter dem ved Connect ved start
|
||||||
|
local savedRun = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
if savedRun ~= nil then
|
||||||
|
local wagons = ADRuns.copyWagons(savedRun.wagons)
|
||||||
|
for tractorId, wagonId in pairs(result.wagons or {}) do wagons[tractorId] = wagonId end
|
||||||
|
savedRun.wagons = wagons
|
||||||
|
end
|
||||||
|
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, runId))
|
||||||
|
self.selectedRunId = runId
|
||||||
|
local saved = string.format(self.i18n:getText("spu_guide_flow_saved"), tostring(result.name))
|
||||||
|
if start and adEnv ~= nil and not ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runId)).isRunning then
|
||||||
|
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, runId))
|
||||||
|
self:showOverride(saved .. " — " .. string.format(self.i18n:getText("spu_status_started"), started, skipped), reasons)
|
||||||
|
else
|
||||||
|
self:showOverride(saved)
|
||||||
|
end
|
||||||
|
ADRunsController.save()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- knapper og [Ret] ----
|
||||||
|
|
||||||
|
function SmartPickupFrame:onClickFlowSetup()
|
||||||
|
self:safe(function(frame)
|
||||||
|
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||||
|
if adEnv == nil or run == nil then return end
|
||||||
|
frame:openGuide("flow", ADFlowGuide.open(run, frame:flowGuideContext(adEnv, run)))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- [Nyt flow] (også fra fanen Oversigt): guiden på Flows-fanen; intet oprettes før [Gem].
|
||||||
|
function SmartPickupFrame:onClickFlowNew()
|
||||||
|
self:safe(function(frame)
|
||||||
|
local adEnv = frame.getAdEnv()
|
||||||
|
if adEnv == nil then return end
|
||||||
|
local name = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
||||||
|
local guide = ADFlowGuide.openNew(name, {})
|
||||||
|
guide = ADGuide.withContext(guide, frame:flowGuideContext(adEnv, guide.draft))
|
||||||
|
if frame.currentTab ~= SmartPickupFrame.TAB_FLOWS then frame:switchTab(SmartPickupFrame.TAB_FLOWS) end
|
||||||
|
frame:openGuide("flow", guide)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:openFlowStep(runId, step)
|
||||||
|
local adEnv = self.getAdEnv()
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
if adEnv == nil or run == nil then return end
|
||||||
|
self:openGuide("flow", ADFlowGuide.open(run, self:flowGuideContext(adEnv, run), {step = step, single = true, returnTo = "close"}))
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
-- Smart Pickup — fanen Flows
|
-- Smart Pickup — fanen Flows
|
||||||
-- Tavle øverst: én tekstrække pr. flow (Vare · Fra → Til · Regel · Traktorer · Status), det valgte
|
-- Tavlen (én række pr. flow, det valgte markeret med ">") bruges af fanerne Oversigt og Flows; Flows-fanens
|
||||||
-- markeret med ">". Under tavlen: detail-blok for det valgte flow — Retning, Fra/Til som BYGNINGER
|
-- resumé og guiden "Opsæt flow" ligger i gui/SmartPickupFlowGuide.lua. Her også bygnings-teksten og de
|
||||||
-- (markøren findes automatisk), Varer, Regler, Vent ved, Traktorer, Status — og Wait-puljerne.
|
-- gamle klik-callbacks for retning/bygninger.
|
||||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||||
|
|
||||||
-- ---- tavlen ----
|
-- ---- tavlen ----
|
||||||
@@ -49,7 +49,11 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
|||||||
end
|
end
|
||||||
local rule
|
local rule
|
||||||
if row.kind == "outbound" then
|
if row.kind == "outbound" then
|
||||||
rule = string.format(i18n:getText("spu_flow_ruleOut"), row.sellAtPercent or 90, row.storeAbovePercent or 80)
|
local sells, stores = ADFlowView.policyUse(row.policies)
|
||||||
|
local parts = {}
|
||||||
|
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||||
|
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
||||||
|
rule = table.concat(parts, ", ")
|
||||||
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
||||||
else
|
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")
|
rule = row.loops ~= nil and row.loops > 0 and string.format(i18n:getText("spu_flow_ruleInLoops"), row.loops) or i18n:getText("spu_flow_ruleIn")
|
||||||
@@ -80,8 +84,12 @@ function SmartPickupFrame:makeRowClickable(row, action)
|
|||||||
row.mouseEvent = function(element, posX, posY, isDown, isUp, button, eventUsed)
|
row.mouseEvent = function(element, posX, posY, isDown, isUp, button, eventUsed)
|
||||||
local used = baseMouseEvent(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
|
-- 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())
|
-- klik tæller inde i rækkens eget layout (listen eller redigeringsvinduet); listen er død mens vinduet er åbent
|
||||||
and isInside(element, posX, posY) and isInside(frame.boxLayout, posX, posY) then
|
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
|
frame.pendingClick = action
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -137,115 +145,6 @@ function SmartPickupFrame:describeBuilding(building, role)
|
|||||||
return string.format(" %s — %s", building.name, table.concat(parts, " · "))
|
return string.format(" %s — %s", building.name, table.concat(parts, " · "))
|
||||||
end
|
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 ----
|
-- ---- 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,24 @@ SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
|
|||||||
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
||||||
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
||||||
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
|
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)
|
-- 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_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"}
|
||||||
SmartPickupFrame.TAB_KINDS = {[4] = "factory", [5] = "silo", [6] = "husbandry", [7] = "objectStorage"}
|
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
||||||
|
|
||||||
function SmartPickupFrame.getTabKind(tab)
|
function SmartPickupFrame.getTabKind(tab)
|
||||||
return SmartPickupFrame.TAB_KINDS[tab]
|
return SmartPickupFrame.TAB_KINDS[tab]
|
||||||
end
|
end
|
||||||
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
|
||||||
-- Afvisningsgrunde der handler om VOGNEN (ikke run'ets opsætning): traktoren kan ikke vælges.
|
-- 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)
|
function SmartPickupFrame.new(i18n, getAdEnv)
|
||||||
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
|
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
|
||||||
@@ -32,6 +39,7 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
|||||||
self.runIds, self.targetIds, self.poolGroups = {}, {}, {}
|
self.runIds, self.targetIds, self.poolGroups = {}, {}, {}
|
||||||
self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...}
|
self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...}
|
||||||
self.clonedRows = {}
|
self.clonedRows = {}
|
||||||
|
self.panelRows = {}
|
||||||
self.refreshTimer = 0
|
self.refreshTimer = 0
|
||||||
self.statusOverride = nil
|
self.statusOverride = nil
|
||||||
self.statusRow = nil -- Text-elementet i den klonede statusrække
|
self.statusRow = nil -- Text-elementet i den klonede statusrække
|
||||||
@@ -49,11 +57,21 @@ function SmartPickupFrame.new(i18n, getAdEnv)
|
|||||||
-- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow.
|
-- 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.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.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.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
|
-- 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}
|
-- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
|
||||||
self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo}
|
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.
|
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
|
||||||
self.menuButtonInfo = self.overviewButtons
|
self.menuButtonInfo = self.overviewButtons
|
||||||
self:setMenuButtonInfo(self.menuButtonInfo)
|
self:setMenuButtonInfo(self.menuButtonInfo)
|
||||||
@@ -71,7 +89,7 @@ end
|
|||||||
|
|
||||||
-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker.
|
-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker.
|
||||||
function SmartPickupFrame:initialize()
|
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]
|
local prefab = self[name]
|
||||||
if prefab ~= nil then
|
if prefab ~= nil then
|
||||||
prefab:unlinkElement()
|
prefab:unlinkElement()
|
||||||
@@ -197,6 +215,9 @@ end
|
|||||||
|
|
||||||
function SmartPickupFrame:onFrameClose()
|
function SmartPickupFrame:onFrameClose()
|
||||||
self.frameOpen = false
|
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)
|
SmartPickupFrame:superClass().onFrameClose(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -243,6 +264,11 @@ function SmartPickupFrame:requestRebuild()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:switchTab(tabIndex)
|
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
|
self.currentTab = tabIndex
|
||||||
for index, tab in ipairs(self.subCategoryTabs) do
|
for index, tab in ipairs(self.subCategoryTabs) do
|
||||||
tab:setSelected(index == tabIndex)
|
tab:setSelected(index == tabIndex)
|
||||||
@@ -263,7 +289,8 @@ end
|
|||||||
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
-- 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.
|
-- Kør til lager for udkørsel. Fanen Lagre viser ingen.
|
||||||
function SmartPickupFrame:setStaticRowsVisible(tab, run)
|
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 hasRun = isFlows and run ~= nil
|
||||||
local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound"
|
local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound"
|
||||||
local visibility = {
|
local visibility = {
|
||||||
@@ -312,6 +339,7 @@ function SmartPickupFrame:clearRows()
|
|||||||
self.productRows, self.outboundStatusRows = {}, {}
|
self.productRows, self.outboundStatusRows = {}, {}
|
||||||
self.storageRows = {}
|
self.storageRows = {}
|
||||||
self.storageDetailRows = {}
|
self.storageDetailRows = {}
|
||||||
|
self.itemView = nil
|
||||||
self.boardRows = {}
|
self.boardRows = {}
|
||||||
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
|
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
|
||||||
end
|
end
|
||||||
@@ -328,9 +356,11 @@ function SmartPickupFrame:addToggleRow(kind, title, texts, isOn)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:addRow(prefab, data)
|
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)
|
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")
|
local option = row:getDescendantByName("option")
|
||||||
if option ~= nil then
|
if option ~= nil then
|
||||||
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
||||||
@@ -339,23 +369,56 @@ function SmartPickupFrame:addRow(prefab, data)
|
|||||||
return row
|
return row
|
||||||
end
|
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.
|
-- 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)
|
function SmartPickupFrame:addTextRow(text)
|
||||||
|
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")
|
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||||
title:setText(text)
|
title:setText(line)
|
||||||
return title
|
first = first or title
|
||||||
|
end
|
||||||
|
return first
|
||||||
end
|
end
|
||||||
|
|
||||||
-- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning,
|
-- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning,
|
||||||
-- fordi rækkerne først findes på det tidspunkt.
|
-- fordi rækkerne først findes på det tidspunkt.
|
||||||
function SmartPickupFrame:colorRows()
|
function SmartPickupFrame:colorRows()
|
||||||
|
self:colorLayout(self.boxLayout)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:colorLayout(layout)
|
||||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
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
|
return
|
||||||
end
|
end
|
||||||
local unpackFn = table.unpack or unpack
|
local unpackFn = table.unpack or unpack
|
||||||
local isEvenRow = false
|
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
|
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
||||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
||||||
end
|
end
|
||||||
@@ -390,10 +453,45 @@ function SmartPickupFrame:rebuild()
|
|||||||
self.statusRow = self:addTextRow("")
|
self.statusRow = self:addTextRow("")
|
||||||
self:colorRows()
|
self:colorRows()
|
||||||
self.boxLayout:invalidateLayout()
|
self.boxLayout:invalidateLayout()
|
||||||
|
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||||
self:refreshLive()
|
self:refreshLive()
|
||||||
self:restoreFocusIfLost()
|
self:restoreFocusIfLost()
|
||||||
return
|
return
|
||||||
end
|
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 self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
|
||||||
if adEnv ~= nil then
|
if adEnv ~= nil then
|
||||||
self:fillOverviewRows(adEnv)
|
self:fillOverviewRows(adEnv)
|
||||||
@@ -407,46 +505,17 @@ function SmartPickupFrame:rebuild()
|
|||||||
self:restoreFocusIfLost()
|
self:restoreFocusIfLost()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:fillRunOption(adEnv)
|
if adEnv ~= nil then
|
||||||
local hasRun = run ~= nil and adEnv ~= nil
|
self:fillFlowTabRows(adEnv)
|
||||||
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"))
|
|
||||||
end
|
end
|
||||||
self.statusRow = self:addTextRow("")
|
self.statusRow = self:addTextRow("")
|
||||||
self:colorRows()
|
self:colorRows()
|
||||||
self.boxLayout:invalidateLayout()
|
self.boxLayout:invalidateLayout()
|
||||||
|
if self.editPanelOpen then self:fillEditPanel(adEnv) end
|
||||||
self:refreshLive()
|
self:refreshLive()
|
||||||
self:restoreFocusIfLost()
|
self:restoreFocusIfLost()
|
||||||
end
|
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)
|
-- Overblik: "Cow 1 food: TMR 18.0k/27.8k · Silage 3.0k/…" (mest trængende først)
|
||||||
function SmartPickupFrame:describeTargetLevels(target)
|
function SmartPickupFrame:describeTargetLevels(target)
|
||||||
if #target.levels == 0 then
|
if #target.levels == 0 then
|
||||||
@@ -455,114 +524,6 @@ function SmartPickupFrame:describeTargetLevels(target)
|
|||||||
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
|
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
|
||||||
end
|
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,
|
-- 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.
|
-- og loggen ("run '…': waitPoolGroup=…") viser om klikket blev registreret.
|
||||||
function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
|
function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
|
||||||
@@ -657,10 +618,20 @@ end
|
|||||||
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
|
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
|
||||||
function SmartPickupFrame:refreshButtons()
|
function SmartPickupFrame:refreshButtons()
|
||||||
local wanted = self.flowButtons
|
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
|
wanted = self.overviewButtons
|
||||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
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
|
end
|
||||||
if self.menuButtonInfo ~= wanted then
|
if self.menuButtonInfo ~= wanted then
|
||||||
self.menuButtonInfo = wanted
|
self.menuButtonInfo = wanted
|
||||||
@@ -681,10 +652,24 @@ function SmartPickupFrame:refreshLive()
|
|||||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||||
return
|
return
|
||||||
end
|
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
|
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
|
||||||
if isOverview then
|
if isOverview then
|
||||||
self:refreshOverview(adEnv)
|
self:refreshOverview(adEnv)
|
||||||
end
|
end
|
||||||
|
if self.editPanelOpen then self:refreshEditPanel(adEnv) end
|
||||||
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||||
self:refreshFieldRows(adEnv)
|
self:refreshFieldRows(adEnv)
|
||||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||||
@@ -692,6 +677,13 @@ function SmartPickupFrame:refreshLive()
|
|||||||
self:refreshButtons()
|
self:refreshButtons()
|
||||||
return
|
return
|
||||||
end
|
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)
|
self:refreshStorageRows(adEnv)
|
||||||
local isRunning = false
|
local isRunning = false
|
||||||
if isOverview then
|
if isOverview then
|
||||||
@@ -701,8 +693,19 @@ function SmartPickupFrame:refreshLive()
|
|||||||
running, total = running + status.running, total + status.total
|
running, total = running + status.running, total + status.total
|
||||||
if status.running > 0 then activeRuns = activeRuns + 1 end
|
if status.running > 0 then activeRuns = activeRuns + 1 end
|
||||||
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
|
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
|
elseif run ~= nil then
|
||||||
self:syncOptionsFromModel(adEnv, run)
|
self:syncOptionsFromModel(adEnv, run)
|
||||||
if run.kind == "outbound" then
|
if run.kind == "outbound" then
|
||||||
@@ -711,7 +714,7 @@ function SmartPickupFrame:refreshLive()
|
|||||||
self:refreshNeedsRows(adEnv, run)
|
self:refreshNeedsRows(adEnv, run)
|
||||||
end
|
end
|
||||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
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)
|
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)
|
or string.format(self.i18n:getText("spu_status_stopped"), status.total)
|
||||||
self:setStatusText(self.statusOverride or text)
|
self:setStatusText(self.statusOverride or text)
|
||||||
@@ -747,27 +750,14 @@ function SmartPickupFrame:onClickRun(state)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:onClickIngredient(state, element)
|
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]
|
local data = self.rowData[element]
|
||||||
if data ~= nil and data.kind == "target" then
|
if data ~= nil and data.kind == "guideRow" then
|
||||||
return self:onClickTarget(state, element)
|
return self:onClickGuideRow(state, data)
|
||||||
elseif data ~= nil and data.kind == "source" then
|
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
|
||||||
return self:onClickSource(state, element)
|
return self:onClickFieldSiloRow(state, data)
|
||||||
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
|
|
||||||
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
||||||
return self:onClickFieldRow(state, data)
|
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
|
end
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
local run, data = frame:getRun(), frame.rowData[element]
|
local run, data = frame:getRun(), frame.rowData[element]
|
||||||
@@ -793,12 +783,18 @@ end
|
|||||||
|
|
||||||
function SmartPickupFrame:onClickVehicle(state, element)
|
function SmartPickupFrame:onClickVehicle(state, element)
|
||||||
local clicked = self.rowData[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
|
if clicked ~= nil and clicked.kind == "fleetVehicle" then
|
||||||
return self:onClickFieldRow(state, clicked)
|
return self:onClickFieldRow(state, clicked)
|
||||||
end
|
end
|
||||||
|
if clicked ~= nil and clicked.kind == "guideRow" then
|
||||||
|
return self:onClickGuideRow(state, clicked)
|
||||||
|
end
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
local run, data = frame:getRun(), frame.rowData[element]
|
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
|
if state == SmartPickupFrame.STATE_ON then
|
||||||
local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle)
|
local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle)
|
||||||
if previousRunId ~= nil then
|
if previousRunId ~= nil then
|
||||||
@@ -819,14 +815,7 @@ function SmartPickupFrame:onClickStartStop()
|
|||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
local adEnv, run = frame.getAdEnv(), frame:getRun()
|
||||||
if adEnv == nil or run == nil then return end
|
if adEnv == nil or run == nil then return end
|
||||||
if ADRunsController.getRunStatus(adEnv, run).isRunning then
|
frame:toggleRun(adEnv, run.id)
|
||||||
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:requestRebuild()
|
frame:requestRebuild()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -866,11 +855,28 @@ function SmartPickupFrame:onClickStartAll()
|
|||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
local adEnv = frame.getAdEnv()
|
local adEnv = frame.getAdEnv()
|
||||||
if adEnv == nil then return end
|
if adEnv == nil then return end
|
||||||
local summary = ADRunsController.startAllRuns(adEnv)
|
-- pausen (Stop alt og parkér) ophæves; traktorerne startes én ad gangen med mellemrum
|
||||||
local _, skipped, reasons = frame:summarizeStartResults(summary.results)
|
if ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then ADFieldJobs.setPaused(false) end
|
||||||
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d flows startet (%d traktorer), %d kørte allerede, %d sprunget over",
|
local summary = ADRunsController.queueStartAll(adEnv)
|
||||||
summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped)
|
local _, skipped, reasons = frame:summarizeStartResults(summary.skipped)
|
||||||
frame:showOverride(string.format(frame.i18n:getText("spu_status_startedAll"), summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped), reasons)
|
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()
|
ADRunsController.save()
|
||||||
frame:requestRebuild()
|
frame:requestRebuild()
|
||||||
end)
|
end)
|
||||||
@@ -897,25 +903,28 @@ function SmartPickupFrame:openFlow(runId)
|
|||||||
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Nyt run: navnet vælges FØR run'et oprettes (annulleres dialogen, oprettes intet).
|
-- Nyt flow: guiden "Opsæt flow" (gui/SmartPickupFlowGuide.lua) — intet oprettes før [Gem].
|
||||||
-- Forslaget tæller efter antal runs, ikke efter løbenummeret, så "Run 2" kan genbruges.
|
|
||||||
function SmartPickupFrame:onClickNew()
|
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)
|
self:safe(function(frame)
|
||||||
local suggestion = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
|
local placeable = frame.storageDetailPlaceable
|
||||||
TextInputDialog.show(SmartPickupFrame.onNewRunName, frame, suggestion, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:onNewRunName(value, clickOk)
|
function SmartPickupFrame:onRenameBuildingResult(value, clickOk)
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
if not clickOk or value == nil or not value:match("%S") then
|
if not clickOk or frame.renamingBuildingId == nil then return end
|
||||||
return
|
ADNames.set(frame.renamingBuildingId, value)
|
||||||
end
|
frame.renamingBuildingId = nil
|
||||||
local run = ADRunsController.createRun(value, "supply")
|
frame:requestRebuild()
|
||||||
frame.selectedRunId = run.id
|
|
||||||
frame.statusOverride = nil
|
|
||||||
frame.currentTab = SmartPickupFrame.TAB_FLOWS
|
|
||||||
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,14 @@
|
|||||||
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_storeAbove"/>
|
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_storeAbove"/>
|
||||||
</Bitmap>
|
</Bitmap>
|
||||||
</ScrollingLayout>
|
</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 -->
|
<!-- prefabs: klones pr. vare / traktor / tekstlinje og fjernes fra layoutet ved initialize -->
|
||||||
<Bitmap profile="fs25_multiTextOptionContainer" id="ingredientPrefab">
|
<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="c5" position="620px 0px" size="50px 40px" textMaxWidth="42px"/>
|
||||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||||
</Bitmap>
|
</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. -->
|
<!-- 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">
|
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
||||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="100px 40px" textMaxWidth="92px"/>
|
<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="c5" position="440px 0px" size="230px 40px" textMaxWidth="222px"/>
|
||||||
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
<Text profile="spuCell" name="c6" position="670px 0px" size="140px 40px" textMaxWidth="132px"/>
|
||||||
</Bitmap>
|
</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.) -->
|
<!-- Lagre: statistik-tabel (vare · i bygningen · værdi nu · ved toppris · gården · rækker til · forbrug/prod.) -->
|
||||||
<Bitmap profile="fs25_multiTextOptionContainer" id="statPrefab">
|
<Bitmap profile="fs25_multiTextOptionContainer" id="statPrefab">
|
||||||
<Text profile="spuCellBold" name="c1" position="20px 0px" size="140px 40px" textMaxWidth="132px"/>
|
<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="c2" position="290px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||||
<Text profile="spuCell" name="c3" position="550px 0px" size="260px 40px" textMaxWidth="252px"/>
|
<Text profile="spuCell" name="c3" position="550px 0px" size="260px 40px" textMaxWidth="252px"/>
|
||||||
</Bitmap>
|
</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 -->
|
<!-- udkørsel: én række pr. vare med politik Fra/Lager/Salg/Salg, ellers lager -->
|
||||||
<Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab">
|
<Bitmap profile="fs25_multiTextOptionContainer" id="productPrefab">
|
||||||
<MultiTextOption profile="fs25_settingsMultiTextOption" onClick="onClickProduct" name="option">
|
<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
|
return 1
|
||||||
end
|
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).
|
-- Hvert sekund: produktrækkernes mængde/pris og statuslinjerne (antal linjer ændres kun ved rebuild).
|
||||||
function SmartPickupFrame:refreshOutboundRows(adEnv, run)
|
function SmartPickupFrame:refreshOutboundRows(adEnv, run)
|
||||||
@@ -167,21 +110,21 @@ end
|
|||||||
|
|
||||||
-- ---- callbacks ----
|
-- ---- 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)
|
function SmartPickupFrame:onClickProduct(state, element)
|
||||||
local clicked = self.rowData[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
|
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
||||||
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
||||||
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
|
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
||||||
return self:onClickFieldRow(state, clicked)
|
return self:onClickFieldRow(state, clicked)
|
||||||
end
|
end
|
||||||
@@ -210,10 +153,3 @@ function SmartPickupFrame:onClickStoreAbove(state)
|
|||||||
end)
|
end)
|
||||||
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)
|
function SmartPickupFrame:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||||
self.storageDetailRows = {}
|
self.storageDetailRows = {}
|
||||||
|
self.storageDetailPlaceable = nil
|
||||||
if #inventory == 0 then
|
if #inventory == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -177,6 +178,12 @@ function SmartPickupFrame:fillStorageRows(adEnv)
|
|||||||
self.storageRows = {}
|
self.storageRows = {}
|
||||||
local fullInventory = self:getInventory(adEnv)
|
local fullInventory = self:getInventory(adEnv)
|
||||||
local inventory = ADStorageStats.filterKind(fullInventory, SmartPickupFrame.getTabKind(self.currentTab))
|
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:fillStorageDetail(adEnv, inventory, fullInventory)
|
||||||
self:addTextRow(self.i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))))
|
self:addTextRow(self.i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))))
|
||||||
for _, entry in ipairs(inventory) do
|
for _, entry in ipairs(inventory) do
|
||||||
@@ -193,6 +200,11 @@ end
|
|||||||
|
|
||||||
function SmartPickupFrame:refreshStorageRows(adEnv)
|
function SmartPickupFrame:refreshStorageRows(adEnv)
|
||||||
local kind = SmartPickupFrame.getTabKind(self.currentTab)
|
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
|
if kind == nil or #(self.storageRows or {}) == 0 then
|
||||||
return
|
return
|
||||||
end
|
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.
|
-- 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).
|
-- 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)
|
function SmartPickupFrame:describeTargetNeeds(target)
|
||||||
if #target.levels == 0 then
|
if #target.levels == 0 then
|
||||||
@@ -58,39 +24,8 @@ function SmartPickupFrame:refreshNeedsRows(adEnv, run)
|
|||||||
end
|
end
|
||||||
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)
|
function SmartPickupFrame:onClickLoops(state)
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<Profile name="spuInfoText" extends="fs25_settingsMultiTextOptionTitle">
|
<Profile name="spuInfoText" extends="fs25_settingsMultiTextOptionTitle">
|
||||||
<textBold value="false"/>
|
<textBold value="false"/>
|
||||||
<size value="1400px 40px"/>
|
<size value="1400px 40px"/>
|
||||||
|
<textMaxWidth value="800px"/>
|
||||||
</Profile>
|
</Profile>
|
||||||
<!-- Tabel-celler i fanen Lagre: position og bredde sættes pr. celle i SmartPickupFrame.xml. -->
|
<!-- Tabel-celler i fanen Lagre: position og bredde sættes pr. celle i SmartPickupFrame.xml. -->
|
||||||
<Profile name="spuCell" extends="fs25_settingsMultiTextOptionTitle">
|
<Profile name="spuCell" extends="fs25_settingsMultiTextOptionTitle">
|
||||||
@@ -19,4 +20,53 @@
|
|||||||
<Profile name="spuCellBold" extends="spuCell">
|
<Profile name="spuCellBold" extends="spuCell">
|
||||||
<textBold value="true"/>
|
<textBold value="true"/>
|
||||||
</Profile>
|
</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>
|
</GUIProfiles>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<text name="spu_title" text="Smart Pickup"/>
|
<text name="spu_title" text="Smart Pickup"/>
|
||||||
<text name="spu_run" text="Run"/>
|
<text name="spu_run" text="Run"/>
|
||||||
<text name="spu_run_tooltip" text="Vælg det run du vil se eller ændre."/>
|
<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" text="Mål"/>
|
||||||
<text name="spu_target_tooltip" text="AutoDrive-markør ved stalden eller fabrikken der skal fyldes."/>
|
<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)"/>
|
<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_started" text="Startet %d, sprunget over %d"/>
|
||||||
<text name="spu_status_noAd" text="AutoDrive-API ikke fundet"/>
|
<text name="spu_status_noAd" text="AutoDrive-API ikke fundet"/>
|
||||||
<text name="spu_status_error" text="Fejl: %s"/>
|
<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_cannotCarry" text="vognen kan ikke bære run'ets varer"/>
|
||||||
<text name="spu_reason_farFromNetwork" text="for langt fra vejnettet"/>
|
<text name="spu_reason_farFromNetwork" text="for langt fra vejnettet"/>
|
||||||
<text name="spu_reason_noTarget" text="intet mål"/>
|
<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_status_stoppedAll" text="Stoppet %d traktorer"/>
|
||||||
<text name="spu_overview_hint" text="Klik på et flow for at redigere det"/>
|
<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_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_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_button_stop" text="Stop mark"/>
|
||||||
<text name="spu_field_noMarker" text="! mangler markør F%d"/>
|
<text name="spu_field_noMarker" text="! mangler markør F%d"/>
|
||||||
<text name="spu_field_stage_idle" text="Venter"/>
|
<text name="spu_field_stage_idle" text="Venter"/>
|
||||||
@@ -51,8 +75,45 @@
|
|||||||
<text name="spu_field_stage_collecting" text="Samles op"/>
|
<text name="spu_field_stage_collecting" text="Samles op"/>
|
||||||
<text name="spu_field_stage_done" text="Færdig"/>
|
<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_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_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_tooWide" text="%s for bred: %.1f m > %.0f m (Courseplay)"/>
|
||||||
|
<text name="spu_field_reason_widthUnknown" text="%s: arbejdsbredde %.1f m kan ikke være rigtig — sæt bredden i Courseplay (sendes ikke ud)"/>
|
||||||
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
|
<text name="spu_field_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_mower" text="slåmaskine"/>
|
||||||
<text name="spu_field_role_tedder" text="vender"/>
|
<text name="spu_field_role_tedder" text="vender"/>
|
||||||
@@ -65,25 +126,33 @@
|
|||||||
<text name="spu_field_mode_manual" text="Manuel"/>
|
<text name="spu_field_mode_manual" text="Manuel"/>
|
||||||
<text name="spu_field_notMowable" text="ikke græs/kløver"/>
|
<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_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_field_showAll" text="Vis alle marker (%d skjult)"/>
|
||||||
<text name="spu_col_field" text="Mark"/>
|
<text name="spu_col_field" text="Mark"/>
|
||||||
<text name="spu_col_crop" text="Afgrøde"/>
|
<text name="spu_col_crop" text="Afgrøde"/>
|
||||||
<text name="spu_col_plan" text="Plan"/>
|
<text name="spu_col_plan" text="Plan"/>
|
||||||
<text name="spu_col_mode" text="Kørsel"/>
|
<text name="spu_col_mode" text="Kørsel"/>
|
||||||
<text name="spu_field_section" text="— Mark F%d: %s —"/>
|
<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_plan" text="Plan"/>
|
||||||
<text name="spu_field_mode" text="Kørsel"/>
|
<text name="spu_field_mode" text="Kørsel"/>
|
||||||
<text name="spu_field_rake" text="Riv skårene sammen før opsamling (gør smalle læssevogne brugbare)"/>
|
<text name="spu_field_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_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_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_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_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_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_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_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
|
||||||
<text name="spu_field_section_fleet" text="— Markflåde (flueben = må sendes ud) —"/>
|
<text name="spu_field_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
||||||
|
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
||||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||||
|
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
||||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
<text name="spu_field_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_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_field_startRequested" text="F%d startes, når en maskine er ledig"/>
|
||||||
@@ -94,9 +163,11 @@
|
|||||||
<text name="spu_field_fermenter" text="gæringssilo"/>
|
<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_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_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_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_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_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_mulch" text="Stubbehakning efter høst"/>
|
||||||
<text name="spu_field_need_fert" text="gødning"/>
|
<text name="spu_field_need_fert" text="gødning"/>
|
||||||
<text name="spu_field_need_lime" text="kalk"/>
|
<text name="spu_field_need_lime" text="kalk"/>
|
||||||
@@ -116,7 +187,7 @@
|
|||||||
<text name="spu_field_rig_seeds" text="frø ok"/>
|
<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_rigs" text="Maskiner pr. trin (Courseplay multi-tool)"/>
|
||||||
<text name="spu_field_role_cultivator" text="kultivator"/>
|
<text name="spu_field_role_cultivator" text="kultivator"/>
|
||||||
<text name="spu_field_role_fertilizer" text="gødningsspreder"/>
|
<text name="spu_field_role_fertilizer" text="gødskning"/>
|
||||||
<text name="spu_field_role_harvester" text="høster"/>
|
<text name="spu_field_role_harvester" text="høster"/>
|
||||||
<text name="spu_field_role_limer" text="kalkspreder"/>
|
<text name="spu_field_role_limer" text="kalkspreder"/>
|
||||||
<text name="spu_field_role_mulcher" text="stubhakker"/>
|
<text name="spu_field_role_mulcher" text="stubhakker"/>
|
||||||
@@ -152,6 +223,7 @@
|
|||||||
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
||||||
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
||||||
<text name="spu_field_notice_done" text="er færdig"/>
|
<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_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" text="Afgrøde på marken (moden passer resten)"/>
|
||||||
<text name="spu_field_target_none" text="Ingen — planlæg selv"/>
|
<text name="spu_field_target_none" text="Ingen — planlæg selv"/>
|
||||||
@@ -169,13 +241,35 @@
|
|||||||
<text name="spu_field_straw_chop" text="Hakkes (spredes på marken)"/>
|
<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_collect" text="Samles op til silo"/>
|
||||||
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
|
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
|
||||||
<text name="spu_button_start" text="Start run"/>
|
<text name="spu_button_start" text="Start flow"/>
|
||||||
<text name="spu_button_stop" text="Stop run"/>
|
<text name="spu_button_stop" text="Stop flow"/>
|
||||||
<text name="spu_button_new" text="Nyt run"/>
|
<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_rename" text="Omdøb"/>
|
||||||
<text name="spu_button_delete" text="Slet run"/>
|
<text name="spu_button_delete" text="Slet flow"/>
|
||||||
<text name="spu_newRunName" text="Run %d"/>
|
<text name="spu_panel_close" text="Luk"/>
|
||||||
<text name="spu_renameTitle" text="Navn på run"/>
|
<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_deleteConfirm" text="Slet run '%s'? Dets kørende traktorer stoppes. Kan ikke fortrydes."/>
|
||||||
<text name="spu_capacity" text="%s l"/>
|
<text name="spu_capacity" text="%s l"/>
|
||||||
<text name="spu_capacity_pallets" text="autoload (paller)"/>
|
<text name="spu_capacity_pallets" text="autoload (paller)"/>
|
||||||
@@ -219,6 +313,8 @@
|
|||||||
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
<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_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_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_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" 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."/>
|
<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."/>
|
||||||
@@ -228,6 +324,7 @@
|
|||||||
<text name="spu_reason_noProducts" text="ingen vare valgt (sæt Lager/Salg på en vare ovenfor)"/>
|
<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_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_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_reason_needsAutoload" text="pallevarer kræver Universal Autoload på vognen"/>
|
||||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||||
<text name="spu_price_pct" text="%d %% af top (%s)"/>
|
<text name="spu_price_pct" text="%d %% af top (%s)"/>
|
||||||
@@ -253,14 +350,16 @@
|
|||||||
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
||||||
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
||||||
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
||||||
|
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
||||||
|
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
||||||
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
||||||
<text name="spu_flow_round" text="runde"/>
|
<text name="spu_flow_round" text="runde"/>
|
||||||
<text name="spu_flow_ruleIn" text="behov"/>
|
<text name="spu_flow_ruleIn" text="efter behov — laveste først"/>
|
||||||
<text name="spu_flow_ruleInLoops" text="behov, %d ture"/>
|
<text name="spu_flow_ruleInLoops" text="efter behov, %d ture"/>
|
||||||
<text name="spu_markerAt" text="%s %d m"/>
|
<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_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_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_reason_noMarker" text="'%s' mangler en AD-markør ved triggeren"/>
|
||||||
<text name="spu_storage_select" text="Vis bygning"/>
|
<text name="spu_storage_select" text="Vis bygning"/>
|
||||||
<text name="spu_stat_now" text="nu %s"/>
|
<text name="spu_stat_now" text="nu %s"/>
|
||||||
@@ -300,5 +399,245 @@
|
|||||||
<text name="spu_col_lasts" text="Rækker til"/>
|
<text name="spu_col_lasts" text="Rækker til"/>
|
||||||
<text name="spu_col_rate" text="Forbrug/prod. døgn"/>
|
<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_storage_select_tooltip" text="Vælg bygningen tabellen viser."/>
|
||||||
|
<text name="spu_guide_next" text="Næste"/>
|
||||||
|
<text name="spu_guide_save" text="Gem"/>
|
||||||
|
<text name="spu_guide_ok" text="OK"/>
|
||||||
|
<text name="spu_guide_back" text="Tilbage"/>
|
||||||
|
<text name="spu_guide_cancel" text="Annullér"/>
|
||||||
|
<text name="spu_guide_saveOnly" text="Gem uden at starte"/>
|
||||||
|
<text name="spu_guide_runAuto" text="Kør automatisk"/>
|
||||||
|
<text name="spu_guide_stepOf" text="Trin %d af %d — %s"/>
|
||||||
|
<text name="spu_guide_stepOne" text="Trin %d — %s"/>
|
||||||
|
<text name="spu_guide_editStep" text="Ret: %s"/>
|
||||||
|
<text name="spu_guide_editSuffix" text=" [Ret]"/>
|
||||||
|
<text name="spu_guide_sum_hint" text="Tryk på en linje for at rette netop det."/>
|
||||||
|
<text name="spu_guide_auto" text="Automatisk (anbefalet)"/>
|
||||||
|
<text name="spu_guide_manual" text="Vælg selv"/>
|
||||||
|
<text name="spu_guide_closed" text="Guiden er lukket — marken/flowet findes ikke længere"/>
|
||||||
|
<text name="spu_guide_cannotAdvance" text="Vælg først"/>
|
||||||
|
<text name="spu_guide_pickField" text="Vælg en mark først"/>
|
||||||
|
<text name="spu_guide_saved" text="F%d gemt"/>
|
||||||
|
<text name="spu_guide_noMarker" text="! Sæt en AD-markør ved F%d — arbejdet venter på markøren"/>
|
||||||
|
<text name="spu_guide_field_open" text="Opsæt mark"/>
|
||||||
|
<text name="spu_guide_fleet_open" text="Markflåde"/>
|
||||||
|
<text name="spu_guide_fleet_close" text="Til marker"/>
|
||||||
|
<text name="spu_guide_field_title" text="— Opsæt mark F%d —"/>
|
||||||
|
<text name="spu_guide_field_titleNone" text="— Opsæt mark —"/>
|
||||||
|
<text name="spu_guide_field_step_field" text="Mark"/>
|
||||||
|
<text name="spu_guide_field_step_crop" text="Afgrøde"/>
|
||||||
|
<text name="spu_guide_field_step_cropChoice" text="Afgrødens valg"/>
|
||||||
|
<text name="spu_guide_field_step_prep" text="Klargøring"/>
|
||||||
|
<text name="spu_guide_field_step_care" text="Pleje"/>
|
||||||
|
<text name="spu_guide_field_step_machines" text="Maskiner"/>
|
||||||
|
<text name="spu_guide_field_step_store" text="Lager"/>
|
||||||
|
<text name="spu_guide_field_step_summary" text="Oversigt"/>
|
||||||
|
<text name="spu_guide_field_hint_field" text="Vælg den mark du vil sætte op."/>
|
||||||
|
<text name="spu_guide_field_hint_crop" text="Hvad skal gro på marken? 'Behold' kører videre med det der står nu."/>
|
||||||
|
<text name="spu_guide_field_hint_cropChoice" text="Hvad skal der ske med afgrøden ved slåning eller høst?"/>
|
||||||
|
<text name="spu_guide_field_hint_prep" text="Efter høst / på bar mark. Automatisk pløjer eller kultiverer og kalker efter behov."/>
|
||||||
|
<text name="spu_guide_field_hint_care" text="Mens afgrøden gror. Der gødes/sprøjtes kun når marken trænger."/>
|
||||||
|
<text name="spu_guide_field_hint_machines" text="Hvor mange ad gangen, og skal marken have sine egne faste maskiner?"/>
|
||||||
|
<text name="spu_guide_field_hint_store" text="Hvor skal det høstede/slåede hen?"/>
|
||||||
|
<text name="spu_guide_field_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||||
|
<text name="spu_guide_crop" text="Afgrøde"/>
|
||||||
|
<text name="spu_guide_crop_keep" text="Behold det der gror"/>
|
||||||
|
<text name="spu_guide_crop_sowAgain" text="Så samme afgrøde igen efter høst"/>
|
||||||
|
<text name="spu_guide_prep" text="Klargøring"/>
|
||||||
|
<text name="spu_guide_prep_autoText" text=" Pløjer når jorden trænger, ellers kultiverer; kalker ved lav pH."/>
|
||||||
|
<text name="spu_guide_care" text="Pleje"/>
|
||||||
|
<text name="spu_guide_care_autoText" text=" Gøder op til max når afgrøden vokser, sprøjter/harver ukrudt væk og kalker ved lav pH."/>
|
||||||
|
<text name="spu_guide_rigs" text="Maskiner ad gangen (multi-tool)"/>
|
||||||
|
<text name="spu_guide_dedicated" text="Faste maskiner til marken?"/>
|
||||||
|
<text name="spu_guide_useFieldSilo" text="Brug marksiloen?"/>
|
||||||
|
<text name="spu_guide_store" text="Lager"/>
|
||||||
|
<text name="spu_guide_store_autoText" text=" Nærmeste silo med plads (ensilage: gæringssilo)."/>
|
||||||
|
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||||
|
<text name="spu_guide_sum_sowAgain" text="så igen"/>
|
||||||
|
<text name="spu_guide_sum_rake" text="river"/>
|
||||||
|
<text name="spu_guide_sum_noRake" text="uden river"/>
|
||||||
|
<text name="spu_guide_sum_straw_chop" text="halm hakkes"/>
|
||||||
|
<text name="spu_guide_sum_straw_collect" text="halm samles"/>
|
||||||
|
<text name="spu_guide_sum_straw_bale" text="halm presses"/>
|
||||||
|
<text name="spu_guide_sum_auto" text="Automatisk"/>
|
||||||
|
<text name="spu_guide_sum_lime" text="kalk"/>
|
||||||
|
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||||
|
<text name="spu_guide_sum_roll" text="tromle"/>
|
||||||
|
<text name="spu_guide_sum_fert" text="gødning"/>
|
||||||
|
<text name="spu_guide_sum_weed" text="ukrudt"/>
|
||||||
|
<text name="spu_guide_sum_noCare" text="ingen pleje"/>
|
||||||
|
<text name="spu_guide_sum_dedicated" text="%d faste"/>
|
||||||
|
<text name="spu_guide_sum_freeRigs" text="ledige maskiner"/>
|
||||||
|
<text name="spu_guide_sum_rigs" text="%d ad gangen"/>
|
||||||
|
<text name="spu_guide_sum_unloaders" text="%d aflæsningsvogne"/>
|
||||||
|
<text name="spu_guide_sum_fieldSilo" text="Marksilo"/>
|
||||||
|
<text name="spu_guide_sum_stores" text="%d valgte siloer"/>
|
||||||
|
<text name="spu_guide_flow_open" text="Opsæt flow"/>
|
||||||
|
<text name="spu_guide_flow_title" text="— Opsæt flow: %s —"/>
|
||||||
|
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||||
|
<text name="spu_guide_flow_saved" text="Flow '%s' gemt"/>
|
||||||
|
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||||
|
<text name="spu_guide_flow_rename" text="Skift navn …"/>
|
||||||
|
<text name="spu_guide_flow_step_name" text="Navn"/>
|
||||||
|
<text name="spu_guide_flow_step_direction" text="Retning"/>
|
||||||
|
<text name="spu_guide_flow_step_from" text="Fra"/>
|
||||||
|
<text name="spu_guide_flow_step_to" text="Til"/>
|
||||||
|
<text name="spu_guide_flow_step_goods" text="Varer"/>
|
||||||
|
<text name="spu_guide_flow_step_store" text="Lager"/>
|
||||||
|
<text name="spu_guide_flow_step_rules" text="Regler"/>
|
||||||
|
<text name="spu_guide_flow_step_wait" text="Vent ved"/>
|
||||||
|
<text name="spu_guide_flow_step_tractors" text="Traktorer"/>
|
||||||
|
<text name="spu_guide_flow_step_summary" text="Oversigt"/>
|
||||||
|
<text name="spu_guide_flow_hint_name" text="Flowets navn (traktorerne får navn efter det)."/>
|
||||||
|
<text name="spu_guide_flow_hint_direction" text="Ind til bygning: fyld stalde/fabrikker. Ud fra bygning: tøm, lagr og sælg."/>
|
||||||
|
<text name="spu_guide_flow_hint_buildings_in" text="Hvilke stalde/fabrikker skal fyldes? Markøren findes automatisk."/>
|
||||||
|
<text name="spu_guide_flow_hint_buildings_out" text="Hvor skal der hentes fra? Markøren findes automatisk."/>
|
||||||
|
<text name="spu_guide_flow_hint_goods_in" text="Automatisk henter hver tur den vare målet mangler mest."/>
|
||||||
|
<text name="spu_guide_flow_hint_goods_out" text="For hver vare: Lager, Salg, eller Salg ellers lager."/>
|
||||||
|
<text name="spu_guide_flow_hint_store" text="Hvor skal lagervarerne hen?"/>
|
||||||
|
<text name="spu_guide_flow_hint_rules_in" text="Antal ture, og hvornår traktoren kører hjem fra Wait."/>
|
||||||
|
<text name="spu_guide_flow_hint_rules_out" text="Hvornår der sælges, og hvornår der køres til lager."/>
|
||||||
|
<text name="spu_guide_flow_hint_wait" text="Hvor venter traktoren med et rest-læs?"/>
|
||||||
|
<text name="spu_guide_flow_hint_tractors" text="Hvilke traktorer kører flowet? Traktorer i andre flows vises ikke."/>
|
||||||
|
<text name="spu_guide_flow_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
|
||||||
|
<text name="spu_guide_flow_goods" text="Varer"/>
|
||||||
|
<text name="spu_guide_flow_goodsAutoChoice" text="Automatisk (det bygningen mangler)"/>
|
||||||
|
<text name="spu_guide_flow_goodsAutoText" text=" Hver tur hentes den vare målet mangler mest."/>
|
||||||
|
<text name="spu_guide_flow_noGoodsTo" text=" Vælg først hvor der skal køres til (trinnet Til)"/>
|
||||||
|
<text name="spu_guide_flow_noProducts" text=" Vælg først hvor der hentes fra (trinnet Fra)"/>
|
||||||
|
<text name="spu_guide_flow_storeAutoChoice" text="Automatisk (nærmeste med plads)"/>
|
||||||
|
<text name="spu_guide_flow_storeAutoText" text=" Lagerturene kører til nærmeste lager med plads."/>
|
||||||
|
<text name="spu_guide_flow_noTractors" text=" Ingen ledige traktorer"/>
|
||||||
|
<text name="spu_guide_flow_otherFlows" text=" %d traktorer kører i andre flows og vises ikke"/>
|
||||||
|
<text name="spu_guide_flow_blockedShow" text=" %d traktorer kan ikke bruges her (vognen passer ikke) — klik for at se hvorfor"/>
|
||||||
|
<text name="spu_guide_flow_blockedHide" text=" %d traktorer kan ikke bruges her — klik for at skjule"/>
|
||||||
|
<text name="spu_guide_flow_blocked" text=" %d traktorer kan ikke bruges her (vognen passer ikke)"/>
|
||||||
|
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
||||||
|
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
||||||
|
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumRound" text="samlerunde"/>
|
||||||
|
<text name="spu_guide_flow_sumLoops" text="%d ture"/>
|
||||||
|
<text name="spu_guide_flow_sumLoopsInf" text="uendelige ture"/>
|
||||||
|
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
|
||||||
|
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
|
||||||
|
<text name="spu_tab_fieldsilos" text="Marksiloer"/>
|
||||||
|
<text name="spu_fs_enabled" text="Hent marksiloer hjem automatisk"/>
|
||||||
|
<text name="spu_fs_maxVehicles" text="Maks. vogne ude"/>
|
||||||
|
<text name="spu_fs_minLiters" text="Mindste mængde pr. vare"/>
|
||||||
|
<text name="spu_fs_status" text="%d/%d vogne ude · %d siloer venter"/>
|
||||||
|
<text name="spu_fs_noStores" text="Vælg mindst ét gårdlager nedenfor — ellers køres intet hjem"/>
|
||||||
|
<text name="spu_fs_silosTitle" text="Marksiloer med varer"/>
|
||||||
|
<text name="spu_fs_noVehicle" text="Ingen ledig tømmevogn: %d uden AD-parkeringspunkt, %d optaget, %d med spiller i, %d ikke i flåden (vogne i flows lånes ikke)"/>
|
||||||
|
<text name="spu_fs_nothing" text="Ingen marksiloer har varer over mindste mængde"/>
|
||||||
|
<text name="spu_fs_col_silo" text="Silo"/>
|
||||||
|
<text name="spu_fs_col_liters" text="Liter"/>
|
||||||
|
<text name="spu_fs_col_onWay" text="På vej"/>
|
||||||
|
<text name="spu_fs_col_state" text="Status"/>
|
||||||
|
<text name="spu_fs_state_ok" text="ok"/>
|
||||||
|
<text name="spu_fs_state_noMarker" text="mangler markør"/>
|
||||||
|
<text name="spu_fs_state_noFarmStore" text="ingen gård tager %s"/>
|
||||||
|
<text name="spu_fs_state_paused" text="pause (kom ikke i gang)"/>
|
||||||
|
<text name="spu_fs_storesTitle" text="Gårdlagre (hvor varerne køres hjem til)"/>
|
||||||
|
<text name="spu_fs_toProduction" text="Kør overskud til produktion (foder til staldene først)"/>
|
||||||
|
<text name="spu_fs_reserveDays" text="Foderreserve: staldenes forbrug i"/>
|
||||||
|
<text name="spu_fs_days" text="%d døgn"/>
|
||||||
|
<text name="spu_fs_toFactory" text="→ fabrik"/>
|
||||||
|
<text name="spu_fs_harvestHeld" text="%d tømmevogn(e) holdes tilbage til høstere der arbejder"/>
|
||||||
|
<text name="spu_view_title" text="Vis"/>
|
||||||
|
<text name="spu_view_items" text="Varer"/>
|
||||||
|
<text name="spu_view_buildings" text="Bygninger"/>
|
||||||
|
<text name="spu_view_tooltip" text="Varer: samlet beholdning pr. vare (klik for at se hvor den ligger). Bygninger: tabel for én bygning."/>
|
||||||
|
<text name="spu_kpi_value" text="Lagerværdi nu"/>
|
||||||
|
<text name="spu_kpi_gain" text="Ekstra ved toppris"/>
|
||||||
|
<text name="spu_kpi_empty" text="Løber tør snart"/>
|
||||||
|
<text name="spu_kpi_full" text="Næsten fuldt"/>
|
||||||
|
<text name="spu_attention" text="— Kræver handling —"/>
|
||||||
|
<text name="spu_attention_none" text="Intet kræver handling lige nu"/>
|
||||||
|
<text name="spu_attention_hintFlow" text=" → klik: opret flow"/>
|
||||||
|
<text name="spu_attention_hintOpen" text=" → klik: åbn flow '%s'"/>
|
||||||
|
<text name="spu_attention_hintProduce" text=" → producér/køb"/>
|
||||||
|
<text name="spu_attention_produce" text="Et flow hjælper ikke — gården har kun %2$s %1$s i alt. Producér eller køb mere."/>
|
||||||
|
<text name="spu_attention_flowName" text="%s til %s"/>
|
||||||
|
<text name="spu_attention_hintSellFlow" text=" → klik: opret salgsflow"/>
|
||||||
|
<text name="spu_attention_sellFlowName" text="Sælg %s"/>
|
||||||
|
<text name="spu_alert_empty" text="%s løber tør om %s — gården har %s, bruger %s/døgn"/>
|
||||||
|
<text name="spu_alert_fullIn" text="%s er %d %% fuld — fuld om %s"/>
|
||||||
|
<text name="spu_alert_full" text="%s er %d %% fuld"/>
|
||||||
|
<text name="spu_alert_sell" text="Sælg %s nu: prisen er %d %% af årets top — %s"/>
|
||||||
|
<text name="spu_items_hint" text="(klik på en vare)"/>
|
||||||
|
<text name="spu_icol_amount" text="Beholdning"/>
|
||||||
|
<text name="spu_icol_value" text="Værdi nu"/>
|
||||||
|
<text name="spu_icol_rate" text="Gården netto · rækker"/>
|
||||||
|
<text name="spu_item_perDay" text="%s/døgn"/>
|
||||||
|
<text name="spu_item_fullIn" text="fuld om %s"/>
|
||||||
|
<text name="spu_item_balanced" text="i balance"/>
|
||||||
|
<text name="spu_item_share" text="%d %% af varen"/>
|
||||||
|
<text name="spu_tab_chains" text="Kæder"/>
|
||||||
|
<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_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>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<text name="spu_title" text="Smart Pickup"/>
|
<text name="spu_title" text="Smart Pickup"/>
|
||||||
<text name="spu_run" text="Run"/>
|
<text name="spu_run" text="Run"/>
|
||||||
<text name="spu_run_tooltip" text="Select the run to view or edit."/>
|
<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" text="Target"/>
|
||||||
<text name="spu_target_tooltip" text="AutoDrive marker at the barn or factory to keep filled."/>
|
<text name="spu_target_tooltip" text="AutoDrive marker at the barn or factory to keep filled."/>
|
||||||
<text name="spu_noTarget" text="(choose target)"/>
|
<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_started" text="Started %d, skipped %d"/>
|
||||||
<text name="spu_status_noAd" text="AutoDrive API not found"/>
|
<text name="spu_status_noAd" text="AutoDrive API not found"/>
|
||||||
<text name="spu_status_error" text="Error: %s"/>
|
<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_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_farFromNetwork" text="too far from the road network"/>
|
||||||
<text name="spu_reason_noTarget" text="no target"/>
|
<text name="spu_reason_noTarget" text="no target"/>
|
||||||
@@ -40,7 +43,28 @@
|
|||||||
<text name="spu_status_stoppedAll" text="Stopped %d tractors"/>
|
<text name="spu_status_stoppedAll" text="Stopped %d tractors"/>
|
||||||
<text name="spu_overview_hint" text="Click a flow to edit it"/>
|
<text name="spu_overview_hint" text="Click a flow to edit it"/>
|
||||||
<text name="spu_tab_fields" text="Fields"/>
|
<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_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_button_stop" text="Stop field"/>
|
||||||
<text name="spu_field_noMarker" text="! missing marker F%d"/>
|
<text name="spu_field_noMarker" text="! missing marker F%d"/>
|
||||||
<text name="spu_field_stage_idle" text="Waiting"/>
|
<text name="spu_field_stage_idle" text="Waiting"/>
|
||||||
@@ -51,8 +75,45 @@
|
|||||||
<text name="spu_field_stage_collecting" text="Collecting"/>
|
<text name="spu_field_stage_collecting" text="Collecting"/>
|
||||||
<text name="spu_field_stage_done" text="Done"/>
|
<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_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_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_tooWide" text="%s too wide: %.1f m > %.0f m (Courseplay)"/>
|
||||||
|
<text name="spu_field_reason_widthUnknown" text="%s: work width %.1f m cannot be right — set the width in Courseplay (not sent out)"/>
|
||||||
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m — turn on Rake or use a wider one"/>
|
<text name="spu_field_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_mower" text="mower"/>
|
||||||
<text name="spu_field_role_tedder" text="tedder"/>
|
<text name="spu_field_role_tedder" text="tedder"/>
|
||||||
@@ -65,25 +126,33 @@
|
|||||||
<text name="spu_field_mode_manual" text="Manual"/>
|
<text name="spu_field_mode_manual" text="Manual"/>
|
||||||
<text name="spu_field_notMowable" text="not grass/clover"/>
|
<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_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_field_showAll" text="Show all fields (%d hidden)"/>
|
||||||
<text name="spu_col_field" text="Field"/>
|
<text name="spu_col_field" text="Field"/>
|
||||||
<text name="spu_col_crop" text="Crop"/>
|
<text name="spu_col_crop" text="Crop"/>
|
||||||
<text name="spu_col_plan" text="Plan"/>
|
<text name="spu_col_plan" text="Plan"/>
|
||||||
<text name="spu_col_mode" text="Mode"/>
|
<text name="spu_col_mode" text="Mode"/>
|
||||||
<text name="spu_field_section" text="— Field F%d: %s —"/>
|
<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_plan" text="Plan"/>
|
||||||
<text name="spu_field_mode" text="Mode"/>
|
<text name="spu_field_mode" text="Mode"/>
|
||||||
<text name="spu_field_rake" text="Rake the swaths before collecting (lets narrow loading wagons collect)"/>
|
<text name="spu_field_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_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_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_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_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_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_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_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
|
||||||
<text name="spu_field_section_fleet" text="— Field fleet (ticked = may be sent out) —"/>
|
<text name="spu_field_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
||||||
|
<text name="spu_field_rig_working" text="working on F%s"/>
|
||||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||||
|
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
||||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
<text name="spu_field_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_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_field_startRequested" text="F%d starts when a machine is free"/>
|
||||||
@@ -94,9 +163,11 @@
|
|||||||
<text name="spu_field_fermenter" text="fermenting silo"/>
|
<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_storeAutoSilage" text="none chosen: nearest fermenting silo with room, else nearest silo"/>
|
||||||
<text name="spu_field_bare" text="bare soil"/>
|
<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_careNeed" text="needs %s"/>
|
||||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
<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_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_mulch" text="Mulch stubble after harvest"/>
|
||||||
<text name="spu_field_need_fert" text="fertilizer"/>
|
<text name="spu_field_need_fert" text="fertilizer"/>
|
||||||
<text name="spu_field_need_lime" text="lime"/>
|
<text name="spu_field_need_lime" text="lime"/>
|
||||||
@@ -116,7 +187,7 @@
|
|||||||
<text name="spu_field_rig_seeds" text="seeds ok"/>
|
<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_rigs" text="Machines per step (Courseplay multi-tool)"/>
|
||||||
<text name="spu_field_role_cultivator" text="cultivator"/>
|
<text name="spu_field_role_cultivator" text="cultivator"/>
|
||||||
<text name="spu_field_role_fertilizer" text="fertilizer spreader"/>
|
<text name="spu_field_role_fertilizer" text="fertilizing"/>
|
||||||
<text name="spu_field_role_harvester" text="harvester"/>
|
<text name="spu_field_role_harvester" text="harvester"/>
|
||||||
<text name="spu_field_role_limer" text="lime spreader"/>
|
<text name="spu_field_role_limer" text="lime spreader"/>
|
||||||
<text name="spu_field_role_mulcher" text="mulcher"/>
|
<text name="spu_field_role_mulcher" text="mulcher"/>
|
||||||
@@ -152,6 +223,7 @@
|
|||||||
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
||||||
<text name="spu_field_weed" text="Control weeds"/>
|
<text name="spu_field_weed" text="Control weeds"/>
|
||||||
<text name="spu_field_notice_done" text="is done"/>
|
<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_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" 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_none" text="None — plan it yourself"/>
|
||||||
@@ -169,13 +241,35 @@
|
|||||||
<text name="spu_field_straw_chop" text="Chopped (spread on the field)"/>
|
<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_collect" text="Collected to a silo"/>
|
||||||
<text name="spu_field_straw_bale" text="Baled and collected"/>
|
<text name="spu_field_straw_bale" text="Baled and collected"/>
|
||||||
<text name="spu_button_start" text="Start run"/>
|
<text name="spu_button_start" text="Start flow"/>
|
||||||
<text name="spu_button_stop" text="Stop run"/>
|
<text name="spu_button_stop" text="Stop flow"/>
|
||||||
<text name="spu_button_new" text="New run"/>
|
<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_rename" text="Rename"/>
|
||||||
<text name="spu_button_delete" text="Delete run"/>
|
<text name="spu_button_delete" text="Delete flow"/>
|
||||||
<text name="spu_newRunName" text="Run %d"/>
|
<text name="spu_panel_close" text="Close"/>
|
||||||
<text name="spu_renameTitle" text="Run name"/>
|
<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_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" text="%s l"/>
|
||||||
<text name="spu_capacity_pallets" text="autoload (pallets)"/>
|
<text name="spu_capacity_pallets" text="autoload (pallets)"/>
|
||||||
@@ -219,6 +313,8 @@
|
|||||||
<text name="spu_sellAt" text="Sell from (% of yearly top 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_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_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_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" 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."/>
|
<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."/>
|
||||||
@@ -228,6 +324,7 @@
|
|||||||
<text name="spu_reason_noProducts" text="no good selected (set Store/Sell on a good above)"/>
|
<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_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_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_reason_needsAutoload" text="pallet goods need Universal Autoload on the trailer"/>
|
||||||
<text name="spu_product_row" text="%s — %s l — %s"/>
|
<text name="spu_product_row" text="%s — %s l — %s"/>
|
||||||
<text name="spu_price_pct" text="%d %% of top (%s)"/>
|
<text name="spu_price_pct" text="%d %% of top (%s)"/>
|
||||||
@@ -253,14 +350,16 @@
|
|||||||
<text name="spu_flow_noSource" text="(no source)"/>
|
<text name="spu_flow_noSource" text="(no source)"/>
|
||||||
<text name="spu_flow_noTarget" text="(no target)"/>
|
<text name="spu_flow_noTarget" text="(no target)"/>
|
||||||
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
||||||
|
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
||||||
|
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
||||||
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
||||||
<text name="spu_flow_round" text="round"/>
|
<text name="spu_flow_round" text="round"/>
|
||||||
<text name="spu_flow_ruleIn" text="need"/>
|
<text name="spu_flow_ruleIn" text="by need — lowest first"/>
|
||||||
<text name="spu_flow_ruleInLoops" text="need, %d loops"/>
|
<text name="spu_flow_ruleInLoops" text="by need, %d trips"/>
|
||||||
<text name="spu_markerAt" text="%s %d m"/>
|
<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_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_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_reason_noMarker" text="'%s' has no AD marker at its trigger"/>
|
||||||
<text name="spu_storage_select" text="Show building"/>
|
<text name="spu_storage_select" text="Show building"/>
|
||||||
<text name="spu_stat_now" text="now %s"/>
|
<text name="spu_stat_now" text="now %s"/>
|
||||||
@@ -300,5 +399,245 @@
|
|||||||
<text name="spu_col_lasts" text="Lasts"/>
|
<text name="spu_col_lasts" text="Lasts"/>
|
||||||
<text name="spu_col_rate" text="Use/prod. per day"/>
|
<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_storage_select_tooltip" text="Choose the building shown in the table."/>
|
||||||
|
<text name="spu_guide_next" text="Next"/>
|
||||||
|
<text name="spu_guide_save" text="Save"/>
|
||||||
|
<text name="spu_guide_ok" text="OK"/>
|
||||||
|
<text name="spu_guide_back" text="Back"/>
|
||||||
|
<text name="spu_guide_cancel" text="Cancel"/>
|
||||||
|
<text name="spu_guide_saveOnly" text="Save without starting"/>
|
||||||
|
<text name="spu_guide_runAuto" text="Run automatically"/>
|
||||||
|
<text name="spu_guide_stepOf" text="Step %d of %d — %s"/>
|
||||||
|
<text name="spu_guide_stepOne" text="Step %d — %s"/>
|
||||||
|
<text name="spu_guide_editStep" text="Edit: %s"/>
|
||||||
|
<text name="spu_guide_editSuffix" text=" [Edit]"/>
|
||||||
|
<text name="spu_guide_sum_hint" text="Click a line to change just that."/>
|
||||||
|
<text name="spu_guide_auto" text="Automatic (recommended)"/>
|
||||||
|
<text name="spu_guide_manual" text="Choose myself"/>
|
||||||
|
<text name="spu_guide_closed" text="Guide closed — the field/flow no longer exists"/>
|
||||||
|
<text name="spu_guide_cannotAdvance" text="Choose first"/>
|
||||||
|
<text name="spu_guide_pickField" text="Pick a field first"/>
|
||||||
|
<text name="spu_guide_saved" text="F%d saved"/>
|
||||||
|
<text name="spu_guide_noMarker" text="! Place an AD marker at F%d — work waits for the marker"/>
|
||||||
|
<text name="spu_guide_field_open" text="Set up field"/>
|
||||||
|
<text name="spu_guide_fleet_open" text="Field fleet"/>
|
||||||
|
<text name="spu_guide_fleet_close" text="Back to fields"/>
|
||||||
|
<text name="spu_guide_field_title" text="— Set up field F%d —"/>
|
||||||
|
<text name="spu_guide_field_titleNone" text="— Set up field —"/>
|
||||||
|
<text name="spu_guide_field_step_field" text="Field"/>
|
||||||
|
<text name="spu_guide_field_step_crop" text="Crop"/>
|
||||||
|
<text name="spu_guide_field_step_cropChoice" text="Crop options"/>
|
||||||
|
<text name="spu_guide_field_step_prep" text="Preparation"/>
|
||||||
|
<text name="spu_guide_field_step_care" text="Crop care"/>
|
||||||
|
<text name="spu_guide_field_step_machines" text="Machines"/>
|
||||||
|
<text name="spu_guide_field_step_store" text="Storage"/>
|
||||||
|
<text name="spu_guide_field_step_summary" text="Summary"/>
|
||||||
|
<text name="spu_guide_field_hint_field" text="Pick the field you want to set up."/>
|
||||||
|
<text name="spu_guide_field_hint_crop" text="What should grow here? 'Keep' carries on with the current crop."/>
|
||||||
|
<text name="spu_guide_field_hint_cropChoice" text="What happens to the crop when mowing or harvesting?"/>
|
||||||
|
<text name="spu_guide_field_hint_prep" text="After harvest / on bare land. Automatic plows or cultivates and limes as needed."/>
|
||||||
|
<text name="spu_guide_field_hint_care" text="While the crop grows. Fertilizing/spraying only happens when needed."/>
|
||||||
|
<text name="spu_guide_field_hint_machines" text="How many at a time, and should the field have its own dedicated machines?"/>
|
||||||
|
<text name="spu_guide_field_hint_store" text="Where should the harvest go?"/>
|
||||||
|
<text name="spu_guide_field_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||||
|
<text name="spu_guide_crop" text="Crop"/>
|
||||||
|
<text name="spu_guide_crop_keep" text="Keep what grows"/>
|
||||||
|
<text name="spu_guide_crop_sowAgain" text="Sow the same crop again after harvest"/>
|
||||||
|
<text name="spu_guide_prep" text="Preparation"/>
|
||||||
|
<text name="spu_guide_prep_autoText" text=" Plows when the soil needs it, otherwise cultivates; limes at low pH."/>
|
||||||
|
<text name="spu_guide_care" text="Crop care"/>
|
||||||
|
<text name="spu_guide_care_autoText" text=" Fertilizes up to max while the crop grows, removes weeds and limes at low pH."/>
|
||||||
|
<text name="spu_guide_rigs" text="Machines at a time (multi-tool)"/>
|
||||||
|
<text name="spu_guide_dedicated" text="Dedicated machines for this field?"/>
|
||||||
|
<text name="spu_guide_useFieldSilo" text="Use the field silo?"/>
|
||||||
|
<text name="spu_guide_store" text="Storage"/>
|
||||||
|
<text name="spu_guide_store_autoText" text=" Nearest silo with room (silage: fermenting silo)."/>
|
||||||
|
<text name="spu_guide_sum_target" text="-> %s"/>
|
||||||
|
<text name="spu_guide_sum_sowAgain" text="sow again"/>
|
||||||
|
<text name="spu_guide_sum_rake" text="rake"/>
|
||||||
|
<text name="spu_guide_sum_noRake" text="no rake"/>
|
||||||
|
<text name="spu_guide_sum_straw_chop" text="straw chopped"/>
|
||||||
|
<text name="spu_guide_sum_straw_collect" text="straw collected"/>
|
||||||
|
<text name="spu_guide_sum_straw_bale" text="straw baled"/>
|
||||||
|
<text name="spu_guide_sum_auto" text="Automatic"/>
|
||||||
|
<text name="spu_guide_sum_lime" text="lime"/>
|
||||||
|
<text name="spu_guide_sum_mulch" text="mulch"/>
|
||||||
|
<text name="spu_guide_sum_roll" text="roll"/>
|
||||||
|
<text name="spu_guide_sum_fert" text="fertilizer"/>
|
||||||
|
<text name="spu_guide_sum_weed" text="weeds"/>
|
||||||
|
<text name="spu_guide_sum_noCare" text="no care"/>
|
||||||
|
<text name="spu_guide_sum_dedicated" text="%d dedicated"/>
|
||||||
|
<text name="spu_guide_sum_freeRigs" text="free machines"/>
|
||||||
|
<text name="spu_guide_sum_rigs" text="%d at a time"/>
|
||||||
|
<text name="spu_guide_sum_unloaders" text="%d unloaders"/>
|
||||||
|
<text name="spu_guide_sum_fieldSilo" text="Field silo"/>
|
||||||
|
<text name="spu_guide_sum_stores" text="%d chosen silos"/>
|
||||||
|
<text name="spu_guide_flow_open" text="Set up flow"/>
|
||||||
|
<text name="spu_guide_flow_title" text="— Set up flow: %s —"/>
|
||||||
|
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
|
||||||
|
<text name="spu_guide_flow_saved" text="Flow '%s' saved"/>
|
||||||
|
<text name="spu_guide_flow_start" text="Start flow"/>
|
||||||
|
<text name="spu_guide_flow_rename" text="Change name …"/>
|
||||||
|
<text name="spu_guide_flow_step_name" text="Name"/>
|
||||||
|
<text name="spu_guide_flow_step_direction" text="Direction"/>
|
||||||
|
<text name="spu_guide_flow_step_from" text="From"/>
|
||||||
|
<text name="spu_guide_flow_step_to" text="To"/>
|
||||||
|
<text name="spu_guide_flow_step_goods" text="Goods"/>
|
||||||
|
<text name="spu_guide_flow_step_store" text="Storage"/>
|
||||||
|
<text name="spu_guide_flow_step_rules" text="Rules"/>
|
||||||
|
<text name="spu_guide_flow_step_wait" text="Wait at"/>
|
||||||
|
<text name="spu_guide_flow_step_tractors" text="Tractors"/>
|
||||||
|
<text name="spu_guide_flow_step_summary" text="Summary"/>
|
||||||
|
<text name="spu_guide_flow_hint_name" text="The flow's name (tractors are named after it)."/>
|
||||||
|
<text name="spu_guide_flow_hint_direction" text="Into building: keep barns/factories filled. Out of building: empty, store and sell."/>
|
||||||
|
<text name="spu_guide_flow_hint_buildings_in" text="Which barns/factories should be filled? The marker is found automatically."/>
|
||||||
|
<text name="spu_guide_flow_hint_buildings_out" text="Where should goods be picked up? The marker is found automatically."/>
|
||||||
|
<text name="spu_guide_flow_hint_goods_in" text="Automatic fetches whatever the target lacks most on each trip."/>
|
||||||
|
<text name="spu_guide_flow_hint_goods_out" text="For each good: Store, Sell, or Sell else store."/>
|
||||||
|
<text name="spu_guide_flow_hint_store" text="Where should stored goods go?"/>
|
||||||
|
<text name="spu_guide_flow_hint_rules_in" text="Number of trips, and when the tractor leaves Wait."/>
|
||||||
|
<text name="spu_guide_flow_hint_rules_out" text="When to sell, and when to drive to storage."/>
|
||||||
|
<text name="spu_guide_flow_hint_wait" text="Where does the tractor wait with a leftover load?"/>
|
||||||
|
<text name="spu_guide_flow_hint_tractors" text="Which tractors run this flow? Tractors in other flows are hidden."/>
|
||||||
|
<text name="spu_guide_flow_hint_summary" text="Check your choices. Click a line to change it."/>
|
||||||
|
<text name="spu_guide_flow_goods" text="Goods"/>
|
||||||
|
<text name="spu_guide_flow_goodsAutoChoice" text="Automatic (what the building needs)"/>
|
||||||
|
<text name="spu_guide_flow_goodsAutoText" text=" Each trip fetches the good the target lacks most."/>
|
||||||
|
<text name="spu_guide_flow_noGoodsTo" text=" First choose where to deliver (step To)"/>
|
||||||
|
<text name="spu_guide_flow_noProducts" text=" First choose where to pick up (step From)"/>
|
||||||
|
<text name="spu_guide_flow_storeAutoChoice" text="Automatic (nearest with room)"/>
|
||||||
|
<text name="spu_guide_flow_storeAutoText" text=" Storage trips go to the nearest store with room."/>
|
||||||
|
<text name="spu_guide_flow_noTractors" text=" No free tractors"/>
|
||||||
|
<text name="spu_guide_flow_otherFlows" text=" %d tractors run in other flows and are hidden"/>
|
||||||
|
<text name="spu_guide_flow_blockedShow" text=" %d tractors cannot be used here (trailer does not fit) — click to see why"/>
|
||||||
|
<text name="spu_guide_flow_blockedHide" text=" %d tractors cannot be used here — click to hide"/>
|
||||||
|
<text name="spu_guide_flow_blocked" text=" %d tractors cannot be used here (trailer does not fit)"/>
|
||||||
|
<text name="spu_guide_flow_none" text="none chosen"/>
|
||||||
|
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
||||||
|
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumRound" text="collection round"/>
|
||||||
|
<text name="spu_guide_flow_sumLoops" text="%d trips"/>
|
||||||
|
<text name="spu_guide_flow_sumLoopsInf" text="endless trips"/>
|
||||||
|
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
|
||||||
|
<text name="spu_guide_flow_sumNoTractors" text="none"/>
|
||||||
|
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
|
||||||
|
<text name="spu_tab_fieldsilos" text="Field silos"/>
|
||||||
|
<text name="spu_fs_enabled" text="Bring field silos home automatically"/>
|
||||||
|
<text name="spu_fs_maxVehicles" text="Max. vehicles out"/>
|
||||||
|
<text name="spu_fs_minLiters" text="Minimum amount per product"/>
|
||||||
|
<text name="spu_fs_status" text="%d/%d vehicles out · %d silos waiting"/>
|
||||||
|
<text name="spu_fs_noStores" text="Select at least one farm storage below — otherwise nothing is brought home"/>
|
||||||
|
<text name="spu_fs_silosTitle" text="Field silos with products"/>
|
||||||
|
<text name="spu_fs_noVehicle" text="No free unloading trailer: %d without AD park destination, %d busy, %d with a player in it, %d not in the fleet (flow vehicles are never borrowed)"/>
|
||||||
|
<text name="spu_fs_nothing" text="No field silo has products above the minimum amount"/>
|
||||||
|
<text name="spu_fs_col_silo" text="Silo"/>
|
||||||
|
<text name="spu_fs_col_liters" text="Liters"/>
|
||||||
|
<text name="spu_fs_col_onWay" text="En route"/>
|
||||||
|
<text name="spu_fs_col_state" text="Status"/>
|
||||||
|
<text name="spu_fs_state_ok" text="ok"/>
|
||||||
|
<text name="spu_fs_state_noMarker" text="missing marker"/>
|
||||||
|
<text name="spu_fs_state_noFarmStore" text="no farm storage takes %s"/>
|
||||||
|
<text name="spu_fs_state_paused" text="paused (did not get going)"/>
|
||||||
|
<text name="spu_fs_storesTitle" text="Farm storages (where products are brought home to)"/>
|
||||||
|
<text name="spu_fs_toProduction" text="Send surplus to production (barn feed first)"/>
|
||||||
|
<text name="spu_fs_reserveDays" text="Feed reserve: barn consumption for"/>
|
||||||
|
<text name="spu_fs_days" text="%d days"/>
|
||||||
|
<text name="spu_fs_toFactory" text="→ factory"/>
|
||||||
|
<text name="spu_fs_harvestHeld" text="%d unloading trailer(s) held back for working harvesters"/>
|
||||||
|
<text name="spu_view_title" text="Show"/>
|
||||||
|
<text name="spu_view_items" text="Goods"/>
|
||||||
|
<text name="spu_view_buildings" text="Buildings"/>
|
||||||
|
<text name="spu_view_tooltip" text="Goods: total stock per good (click to see where it is stored). Buildings: table for one building."/>
|
||||||
|
<text name="spu_kpi_value" text="Stock value now"/>
|
||||||
|
<text name="spu_kpi_gain" text="Extra at top price"/>
|
||||||
|
<text name="spu_kpi_empty" text="Running out soon"/>
|
||||||
|
<text name="spu_kpi_full" text="Almost full"/>
|
||||||
|
<text name="spu_attention" text="— Needs attention —"/>
|
||||||
|
<text name="spu_attention_none" text="Nothing needs attention right now"/>
|
||||||
|
<text name="spu_attention_hintFlow" text=" → click: create flow"/>
|
||||||
|
<text name="spu_attention_hintOpen" text=" → click: open flow '%s'"/>
|
||||||
|
<text name="spu_attention_hintProduce" text=" → produce/buy"/>
|
||||||
|
<text name="spu_attention_produce" text="A flow won't help — the farm only has %2$s %1$s in total. Produce or buy more."/>
|
||||||
|
<text name="spu_attention_flowName" text="%s to %s"/>
|
||||||
|
<text name="spu_attention_hintSellFlow" text=" → click: create sell flow"/>
|
||||||
|
<text name="spu_attention_sellFlowName" text="Sell %s"/>
|
||||||
|
<text name="spu_alert_empty" text="%s runs out in %s — farm has %s, uses %s/day"/>
|
||||||
|
<text name="spu_alert_fullIn" text="%s is %d %% full — full in %s"/>
|
||||||
|
<text name="spu_alert_full" text="%s is %d %% full"/>
|
||||||
|
<text name="spu_alert_sell" text="Sell %s now: price is %d %% of the yearly top — %s"/>
|
||||||
|
<text name="spu_items_hint" text="(click a good)"/>
|
||||||
|
<text name="spu_icol_amount" text="Stock"/>
|
||||||
|
<text name="spu_icol_value" text="Value now"/>
|
||||||
|
<text name="spu_icol_rate" text="Farm net · lasts"/>
|
||||||
|
<text name="spu_item_perDay" text="%s/day"/>
|
||||||
|
<text name="spu_item_fullIn" text="full in %s"/>
|
||||||
|
<text name="spu_item_balanced" text="balanced"/>
|
||||||
|
<text name="spu_item_share" text="%d %% of the good"/>
|
||||||
|
<text name="spu_tab_chains" text="Chains"/>
|
||||||
|
<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_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>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.27.0.0</version>
|
<version>1.61.0.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
<multiplayer supported="true"/>
|
<multiplayer supported="true"/>
|
||||||
<l10n filenamePrefix="l10n/l10n"/>
|
<l10n filenamePrefix="l10n/l10n"/>
|
||||||
<extraSourceFiles>
|
<extraSourceFiles>
|
||||||
|
<sourceFile filename="adCache.lua"/>
|
||||||
|
<sourceFile filename="adNaming.lua"/>
|
||||||
|
<sourceFile filename="adNames.lua"/>
|
||||||
|
<sourceFile filename="adStuckDiag.lua"/>
|
||||||
<sourceFile filename="adSupplyPlanner.lua"/>
|
<sourceFile filename="adSupplyPlanner.lua"/>
|
||||||
<sourceFile filename="adRuns.lua"/>
|
<sourceFile filename="adRuns.lua"/>
|
||||||
<sourceFile filename="adRunsStorage.lua"/>
|
<sourceFile filename="adRunsStorage.lua"/>
|
||||||
@@ -22,18 +26,54 @@
|
|||||||
<sourceFile filename="adOutboundPlanner.lua"/>
|
<sourceFile filename="adOutboundPlanner.lua"/>
|
||||||
<sourceFile filename="adStorageStats.lua"/>
|
<sourceFile filename="adStorageStats.lua"/>
|
||||||
<sourceFile filename="adFlowView.lua"/>
|
<sourceFile filename="adFlowView.lua"/>
|
||||||
|
<sourceFile filename="adGuide.lua"/>
|
||||||
|
<sourceFile filename="adFlowGuide.lua"/>
|
||||||
<sourceFile filename="adSources.lua"/>
|
<sourceFile filename="adSources.lua"/>
|
||||||
<sourceFile filename="adBuildings.lua"/>
|
<sourceFile filename="adBuildings.lua"/>
|
||||||
<sourceFile filename="adOutbound.lua"/>
|
<sourceFile filename="adOutbound.lua"/>
|
||||||
|
<sourceFile filename="adTipSide.lua"/>
|
||||||
<sourceFile filename="adUnloadWait.lua"/>
|
<sourceFile filename="adUnloadWait.lua"/>
|
||||||
<sourceFile filename="adFieldModel.lua"/>
|
<sourceFile filename="adFieldModel.lua"/>
|
||||||
<sourceFile filename="adFieldFleet.lua"/>
|
<sourceFile filename="adFieldFleet.lua"/>
|
||||||
<sourceFile filename="adFieldView.lua"/>
|
<sourceFile filename="adFieldView.lua"/>
|
||||||
|
<sourceFile filename="adFieldGuide.lua"/>
|
||||||
<sourceFile filename="adFields.lua"/>
|
<sourceFile filename="adFields.lua"/>
|
||||||
<sourceFile filename="adCpBridge.lua"/>
|
<sourceFile filename="adCpBridge.lua"/>
|
||||||
|
<sourceFile filename="adCourseCache.lua"/>
|
||||||
<sourceFile filename="adFieldStorage.lua"/>
|
<sourceFile filename="adFieldStorage.lua"/>
|
||||||
|
<sourceFile filename="adManualTransport.lua"/>
|
||||||
<sourceFile filename="adFieldWork.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="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"/>
|
<sourceFile filename="adSmartPickup.lua"/>
|
||||||
</extraSourceFiles>
|
</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>
|
</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
|
- **Forsyning:** holder stalde og fabrikker fyldt. Hver tur henter den vare, målet mangler mest, fra
|
||||||
valgte vare — mindst fyldte først (mælk/gylle: fyldeste først) — i stedet for at prøve markørerne én efter én.
|
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
|
1. Hent `FS25_ADSmartPickup.zip` under [Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases).
|
||||||
TMR-blander — i stedet for én traktor pr. vare.
|
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.
|
## Krav
|
||||||
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
|
- **FS25_AutoDrive** (påkrævet) og et AutoDrive-vejnet mellem bygningerne.
|
||||||
silo **hvor som helst på kortet** (eget lager før nabo-lån, mindst fyldte først). Ingredienser uden
|
- **Courseplay** anbefales. Det er nødvendigt for markarbejdet.
|
||||||
plads i målet (< 1000 l fri) eller uden kilde springes over. Mens AD kører viser HUD'en kun turens
|
- **Universal Autoload** anbefales på vogne, der skal køre paller.
|
||||||
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`).
|
## 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
|
## Feedback
|
||||||
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
|
Fejl og idéer: giv udvikleren besked (issues her kræver en konto på git.powerdata.dk). Vedlæg gerne `log.txt` fra
|
||||||
`ExtendedProductionPoint` slår produktioner fra automatisk, når de løber tør — dem skal vi netop
|
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
|
||||||
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)
|
## Udvikling
|
||||||
|
|
||||||
En fabriks læssestation udleverer også dens **inputs** (halm i en TMR-fabrik). Sådan en vare hentes
|
- Mod-koden ligger i `FS25_ADSmartPickup/`.
|
||||||
aldrig derfra — kun fabrikkens outputs og rigtige siloer er kilder. Gælder både mappe-logik og
|
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||||
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
|
## English summary
|
||||||
stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder.
|
|
||||||
|
|
||||||
### 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
|
**Features**
|
||||||
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
|
- **Supply:** keeps barns and factories filled. Each trip fetches the good the target lacks most, from
|
||||||
plads til; de er en fælles pulje for alle traktorer. Markørernes egne navne er ligegyldige, så
|
the best silo on the map.
|
||||||
dine mark-ventepladser ("F91 Wait" i mark-mapperne) rører den ikke.
|
- **Outbound:** empties factories, barns, silos and pallet sheds. Goods are stored or sold near the
|
||||||
2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt
|
yearly top price, and the mod never sells what the farm needs itself.
|
||||||
(modden holder styr på reservationer) og venter dér.
|
- **Flows:** a named recipe of buildings, goods, rules and tractors. You set flows up in a guide and
|
||||||
3. Når målet igen har plads til **hele** resten — eller er mindst **halvt tomt** (kæmpe vogne kan aldrig
|
start or stop them from a colour-coded list.
|
||||||
tømme hele resten i en lille stald) — kører den tilbage og læsser af. Derefter normal tur.
|
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
|
||||||
4. Wait-punkterne SKAL være forbundet til vejnettet både fra målets udkørsel og tilbage til målet.
|
marker and park it again. Detached implements and free tractors form a shared pool.
|
||||||
Modden tjekker ruten begge veje før den omdirigerer; uden rute prøves næste punkt (AutoDrive
|
- **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.
|
||||||
ville ellers vise "cannot reach" og slukke sig selv).
|
- **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.
|
**Install:** download `FS25_ADSmartPickup.zip` from
|
||||||
Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet.
|
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
|
||||||
Ingen ledige Wait-punkter → AutoDrive venter ved målet som i dag (én advarsel i loggen).
|
in `Documents/My Games/FarmingSimulator2025/mods/`. Enable the mod together with AutoDrive when you pick
|
||||||
Seks markører må gerne hedde det samme (fx `Cow Wait`); loggen viser markørnummeret.
|
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
|
**Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is
|
||||||
i målet, forud; passer intet fuldt læs, tages den mest trængende alligevel (og Wait-punktet bruges).
|
needed for pallets.
|
||||||
|
|
||||||
**Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*.
|
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
|
||||||
Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet,
|
[Changelog](CHANGELOG.md)
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
|
**Development:** the mod code is in `FS25_ADSmartPickup/`. Build the zip with
|
||||||
køre den. I stedet for at sætte AutoDrive op manuelt på hver traktor, defineres run'et ét sted og
|
`cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||||
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/`
|
|
||||||
|
|||||||
+920
-303
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
|||||||
# Testplan — Markarbejde (fanen Marker), version 1.27.0.0
|
|
||||||
|
|
||||||
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
|
||||||
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
|
||||||
begynder altid med `ADSmartPickup: F<marknr>`.
|
|
||||||
|
|
||||||
Kryds af undervejs. Skriv resultatet (OK / FEJL + hvad der skete) ud for hver test, så retter vi i
|
|
||||||
samme rækkefølge.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Forberedelse
|
|
||||||
|
|
||||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.0.0) FS25_ADSmartPickup`.
|
|
||||||
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
|
||||||
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
|
||||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
|
||||||
- [ ] **0.3 F-markører.** Marker uden `F<nr>`-markør står med `!` foran marknummeret og "! mangler
|
|
||||||
markør F…". Lav markører til de marker, der skal testes.
|
|
||||||
- [ ] **0.4 Markflåden.** Nederst: alle maskiner med slåmaskine, vender, river, læssevogn, høster,
|
|
||||||
aflæsningsvogn, kultivator, plov, såmaskine, tromle, strigle/sprøjte, gødnings-/kalkspreder,
|
|
||||||
stubhakker — med rolle og bredde. Tjek:
|
|
||||||
- [ ] Høstere viser "N afgrøder".
|
|
||||||
- [ ] Såmaskiner viser "frø ok" eller "!! tom for frø".
|
|
||||||
- [ ] Strigle viser "strigle (småt ukrudt)", ukrudtssprøjte viser "sprøjte".
|
|
||||||
- [ ] En **tom** sprøjte/spreder vises ikke (den har ingen rolle før den har fyld på).
|
|
||||||
- [ ] Maskiner i et Smart Pickup-flow kan godt ses, men sendes aldrig ud.
|
|
||||||
- [ ] **0.5 Bredder.** BiG M 450 viser **40,8 m** (Courseplays egen bredde), læssevognene 40,8 m.
|
|
||||||
|
|
||||||
## 1. Etape 1 genbekræftet (græs → ensilage)
|
|
||||||
|
|
||||||
Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set.
|
|
||||||
|
|
||||||
- [ ] **1.1 Afslutning af opsamling.** Mark 22 (eller en anden lille græsmark) på Manuel + Ensilage,
|
|
||||||
BiG M + én Boss Alpin med flueben → **Start nu**.
|
|
||||||
- Forvent: slåmaskine → F22 → Courseplay slår → hjem. Læssevogn → F22 → Courseplay samler op med
|
|
||||||
**slåmaskinens kursus** → AutoDrive tømmer ved fuld → tilbage → færdig → rest til lager → parkering.
|
|
||||||
Marken står **Færdig**, og der kommer en grøn besked "F22 er færdig".
|
|
||||||
- Log: `mower … sendt til F22`, `Courseplay startet (mower)`, `mowing -> collecting`,
|
|
||||||
`læssevogn … aflæsser i <gæringssilo>`, `Courseplay startet (collector, mower-kursus kopieret)`,
|
|
||||||
`collector … færdig`, `collecting -> done`.
|
|
||||||
- [ ] **1.2 Gæringssilo.** Ensilage-læs går til nærmeste **gæringssilo** (FermentingSilo/GrassSilageSilo),
|
|
||||||
ikke en Farma-silo. Log: `aflæsser i <gæringssilo>` eller `ingen gæringssilo kan tage græsset (…)`.
|
|
||||||
- [ ] **1.3 Sidde i maskinen.** Sæt dig i slåmaskinen mens Courseplay kører, og stig ud igen.
|
|
||||||
Forvent: den slippes IKKE (ingen `frigivet`-linje). Kør selv > 3 km/t uden CP → `frigivet — spilleren kører det`.
|
|
||||||
- [ ] **1.4 Sovende køretøj.** Parkerede maskiner kører af sted med det samme ved udsendelse, uden at
|
|
||||||
du skal sætte dig ind.
|
|
||||||
|
|
||||||
## 2. Hø og river
|
|
||||||
|
|
||||||
- [ ] **2.1 Hø uden river.** Plan = **Hø**, Riv = Nej. Slå → **Vendes** (venderen i sin egen bredde) →
|
|
||||||
Samles op (læssevogn på slåkurset, ≥ 40,8 m) → hø-silo (kun almindelig silo, aldrig gæringssilo).
|
|
||||||
Log: `mowing -> tedding`, `Courseplay startet (tedder)`, `tedding -> collecting`.
|
|
||||||
- [ ] **2.2 Smal læssevogn uden river.** En læssevogn under 40,8 m → status "læssevogn for smal:
|
|
||||||
… — sæt Riv til eller brug en bredere".
|
|
||||||
- [ ] **2.3 Riv (midterrive).** Riv = Ja. Log: `river lægger skåret 0.x m fra sin kursuslinje`,
|
|
||||||
derefter `Courseplay startet (collector, rake-kursus kopieret)`. Læssevognen rammer skårene.
|
|
||||||
- [ ] **2.4 Sideriver.** Med en river der lægger skåret til siden: log viser fx `3.2 m`, og
|
|
||||||
`skår ligger 3.2 m til … -> læssevognens forskydning …`. **Tjek at vognen kører OVER skåret.**
|
|
||||||
Kører den ved siden af (forkert side), så skriv det ned — så vender jeg fortegnet.
|
|
||||||
- [ ] **2.5 Riv til ensilage.** Plan = Ensilage + Riv = Ja: Slå → Rives → Samles op.
|
|
||||||
|
|
||||||
## 3. Høst (korn, raps, majs …)
|
|
||||||
|
|
||||||
- [ ] **3.1 Høst.** En moden kornmark, Manuel, høster med passende skærebord + én aflæsningsvogn
|
|
||||||
(traktor + tipvogn/overlæssevogn) med flueben → **Start nu**.
|
|
||||||
- Forvent: høster → F-markør → Courseplay høster. Aflæsningsvognen sendes ud når høsteren
|
|
||||||
arbejder, følger den (Courseplays combine-unloader) og kører fulde læs til siloen.
|
|
||||||
- Log: `harvester … sendt`, `Courseplay startet (harvester)`, `unloader … sendt`,
|
|
||||||
`aflæsningsvogn … følger høsteren -> <silo>`.
|
|
||||||
- [ ] **3.2 Afslutning.** Høsten færdig → `harvesting -> <næste trin eller done>`,
|
|
||||||
`aflæsningsvogn … stoppes — høsten er færdig`, evt. `kører rest-læs … til lager`, parkering.
|
|
||||||
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
|
|
||||||
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
|
|
||||||
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
|
|
||||||
- [ ] **3.6 Halm samles op.** Halm efter høst = *Samles op til silo*. Mejetærskeren lægger skår
|
|
||||||
(Courseplay-indstilling halmskår = til), derefter `harvesting -> strawCollecting`, en læssevogn kører
|
|
||||||
**høsterens kursus** (`harvester-kursus kopieret`) og kører halmen til en silo der tager STRAW.
|
|
||||||
- [ ] **3.7 Halm til baller.** Halm = *Presses til baller*: `-> baling` (ballepresser på høsterens
|
|
||||||
kursus) → `-> baleCollecting` (ballesamler/Universal Autoload, `Courseplay startet (ballesamler)`),
|
|
||||||
fulde læs til ballelager (`ballesamler … kører fulde læs til …`).
|
|
||||||
- [ ] **3.8 Halm hakkes.** Halm = *Hakkes*: mejetærskeren lægger IKKE skår (Courseplay halmskår = fra).
|
|
||||||
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
|
|
||||||
Kørende maskiner fortsætter.
|
|
||||||
|
|
||||||
## 4. Roer
|
|
||||||
|
|
||||||
- [ ] **4.1 Roehøst.** Roemark (sukkerroer), roeoptager (Ropa) + aflæsningsvogn → som 3.1, men med
|
|
||||||
roer. Log viser `harvester 'Ropa …'`. Roerne går til nærmeste silo/lager der tager SUGARBEET
|
|
||||||
(brug **Tving lager** hvis de skal et bestemt sted hen, fx roeknuseren).
|
|
||||||
|
|
||||||
## 5. Efter høst: klargøring og såning
|
|
||||||
|
|
||||||
Sæt på en kornmark før høst: Efter høst → Stubbehakning Nej, Jordbehandling **Auto**, **Så bagefter** Ja,
|
|
||||||
Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
|
||||||
|
|
||||||
- [ ] **5.1 Kæde.** Efter høsten fortsætter marken selv: (kalk hvis jorden trænger) → pløj eller
|
|
||||||
kultivér → så → tromle → **Færdig**. Log: `harvesting -> cultivating`, `cultivating -> sowing` …
|
|
||||||
- [ ] **5.2 Auto-jordbehandling.** Står "Jorden trænger til: pløjning" under marken → plov sendes.
|
|
||||||
Ellers kultivator.
|
|
||||||
- [ ] **5.3 Såning.** Såmaskinen sættes til afgrøden (`setSeedIndex`), og den rigtige afgrøde spirer.
|
|
||||||
- [ ] **5.4 Ude af såperioden.** Afgrøde der ikke kan sås nu → status "<afgrøde> kan ikke sås i denne
|
|
||||||
periode", ingen udsendelse.
|
|
||||||
- [ ] **5.5 Tom for frø / påfyldning.** Såmaskine med lidt frø. Moden leder efter en læssestation med frø
|
|
||||||
på lager og en AutoDrive-markør ved triggeren (fx en silo/big-bag-station med SEEDS).
|
|
||||||
- Findes den: log `… fyldes op med SEEDS ved <markør> af AutoDrive når den er tom`. Når den er tom
|
|
||||||
midt på marken, kører AutoDrive derhen, fylder op, kører tilbage, og Courseplay fortsætter.
|
|
||||||
- Findes den ikke: log `ingen påfyldning af SEEDS med AutoDrive-markør fundet …`. Når den er tom:
|
|
||||||
rød besked `'…' er tom for SEEDS — fyld op, så fortsætter marken`, og marken går IKKE videre.
|
|
||||||
- [ ] **5.6 Bar mark alene.** Vælg en bar mark (stub/pløjet) → **Start nu** → kun klargøring/såning.
|
|
||||||
|
|
||||||
## 6. Pleje mens afgrøden vokser
|
|
||||||
|
|
||||||
- [ ] **6.1 Behov vises.** En voksende mark viser "Jorden trænger til: gødning, ukrudt …" og status
|
|
||||||
"— trænger til Gødes". Manuel: intet sker før **Start nu**.
|
|
||||||
- [ ] **6.2 Gødning.** Start nu (eller Auto) → gødningsspreder/-sprøjte med fyld sendes → `pleje:
|
|
||||||
fertilizing` → `fertilizing -> idle`. Højst 2 gange pr. vækstcyklus.
|
|
||||||
- [ ] **6.3 Ukrudt.** Høstafgrøde med tæt ukrudt → strigle (småt ukrudt) eller ukrudtssprøjte
|
|
||||||
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
|
|
||||||
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
|
|
||||||
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
|
|
||||||
|
|
||||||
## 7. Multi-maskine (Courseplay multi-tool)
|
|
||||||
|
|
||||||
- [ ] **7.1 To slåmaskiner.** "Maskiner pr. trin" = 2 på en græsmark med to BiG M med flueben.
|
|
||||||
Forvent: begge sendes samtidig, den ene laver kurset (`bane -1`), den anden kopierer det (`bane 1`)
|
|
||||||
og de kører i konvoj. Log: `… sendt … bane -1`, `… bane 1`, `Courseplay startet (mower, bane -1)`,
|
|
||||||
`Courseplay startet (mower, multi-kursus kopieret, bane 1)`.
|
|
||||||
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
|
|
||||||
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
|
|
||||||
|
|
||||||
## 9. Vælg og glem (afgrøde på marken)
|
|
||||||
|
|
||||||
- [ ] **9.1 Kornmark → hvede.** En mark med byg der vokser: vælg *Afgrøde på marken* = Hvede. Linjen siger
|
|
||||||
"Byg får lov at vokse færdig og høstes; derefter klargøres og tilsås med Hvede". Kørsel skjules (auto).
|
|
||||||
Når byggen er moden: høst → (halm) → klargøring → såning af **hvede** (ikke byg). Log:
|
|
||||||
`mål-afgrøde: WHEAT`, `starter (harvest: harvesting)`, `sowing`.
|
|
||||||
- [ ] **9.2 Bar mark.** Bar mark med mål = Hvede: starter selv klargøring + såning, når det er hvede-såtid.
|
|
||||||
Uden for såtid: status "venter på såtid for Hvede".
|
|
||||||
- [ ] **9.3 Græs → græs.** Græsmark med mål = Græs: slås og samles op hver gang, ingen pløjning/såning.
|
|
||||||
- [ ] **9.4 Græs → hvede.** Græsmark med mål = Hvede: slås og samles op én gang, derefter klargøring
|
|
||||||
(jordbehandling-valget vises) og såning af hvede.
|
|
||||||
- [ ] **9.5 Hele året.** Lad en mark med mål køre en hel sæson uden at røre den: vækst → pleje → høst →
|
|
||||||
klargøring → såning → ... uden at hænge og uden dobbelt-såning.
|
|
||||||
- [ ] **9.6 Alle ledige maskiner.** Markflåde: *Brug alle ledige maskiner* = Ja (standard): maskiner uden
|
|
||||||
flueben sendes også ud. = Nej: kun maskiner med flueben. Maskiner du selv kører, maskiner i flows og
|
|
||||||
maskiner AutoDrive/Courseplay allerede kører, bruges aldrig.
|
|
||||||
- [ ] **9.7 Fjern målet.** Vælg *Ingen — planlæg selv*: marken går tilbage til Manuel.
|
|
||||||
|
|
||||||
## 8. Robusthed
|
|
||||||
|
|
||||||
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
|
|
||||||
samme trin, sæt sendes ud igen, slåkurset hentes fra slåmaskinen.
|
|
||||||
- [ ] **8.2 Stop mark.** **Stop mark** midt i høst → alle maskiner på marken stopper; høst står **Klar**
|
|
||||||
igen, pleje/klargøring står **Venter**.
|
|
||||||
- [ ] **8.3 Ingen vej.** Mark hvis F-markør ikke er forbundet → status "… nåede ikke F…" / "intet lager:
|
|
||||||
… AutoDrive finder ingen rute …" og nyt forsøg efter 5 min.
|
|
||||||
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
|
|
||||||
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Kendte usikkerheder (første gang i spillet)
|
|
||||||
|
|
||||||
| Område | Hvad der er gættet | Tegn på fejl |
|
|
||||||
|---|---|---|
|
|
||||||
| Vækstfase | `getFieldDataAtWorldPosition` anden returværdi = vækststadie | Høstet stub vises som voksende, eller moden mark som bar |
|
|
||||||
| Høsterens afgrøder | `spec_cutter.fruitTypes`/`fruitTypeIndices` | Høster vises med "0 afgrøder" |
|
|
||||||
| Combine-unloader | stationsvalg ud fra silovalget | `ingen aflæsningsstation Courseplay kan bruge` |
|
|
||||||
| Multi-tool | `multiTools` + `laneOffset` + `Course:setPosition` | Begge maskiner kører samme bane |
|
|
||||||
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
|
|
||||||
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
|
|
||||||
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
|
|
||||||
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
|
|
||||||
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
|
|
||||||
+1071
-425
File diff suppressed because it is too large
Load Diff
@@ -1,221 +0,0 @@
|
|||||||
# Behovsstyret forsyning Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Én AutoDrive-traktor holder alle input i en produktion (fx TMR-blander) fyldt ved selv at vælge vare + kilde-silo pr. tur.
|
|
||||||
|
|
||||||
**Architecture:** Den eksisterende wrapper om `ADMultipleTargetsManager.getNextPickup` udvides: ved >1 valgt vare i AD læses aflæsningsmålets lager, ingredienserne rangeres efter behov (rene funktioner i ny fil), og en kilde findes blandt alle AD-markører. AD's varevalg indsnævres til én vare pr. tur og gendannes når AD stopper.
|
|
||||||
|
|
||||||
**Tech Stack:** Lua 5.1 (FS25 mod-sandbox), luajit til tests med håndskrevne mocks, AutoDrive 3.0.1.2 via global `FS25_AutoDrive`.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-21-behovsstyret-forsyning-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Rør aldrig `FS25_AutoDrive.zip`; al adgang via den globale `FS25_AutoDrive`-tabel.
|
|
||||||
- Enhver fejl i spil-/AD-API → `Logging.warning` + AutoDrives originale `getNextPickup`. Aldrig crash.
|
|
||||||
- Præcis én valgt vare → opførsel uændret; de 11 eksisterende tests forbliver grønne.
|
|
||||||
- Markør↔station-tærskel: 40 m (`MAX_MARKER_TO_TRIGGER_DISTANCE`). `MIN_FREE_LITERS = 1000`.
|
|
||||||
- Rene funktioner muterer ikke input (returnér nye tabeller).
|
|
||||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`. Commits uden Co-Authored-By.
|
|
||||||
- Verificerede AD-fakta: `selectedFillTypes` er et array-felt på stateModule og gemmes i savegame; `setFillType(ft)` bevarer valget hvis `ft` er i det; `isActive()`, `getSecondMarker()`, `raiseDirtyFlag()` findes; `ADGraphManager:getMapMarkers()` returnerer array indekseret på `markerIndex`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: `adSupplyPlanner.lua` — rene funktioner
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adSupplyPlanner.lua`
|
|
||||||
- Test: `tests/test_adSupplyPlanner.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces (global tabel `ADSupplyPlanner`):
|
|
||||||
- `MIN_FREE_LITERS` (number)
|
|
||||||
- `scoreNeed(level, capacity, freeSpace) -> number` (højere = mere trængende)
|
|
||||||
- `rankNeeds(needs) -> ranked` hvor `needs = {{fillType, level, capacity}, …}` og `ranked = {{fillType, level, capacity, freeSpace, score}, …}` sorteret mest trængende først, uden dem med `freeSpace < MIN_FREE_LITERS`
|
|
||||||
- `resolveIngredients(state, selected) -> ingredients|nil` hvor `state = nil | {ingredients = {ft…}, narrowedTo = ft}`
|
|
||||||
- `copyList(list) -> ny liste`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Skriv fejlende test** — `tests/test_adSupplyPlanner.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
|
||||||
|
|
||||||
local HAY, GRASS, SILAGE = 20, 21, 22
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then
|
|
||||||
print("OK " .. name)
|
|
||||||
else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
|
|
||||||
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
|
|
||||||
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
|
|
||||||
|
|
||||||
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
|
|
||||||
local needs = {
|
|
||||||
{fillType = HAY, level = 80000, capacity = 100000},
|
|
||||||
{fillType = GRASS, level = 20000, capacity = 100000},
|
|
||||||
{fillType = SILAGE, level = 99500, capacity = 100000},
|
|
||||||
}
|
|
||||||
local ranked = ADSupplyPlanner.rankNeeds(needs)
|
|
||||||
check("fuld ingrediens springes over", #ranked, 2)
|
|
||||||
check("mest trængende først", ranked[1].fillType, GRASS)
|
|
||||||
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
|
|
||||||
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
|
|
||||||
check("input muteres ikke", needs[1].freeSpace, nil)
|
|
||||||
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
|
|
||||||
|
|
||||||
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
|
|
||||||
local tie = ADSupplyPlanner.rankNeeds({
|
|
||||||
{fillType = SILAGE, level = 0, capacity = 5000},
|
|
||||||
{fillType = HAY, level = 0, capacity = 5000},
|
|
||||||
})
|
|
||||||
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
|
|
||||||
|
|
||||||
-- resolveIngredients
|
|
||||||
local multi = {HAY, GRASS}
|
|
||||||
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
|
|
||||||
check("multi-valg bliver ingrediensliste", #resolved, 2)
|
|
||||||
check("listen er en kopi", resolved ~= multi, true)
|
|
||||||
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
|
|
||||||
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
|
|
||||||
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
|
|
||||||
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
|
|
||||||
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
|
|
||||||
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør og se den fejle** — `luajit tests/test_adSupplyPlanner.lua` → fejler med "cannot open FS25_ADSmartPickup/adSupplyPlanner.lua".
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér** — `FS25_ADSmartPickup/adSupplyPlanner.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- AD Supply Planner
|
|
||||||
-- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens
|
|
||||||
-- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet.
|
|
||||||
|
|
||||||
ADSupplyPlanner = {}
|
|
||||||
|
|
||||||
-- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af).
|
|
||||||
ADSupplyPlanner.MIN_FREE_LITERS = 1000
|
|
||||||
|
|
||||||
function ADSupplyPlanner.copyList(list)
|
|
||||||
local copy = {}
|
|
||||||
for index, value in ipairs(list) do
|
|
||||||
copy[index] = value
|
|
||||||
end
|
|
||||||
return copy
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hvor meget trænger målet til denne ingrediens? Højere = hentes først.
|
|
||||||
-- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1),
|
|
||||||
-- så en lille næsten-tom tank vinder over en stor halvfuld.
|
|
||||||
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)
|
|
||||||
if capacity <= 0 then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
return freeSpace / capacity
|
|
||||||
end
|
|
||||||
|
|
||||||
-- needs: {{fillType, level, capacity}, ...}. Returnerer NY liste, mest trængende først;
|
|
||||||
-- ingredienser uden plads i målet er sorteret fra.
|
|
||||||
function ADSupplyPlanner.rankNeeds(needs)
|
|
||||||
local ranked = {}
|
|
||||||
for _, need in ipairs(needs) do
|
|
||||||
local freeSpace = need.capacity - need.level
|
|
||||||
if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then
|
|
||||||
table.insert(ranked, {
|
|
||||||
fillType = need.fillType,
|
|
||||||
level = need.level,
|
|
||||||
capacity = need.capacity,
|
|
||||||
freeSpace = freeSpace,
|
|
||||||
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(ranked, function(a, b)
|
|
||||||
if a.score ~= b.score then
|
|
||||||
return a.score > b.score
|
|
||||||
end
|
|
||||||
return a.fillType < b.fillType
|
|
||||||
end)
|
|
||||||
return ranked
|
|
||||||
end
|
|
||||||
|
|
||||||
-- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg.
|
|
||||||
-- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste.
|
|
||||||
-- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra).
|
|
||||||
function ADSupplyPlanner.resolveIngredients(state, selected)
|
|
||||||
if type(selected) ~= "table" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if #selected > 1 then
|
|
||||||
return ADSupplyPlanner.copyList(selected)
|
|
||||||
end
|
|
||||||
if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then
|
|
||||||
return ADSupplyPlanner.copyList(state.ingredients)
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør tests** — `luajit tests/test_adSupplyPlanner.lua` → `ALLE TESTS OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit** — `git add FS25_ADSmartPickup/adSupplyPlanner.lua tests/test_adSupplyPlanner.lua && git commit -m "feat: ADSupplyPlanner — behovsrangering og ingrediensliste-tilstand"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Integration i `adSmartPickup.lua`
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (hele filen omstruktureres let; se kode)
|
|
||||||
- Test: `tests/test_adSmartPickup.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADSupplyPlanner.rankNeeds`, `.resolveIngredients`, `.copyList` (Task 1).
|
|
||||||
- Produces: `ADSmartPickup.choosePickup(adEnv, driver) -> choice|nil` med `choice = {markerIndex, markerName, group, level, fillType?, need?}`; `ADSmartPickup.findLeastFilledPickup` returnerer nu samme `choice`-tabel (uden `fillType`).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Udvid test-mocks og skriv fejlende tests** i `tests/test_adSmartPickup.lua`:
|
|
||||||
- `dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")` før `adSmartPickup.lua`.
|
|
||||||
- `unloadStations`-liste + `getUnloadingStations` i `g_currentMission.storageSystem`; nulstilles i `resetWorld()`.
|
|
||||||
- `getMapMarkers = function() return markers end` i `ADGraphManager`-mock.
|
|
||||||
- `getSecondMarker = function() return nil end` i `makeDriver` (rigtig AD har den altid).
|
|
||||||
- Nye helpers `makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken)` og `makeSupplyDriver(firstMarker, secondMarker, fillTypes)` (muterbar stateModule-mock med `setFillType`, `raiseDirtyFlag`, `isActive`).
|
|
||||||
- Tests S1–S9 (se Step 1-koden i implementeringen; dækker spec'ens testliste).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør og se de nye fejle** — `luajit tests/test_adSmartPickup.lua` → S-tests FAIL, de 11 gamle OK.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér** — i `adSmartPickup.lua`:
|
|
||||||
- `supplyState = setmetatable({}, {__mode = "k"})` (køretøj → `{ingredients, narrowedTo}`).
|
|
||||||
- `getDistanceToTriggers(triggers, nodeFields, x, z)` + `getClosestStation(stations, triggersField, nodeFields, wayPoint, farmId)`; `getLoadStationAtMarker` (felt `loadTriggers`, node `triggerNode`) og `getUnloadStationAtMarker` (felt `unloadTriggers`, noder `exactFillRootNode`, `triggerNode`, `aiNode`).
|
|
||||||
- `collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)` → `{{marker, station}, …}`; `findLeastFilled(markerStations, fillTypes, farmId)` → `choice|nil` (eksisterende eget-lager-først-rangering).
|
|
||||||
- `getTargetNeeds(station, ingredients, farmId)`: pr. understøttet ingrediens summeres `storage:getFillLevel(ft)` og `storage:getFreeCapacity(ft)` over `station.targetStorages`; `capacity = level + free`. Ingen storages → `station:getFreeCapacity(ft, farmId)` som `capacity`, `level = 0`.
|
|
||||||
- `ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)`, `applySupplyChoice(driver, ingredients, choice)` (`setFillType`, `selectedFillTypes = {ft}`, `raiseDirtyFlag`, gem tilstand), `ADSmartPickup.choosePickup` (supply → ellers mappe-logik → ellers nil).
|
|
||||||
- Hook bruger `choice.group` til AD's rotations-bogføring og logger vare + behov ved supply-valg.
|
|
||||||
- `restoreStoppedVehicles()` kaldt fra `update` (pcall): når `not stateModule:isActive()` og valget stadig er `{narrowedTo}` → `selectedFillTypes = kopi af ingredients` + dirty-flag; tilstand slettes.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør alle tests** — `luajit tests/test_adSupplyPlanner.lua && luajit tests/test_adSmartPickup.lua` → begge `ALLE TESTS OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit** — `git add -A && git commit -m "feat: behovsstyret forsyning — én traktor holder alle input fyldt"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Pakning, docs, deploy
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (version `1.1.0.0`, `<sourceFile filename="adSupplyPlanner.lua"/>` FØR `adSmartPickup.lua`, beskrivelse), `README.md`
|
|
||||||
|
|
||||||
- [ ] **Step 1:** Opdatér modDesc + README (betjening: multi-vælg varer; begrænsninger fra spec).
|
|
||||||
- [ ] **Step 2:** Byg: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cd ..`; verificér med `unzip -l` at begge .lua + modDesc + icon er med.
|
|
||||||
- [ ] **Step 3:** Deploy atomisk (spillet kan køre): `cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp && mv ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp ~/FS25-data/mods/FS25_ADSmartPickup.zip`.
|
|
||||||
- [ ] **Step 4:** Code-review (ecc:code-reviewer) af diff; ret CRITICAL/HIGH.
|
|
||||||
- [ ] **Step 5:** Commit + `git push gitea`. Opdatér memory + Obsidian-noten `projekter/FS25 silo-patches + AD Smart Pickup.md`.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
|||||||
# Flow-tavle — implementeringsplan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Én tavle med ét flow pr. række (Vare · Fra · Til · Regel · Traktorer · Status); Fra/Til vælges som bygninger, markører findes automatisk. Faner: Flows · Lagre.
|
|
||||||
|
|
||||||
**Architecture:** Ny spil-adapter `adBuildings.lua` (bygninger + markør-opslag + cache). Modellen får bygnings-id-lister; controlleren udleder waypoint-lister fra dem (`resolveMarkers`) så planlægger/hooks er uændrede. GUI omskrives: `SmartPickupFrame.lua` (faner, tavle, fælles), `SmartPickupFlowTab.lua` (detail-blok), `SmartPickupStorageTab.lua` (uændret).
|
|
||||||
|
|
||||||
**Tech Stack:** FS25 Lua, AutoDrive-globaler via `FS25_AutoDrive`, luajit-tests.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-23-flow-tavle-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Motoren (adOutbound, adOutboundPlanner, adSupplyPlanner, adUnloadWait, adSmartPickup-hooks) røres ikke.
|
|
||||||
- Byg kun når alle tests er grønne (`for t in tests/test_*.lua; do luajit $t|tail -1; done`), derefter zip → `~/FS25-data/mods/` → commit → push gitea main.
|
|
||||||
- Version 1.13.0.0. XML-version 3; v1/v2 læses.
|
|
||||||
- Wait-markører er aldrig bygningsmarkører. Siloers læssemarkør = strengt opslag; alt andet = nærmeste ≤ 40 m.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
1. Run med bygning der er solgt siden sidst: rækken viser "(mangler)", Start afviser med noMarker, intet crash. (Task 3 test)
|
|
||||||
2. v2-fil med markør der ikke kan oversættes til en bygning: markøren beholdes rå og virker som før. (Task 2 test)
|
|
||||||
3. To bygninger med markører inden for 40 m af hinanden (silo-rækker): hver silo får sin egen markør (strengt opslag). (Task 1 test)
|
|
||||||
4. Forsynings-run: Fra vises som "alle siloer" og sources-listen røres ikke. (Task 3 test)
|
|
||||||
5. Fanen rebuild'es mens et run kører: Start/Stop-knappen og status følger `isRunning`. (in-game)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: adBuildings
|
|
||||||
|
|
||||||
**Files:** Create `FS25_ADSmartPickup/adBuildings.lua`; Test `tests/test_adBuildings.lua`.
|
|
||||||
|
|
||||||
**Produces:**
|
|
||||||
```lua
|
|
||||||
ADBuildings.MAX_MARKER_DISTANCE = 40; ADBuildings.CACHE_MS = 1000
|
|
||||||
ADBuildings.list(adEnv, farmId) -- {building...} sorteret kind, navn (cachet)
|
|
||||||
ADBuildings.find(adEnv, farmId, id) -- building | nil
|
|
||||||
ADBuildings.byMarker(adEnv, farmId, wayPointId, role) -- role "load"|"unload": building hvis markøren er dens markør, ellers nil
|
|
||||||
ADBuildings.getId(placeable) -- uniqueId eller "pos:x:z"
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
building = {id, name, kind, placeable, loadMarker={id,name,distance}|nil, unloadMarker=…|nil, canBeSource, canBeTarget, canSell}
|
|
||||||
```
|
|
||||||
Kilder til bygninger: `g_currentMission.placeableSystem.placeables` (ejet af farm eller sell) — kind: factory (spec_productionPoint), silo (spec_silo), husbandry (getHusbandryFillLevel), objectStorage (spec_objectStorage), sell (ADPrices.isSellingStation på placeable.spec_sellingPoint / unloadingStation via storageSystem). Læssetriggere: `productionPoint.loadingStation.loadTriggers`, `spec_silo.loadingStation.loadTriggers`, husbandry: alle loadingStations i storageSystem med owningPlaceable == placeable. Aflæsningstriggere: unloadingStations med owningPlaceable == placeable. Objektlager/pallespawner: rootNode/spawnPlaces[1].startNode.
|
|
||||||
Markørvalg: nærmeste markør (ikke Wait) til en trigger-node ≤ 40 m; for silo-load: kun markører hvor `ADSmartPickup.getLoadStationAtWayPointId(marker.id)` == siloens station.
|
|
||||||
|
|
||||||
- [ ] Tests B1–B7 (kind, markører, silo strengt, Wait udelukket, byMarker, id-fallback, cache-invalidering) → RED → implementér → GREEN → commit `feat: adBuildings`.
|
|
||||||
|
|
||||||
### Task 2: Model v3
|
|
||||||
|
|
||||||
**Files:** Modify `adRuns.lua`, `adRunsStorage.lua`; Test `tests/test_adRuns.lua`.
|
|
||||||
|
|
||||||
**Produces:** run-felter `sourceBuildingIds`, `targetBuildingIds` (lister af strenge); `ADRuns.toggleSourceBuilding/toggleTargetBuilding(store, runId, id)`; XML v3 `#sourceBuildings`/`#targetBuildings` (mellemrumsadskilte). `ADRuns.XML_VERSION = 3`. Migration sker i controlleren (Task 3), ikke i modellen.
|
|
||||||
|
|
||||||
- [ ] Tests R-v3 → RED → implementér → GREEN → commit.
|
|
||||||
|
|
||||||
### Task 3: Controller — resolve + migration + validering
|
|
||||||
|
|
||||||
**Files:** Modify `adRunsController.lua`; Test `tests/test_adRunsController.lua`.
|
|
||||||
|
|
||||||
**Produces:**
|
|
||||||
```lua
|
|
||||||
ADRunsController.resolveMarkers(adEnv, run) -- udfylder run.sourceWayPointIds (outbound) / targetWayPointIds (supply) fra bygninger; rå markører (uden bygning) bevares; returnerer {missing = {buildingName...}}
|
|
||||||
ADRunsController.migrateBuildings(adEnv) -- ved load: markører → bygnings-id via byMarker; logger
|
|
||||||
ADRunsController.getFlowRow(adEnv, run) -- {product, from, to, rule, vehicles, status, warning} tekster til tavlen
|
|
||||||
validateVehicle: ny grund "noMarker" når en valgt bygning mangler markør
|
|
||||||
```
|
|
||||||
Start/plan: `startRun` og `ADOutbound.buildPlanInput` kalder resolveMarkers først (buildPlanInput via ADRunsController.getRunForVehicle → resolve i getRunForVehicle? nej: i `ADSmartPickup.choosePickup`/`ADOutbound.choosePickup` kaldes `ADRunsController.resolveMarkers(adEnv, run)` én gang pr. plan).
|
|
||||||
|
|
||||||
- [ ] Tests C-flow → RED → implementér → GREEN → commit.
|
|
||||||
|
|
||||||
### Task 4: GUI — Flows-fane
|
|
||||||
|
|
||||||
**Files:** Rewrite `gui/SmartPickupFrame.lua` (faner Flows/Lagre, tavle, fælles rækker, knapper), Create `gui/SmartPickupFlowTab.lua` (detail-blok, callbacks), delete `SmartPickupSupplyTab.lua`/`SmartPickupOutboundTab.lua` (indhold flyttes), Modify `gui/SmartPickupFrame.xml` (statiske rækker: runOption "Flow", waitPool, loops, returnBelow, sellAt, storeAbove; prefabs som i dag), l10n da/en (`spu_tab_flows`, `spu_flow_*`, `spu_direction_in/out`, `spu_from_allSilos`, `spu_noMarker`, `spu_reason_noMarker`, `spu_section_*`), `adSmartPickup.lua` (source()).
|
|
||||||
|
|
||||||
- [ ] Implementér; syntaks- og definitions-tjek; in-game.
|
|
||||||
|
|
||||||
### Task 5: README, version, byg, deploy, push, memory.
|
|
||||||
@@ -1,569 +0,0 @@
|
|||||||
# Udkørsel af produkter — implementeringsplan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Runs med retning "Udkørsel": traktorer henter fabrikkers output og lagervarer (bulk og paller, også fra objektlagre) og kører dem til silo eller salgssted efter politik pr. vare, pristærskel og lagertærskel.
|
|
||||||
|
|
||||||
**Architecture:** Ren planlægger (`adOutboundPlanner`) + rene prisregler (`adPrices`) uden spil-API; spiladaptere i `adSources`/`adPrices`; AutoDrive-integration i `adOutbound` via to hooks (næste pålæsning + læsseopgaven), samme mønster som `adUnloadWait`. Wait-puljen udskilles i `adWaitPool` og deles. GUI får tre faner (Oversigt · Forsyning · Udkørsel) med fælles byggeklodser.
|
|
||||||
|
|
||||||
**Tech Stack:** FS25 Lua (GIANTS-sandbox), AutoDrive 3.x via `FS25_AutoDrive`-globalen, Universal Autoload via AD's AL-lag, luajit-tests uden spil.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-23-udkoersel-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Alle spil-API-kald i hooks i `pcall`; fejl → hold ved Wait + WARN én gang, aldrig AutoDrive-stop.
|
|
||||||
- Modellen (`adRuns`) muterer aldrig: ny store ved hver ændring.
|
|
||||||
- Log-præfiks `ADSmartPickup: ` i alle moduler; alle run-ændringer logges.
|
|
||||||
- XML-version 2; version 1-filer læses som `kind="supply"`.
|
|
||||||
- Standarder: `sellAtPercent` 90 (50–100), `storeAbovePercent` 80 (50–95), `collectRound` false.
|
|
||||||
- Politikker: `store` · `sell` · `sellElseStore` (+ "off" = varen udelades af `products`).
|
|
||||||
- Kildetyper: fabrik (produktionspunkt), silo (`spec_silo`), objektlager (`spec_objectStorage`), genkendt inden for 40 m af markøren.
|
|
||||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`; alle eksisterende tests skal blive grønne.
|
|
||||||
- Version bump til 1.12.0.0 i `modDesc.xml`; byg `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy til `~/FS25-data/mods/`.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
1. Traktor med UAL-vogn står ved et objektlager hvis spawn sidder fast (`objectSpawn.isActive` for evigt): forventet: 30 s uden fremdrift → læsningen afsluttes med det der er, ingen palle efterlades ud over den ene der evt. er spawnet. (Task 6 test O5)
|
|
||||||
2. Bedst betalende salgssted er uopnåeligt, næstbedste kan nås: forventet: næstbedste vælges uden advarsel; kun når intet kan nås, vises rød advarsel én gang. (Task 4 test P6 + Task 6 test O7)
|
|
||||||
3. Vare hvis salgsstation ikke findes på kortet, politik `sell`: forventet: varen står som "intet salgssted" i UI og planlæggeren springer den over uden fejl. (Task 4 test P7)
|
|
||||||
4. Version 1-runs-fil (uden `kind`) indlæses: forventet: alle runs bliver Forsyning, intet går tabt, næste gem skriver version 2. (Task 1 test R-v2)
|
|
||||||
5. Bulk-vogn med opsamlingsrunde og to kilder hvor den første giver fuldt læs: forventet: der køres direkte til målet, kilde 2 besøges ikke. (Task 6 test O6)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Model + lagring v2
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRuns.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRunsStorage.lua`
|
|
||||||
- Test: `tests/test_adRuns.lua` (tilføj sektion R-v2)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `ADRuns.KINDS = {supply=true, outbound=true}`, `ADRuns.POLICIES = {store=true, sell=true, sellElseStore=true}`, `ADRuns.DEFAULT_SELL_AT_PERCENT=90`, `ADRuns.DEFAULT_STORE_ABOVE_PERCENT=80`, `ADRuns.createRun(store, name, kind)`, run-felter `kind, sourceWayPointIds, products={{fillType="SUGAR", policy="sell"}}, sellAtPercent, storeAbovePercent, collectRound`, `ADRuns.toggleSource(store, runId, wayPointId)`, `ADRuns.setProductPolicy(store, runId, fillTypeName, policy)` (policy "off" fjerner), `ADRuns.getProductPolicy(run, fillTypeName)` → policy eller "off", `ADRuns.serializeProducts/parseProducts`, `ADRuns.XML_VERSION = 2`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Skriv fejlende tests** — tilføj nederst i `tests/test_adRuns.lua` før den afsluttende opsummering:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
|
|
||||||
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
|
|
||||||
check("Rv2 kind", out.kind, "outbound")
|
|
||||||
check("Rv2 supply som standard", ADRuns.createRun(ADRuns.newStore(), "x").kind == nil and "nil" or select(2, ADRuns.createRun(ADRuns.newStore(), "x")).kind, "supply")
|
|
||||||
check("Rv2 sellAt 90", out.sellAtPercent, 90)
|
|
||||||
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
|
|
||||||
check("Rv2 runde fra", out.collectRound, false)
|
|
||||||
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
|
|
||||||
o2 = ADRuns.toggleSource(o2, out.id, 12386)
|
|
||||||
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
|
|
||||||
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
|
|
||||||
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
|
|
||||||
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
|
|
||||||
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
|
|
||||||
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
|
|
||||||
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
|
|
||||||
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
|
|
||||||
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
|
|
||||||
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
|
|
||||||
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
|
|
||||||
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
|
|
||||||
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
|
|
||||||
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
|
|
||||||
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
|
|
||||||
check("Rv2 parse antal", #parsed, 2)
|
|
||||||
check("Rv2 parse politik", parsed[2].policy, "store")
|
|
||||||
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
|
|
||||||
local rtRun = ADRuns.getRun(rt, out.id)
|
|
||||||
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
|
|
||||||
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
|
|
||||||
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
|
|
||||||
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
|
|
||||||
check("Rv2 roundtrip runde", rtRun.collectRound, true)
|
|
||||||
-- v1-post uden kind/sources/products -> supply med standarder
|
|
||||||
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
|
|
||||||
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
|
|
||||||
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
|
|
||||||
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør testen** — `luajit tests/test_adRuns.lua` → FAIL (`toggleSource` nil).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér i `adRuns.lua`**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADRuns.XML_VERSION = 2
|
|
||||||
ADRuns.KINDS = {supply = true, outbound = true}
|
|
||||||
ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true}
|
|
||||||
ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100
|
|
||||||
ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95
|
|
||||||
|
|
||||||
local function copyProducts(products)
|
|
||||||
local copy = {}
|
|
||||||
for index, product in ipairs(products or {}) do
|
|
||||||
copy[index] = {fillType = product.fillType, policy = product.policy}
|
|
||||||
end
|
|
||||||
return copy
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clampRange(value, default, min, max)
|
|
||||||
local number = tonumber(value) or default
|
|
||||||
return math.max(min, math.min(max, math.floor(number)))
|
|
||||||
end
|
|
||||||
```
|
|
||||||
`copyRun` får `kind, sourceWayPointIds = copyList(...), products = copyProducts(...), sellAtPercent, storeAbovePercent, collectRound`. `createRun(store, name, kind)` sætter `kind = ADRuns.KINDS[kind] and kind or "supply"`, `sourceWayPointIds = {}`, `products = {}`, `sellAtPercent = 90`, `storeAbovePercent = 80`, `collectRound = false`. `updateRun` håndterer `changes.kind` (kun gyldige), `sourceWayPointIds` (copyList), `products` (copyProducts), `sellAtPercent`/`storeAbovePercent` (clampRange), `collectRound` (`== true`).
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function ADRuns.toggleSource(store, runId, wayPointId)
|
|
||||||
local run = ADRuns.getRun(store, runId)
|
|
||||||
if run == nil then return copyStore(store) end
|
|
||||||
local sources, found = {}, false
|
|
||||||
for _, id in ipairs(run.sourceWayPointIds) do
|
|
||||||
if id == wayPointId then found = true else table.insert(sources, id) end
|
|
||||||
end
|
|
||||||
if not found then table.insert(sources, wayPointId) end
|
|
||||||
return ADRuns.updateRun(store, runId, {sourceWayPointIds = sources})
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.getProductPolicy(run, fillTypeName)
|
|
||||||
for _, product in ipairs(run.products or {}) do
|
|
||||||
if product.fillType == fillTypeName then return product.policy end
|
|
||||||
end
|
|
||||||
return "off"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- policy "off" fjerner varen; ukendt politik ignoreres (store uændret).
|
|
||||||
function ADRuns.setProductPolicy(store, runId, fillTypeName, policy)
|
|
||||||
local run = ADRuns.getRun(store, runId)
|
|
||||||
if run == nil or (policy ~= "off" and not ADRuns.POLICIES[policy]) then return copyStore(store) end
|
|
||||||
local products = {}
|
|
||||||
for _, product in ipairs(run.products) do
|
|
||||||
if product.fillType ~= fillTypeName then table.insert(products, product) end
|
|
||||||
end
|
|
||||||
if policy ~= "off" then table.insert(products, {fillType = fillTypeName, policy = policy}) end
|
|
||||||
return ADRuns.updateRun(store, runId, {products = products})
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.serializeProducts(products)
|
|
||||||
local parts = {}
|
|
||||||
for _, product in ipairs(products or {}) do table.insert(parts, product.fillType .. ":" .. product.policy) end
|
|
||||||
return table.concat(parts, " ")
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.parseProducts(text)
|
|
||||||
local products = {}
|
|
||||||
for token in string.gmatch(tostring(text or ""), "%S+") do
|
|
||||||
local name, policy = string.match(token, "^([^:]+):(%a+)$")
|
|
||||||
if name ~= nil and ADRuns.POLICIES[policy] then table.insert(products, {fillType = name, policy = policy}) end
|
|
||||||
end
|
|
||||||
return products
|
|
||||||
end
|
|
||||||
```
|
|
||||||
`toRecords` tilføjer `kind, sources = table.concat(run.sourceWayPointIds, " "), products = serializeProducts, sellAt, storeAbove, collectRound = run.collectRound and 1 or 0`. `fromRecords` læser dem med standarder (`kind` ugyldig/nil → "supply"; `collectRound = tonumber(record.collectRound) == 1`).
|
|
||||||
|
|
||||||
`adRunsStorage.lua`: `writeRecords` skriver `#kind` (string), `#sources`, `#products`, `#sellAt` (int), `#storeAbove` (int), `#collectRound` (int); `readRecords` læser dem (`getString`/`getInt`; manglende attribut → nil → standard i fromRecords).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør** `luajit tests/test_adRuns.lua && luajit tests/test_adRunsController.lua` → alle OK.
|
|
||||||
- [ ] **Step 5: Commit** `feat: run-model v2 — retning, kilder, varepolitik, tærskler, runde (XML v2 + v1-migration)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: adWaitPool udskilt fra adUnloadWait
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adWaitPool.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adUnloadWait.lua`, `adLoadSwap.lua` (excludedPlaceables), `modDesc.xml` (sourceFile før adUnloadWait)
|
|
||||||
- Test: `tests/test_adWaitPool.lua` (ny), eksisterende `tests/test_adSmartPickup.lua` skal stadig køre
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADWaitPool.getCandidates(adEnv, poolGroup) -- {{marker, x, z}}
|
|
||||||
ADWaitPool.claim(markerId, vehicle) / ADWaitPool.releaseVehicle(vehicle) / ADWaitPool.getOwner(markerId)
|
|
||||||
ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable) -- candidate eller nil (nærmeste ledige fra vognens position)
|
|
||||||
ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId) / ADWaitPool.hasRouteBothWays(adEnv, vehicle, toId, homeId)
|
|
||||||
ADWaitPool.redirect(task, destinationID) -- stopper AD's trigger-aktivitet, sætter task.destinationID/state/setPathTo
|
|
||||||
ADWaitPool.getClaims() -- {{marker, vehicle}} sorteret (til GUI)
|
|
||||||
ADWaitPool.STATE_DRIVING -- sættes af adUnloadWait.install (task.STATE_DRIVING or 2)
|
|
||||||
ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables) -- 7. param: {[placeable]=true} udelukkes
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Test** `tests/test_adWaitPool.lua`: mock `FS25_AutoDrive.ADGraphManager` (markers i mapperne "Cow Wait" og "silo", waypoints, `getPathTo` der returnerer `{ {id=target} }` for nåelige), `ADSupplyPlanner` via `dofile`. Tjek: (W1) `getCandidates(adEnv, "Cow Wait")` giver kun Wait-markører i den mappe, tom pulje = alle Wait-mapper; (W2) `pickFree` returnerer nærmeste ledige, springer reserverede over, returnerer eget reserverede; (W3) `claim/releaseVehicle/getOwner`; (W4) `pickFree` med `isReachable` der afviser nærmeste → næste; (W5) `getClaims` sorteret på mappe.
|
|
||||||
- [ ] **Step 2: Kør** → FAIL (fil mangler).
|
|
||||||
- [ ] **Step 3: Implementér** `adWaitPool.lua` ved at flytte `getWaitCandidates`, `claims`, `pathReaches`, `hasRouteTo`, `hasRouteBothWays`, `redirect`, `getClaims`-sorteringen fra `adUnloadWait.lua` (samme kode, offentlige navne som ovenfor; `claims` er `setmetatable({}, {__mode="v"})`; `getClaims` itererer `claims` direkte: `{marker = ADGraphManager:getMapMarkerByWayPointId(id), vehicle}`). `adUnloadWait.lua` kalder dem (`claims[...]` → `ADWaitPool.claim/getOwner/releaseVehicle`; `pickNearestFreeWaitPoint(getWaitCandidates(...), claims, ...)` → `ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, pool), ADWaitPool.claims, x, z, vehicle, isReachable)`); `ADUnloadWait.getClaims/hasRouteTo/hasRouteBothWays` bliver delegater. `ADLoadSwap.findDumpMarker`: `if excludedPlaceables ~= nil and excludedPlaceables[station.owningPlaceable] then skip`.
|
|
||||||
- [ ] **Step 4: Kør** alle tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` → grønne.
|
|
||||||
- [ ] **Step 5: Commit** `refactor: Wait-puljen udskilt i adWaitPool (reservationer, rutetjek, redirect) til deling med udkørsel`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: adPrices — toppris og pris-%
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adPrices.lua`
|
|
||||||
- Test: `tests/test_adPrices.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADPrices.maxFactor(factors) -- højeste værdi i {[period]=factor}; 1 uden faktorer
|
|
||||||
ADPrices.topPrice(baseNow, factorNow, maxFactor) -- baseNow * maxFactor / factorNow; baseNow når factorNow <= 0
|
|
||||||
ADPrices.percentOfTop(price, top) -- afrundet heltal; 0 når top <= 0
|
|
||||||
ADPrices.isSellingStation(station) -- SellingStation, isSellingPoint, ikke hideFromPricesMenu
|
|
||||||
ADPrices.getQuote(station, fillType) -- {price, top, percent} eller nil (spil-API i pcall)
|
|
||||||
ADPrices.bestQuote(quotes) -- højeste percent, dernæst price; nil ved tom
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Test** (P1) `maxFactor({EARLY_SPRING=0.9, LATE_AUTUMN=1.2})` = 1.2; (P2) `topPrice(500, 1.0, 1.25)` = 625; `topPrice(500, 0, 1.25)` = 500; (P3) `percentOfTop(470, 625)` = 75; `percentOfTop(700, 625)` = 112 (stor efterspørgsel > 100); `percentOfTop(1, 0)` = 0; (P4) `getQuote` med fake station `{fillTypePrices={[5]=500}, getEffectiveFillTypePrice=function(_,ft) return 470 end, isa=function() return true end, isSellingPoint=true}` og `g_fillTypeManager.getFillTypeByIndex` → `{economy={factors={A=1.0,B=1.25}}, pricePerLiter=400}`, `g_currentMission.environment.currentPeriod="A"` → percent 75; (P5) uden `fillTypePrices` bruges `pricePerLiter × factorNow × economyManager:getPriceMultiplier()` (1.0) → top 500, percent 94 (470/500); (P6) `bestQuote` vælger højeste percent.
|
|
||||||
- [ ] **Step 2: Kør** → FAIL.
|
|
||||||
- [ ] **Step 3: Implementér**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADPrices = {}
|
|
||||||
ADPrices.LOG_PREFIX = "ADSmartPickup: "
|
|
||||||
|
|
||||||
function ADPrices.maxFactor(factors)
|
|
||||||
local best = nil
|
|
||||||
for _, factor in pairs(factors or {}) do
|
|
||||||
if type(factor) == "number" and (best == nil or factor > best) then best = factor end
|
|
||||||
end
|
|
||||||
return best or 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.topPrice(baseNow, factorNow, maxFactor)
|
|
||||||
if baseNow == nil or baseNow <= 0 then return 0 end
|
|
||||||
if factorNow == nil or factorNow <= 0 or maxFactor == nil or maxFactor <= 0 then return baseNow end
|
|
||||||
return baseNow * maxFactor / factorNow
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.percentOfTop(price, top)
|
|
||||||
if top == nil or top <= 0 or price == nil then return 0 end
|
|
||||||
return math.floor(100 * price / top + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.isSellingStation(station)
|
|
||||||
return station ~= nil and station.isa ~= nil and SellingStation ~= nil and station:isa(SellingStation)
|
|
||||||
and station.isSellingPoint == true and not station.hideFromPricesMenu
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getCurrentPeriod()
|
|
||||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
|
||||||
return environment ~= nil and environment.currentPeriod or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.getQuote(station, fillType)
|
|
||||||
local ok, effective = pcall(station.getEffectiveFillTypePrice, station, fillType)
|
|
||||||
if not ok or type(effective) ~= "number" then return nil end
|
|
||||||
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
|
||||||
local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
|
|
||||||
local period = getCurrentPeriod()
|
|
||||||
local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
|
|
||||||
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
|
|
||||||
if baseNow == nil or baseNow <= 0 then
|
|
||||||
local multiplier = 1
|
|
||||||
local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil
|
|
||||||
if economy ~= nil and economy.getPriceMultiplier ~= nil then
|
|
||||||
local okMult, value = pcall(economy.getPriceMultiplier, economy)
|
|
||||||
if okMult and type(value) == "number" then multiplier = value end
|
|
||||||
end
|
|
||||||
baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * multiplier
|
|
||||||
end
|
|
||||||
local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors))
|
|
||||||
return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.bestQuote(quotes)
|
|
||||||
local best = nil
|
|
||||||
for _, quote in ipairs(quotes or {}) do
|
|
||||||
if best == nil or quote.percent > best.percent or (quote.percent == best.percent and quote.price > best.price) then best = quote end
|
|
||||||
end
|
|
||||||
return best
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adPrices — toppris fra årets prisforløb og pris-% pr. salgssted`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: adOutboundPlanner (ren)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adOutboundPlanner.lua`
|
|
||||||
- Test: `tests/test_adOutboundPlanner.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
-- input:
|
|
||||||
-- { capacity = liter, hasAutoload = bool, sellAtPercent, storeAbovePercent, collectRound,
|
|
||||||
-- products = { { fillType = idx, name = "SUGAR", policy = "sell"|"store"|"sellElseStore", isPallet = bool,
|
|
||||||
-- sources = { {wayPointId, available, level, capacity} ... }, -- kun kilder med available > 0
|
|
||||||
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted
|
|
||||||
-- store = {wayPointId, name, free} | nil } ... } }
|
|
||||||
-- output: trip | nil, reasons
|
|
||||||
-- trip = { kind = "sell"|"store", fillTypes = {idx...}, names = {...}, sourceIds = {wayPointId...}, targetWayPointId, targetName, percent|nil }
|
|
||||||
-- reasons = { "SUGAR 78 % af top (kræver 90), lager 41 %", ... }
|
|
||||||
ADOutboundPlanner.plan(input)
|
|
||||||
ADOutboundPlanner.evaluate(product, input) -- {available, fullLoad, storageHigh, storagePercent, priceOk, percent}
|
|
||||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
|
||||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (`local P = ADOutboundPlanner` efter `dofile`):
|
|
||||||
- P1 salg ved pris over tærskel, uanset mængde: SUGAR sell, sources {{1, 5000, ...}}, quote 92, capacity 20000 → trip.kind "sell", targetWayPointId = quote.wayPointId, sourceIds {1}.
|
|
||||||
- P2 pris under tærskel, lager lavt → nil, reasons[1] = "SUGAR 78 % af top (kræver 90), lager 41 %".
|
|
||||||
- P3 sellElseStore, pris lav, lager 85 % → "store" til store.wayPointId.
|
|
||||||
- P4 store med fuldt læs (available ≥ capacity) og lager 20 % → store.
|
|
||||||
- P5 rangering: to sell-kandidater 95 % og 91 % → første vælges (højeste %).
|
|
||||||
- P6 quote nil (intet nåeligt salgssted), policy sell, lager højt → nil, reason "SUGAR: intet nåeligt salgssted".
|
|
||||||
- P7 store nil ved store-behov → nil, reason "FORAGE: ingen silo med plads".
|
|
||||||
- P8 runde bulk (collectRound, hasAutoload=false): SUGAR i kilde A (3000) og B (9000), capacity 10000 → sourceIds {B, A} (mest først), ét fillType.
|
|
||||||
- P9 runde UAL: STRAW_BASKET (sell 95, station S) + STRAW_CRATE (sell 93, station S) + STRAW_BROOM (sell 96, station T) → fillTypes {BROOM} kun (højest, T) — og med BROOM fjernet: {BASKET, CRATE} samme station S.
|
|
||||||
- P10 uden runde: kun første fillType og kun én kilde (den med mest).
|
|
||||||
- P11 policy off/ingen sources → nil, reasons tom for varer uden kilder ("SUGAR: intet på lager").
|
|
||||||
- [ ] **Step 2: Kør** → FAIL. **Step 3: Implementér**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADOutboundPlanner = {}
|
|
||||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
|
||||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
|
||||||
|
|
||||||
local function sumAvailable(product)
|
|
||||||
local total, high, percent = 0, false, 0
|
|
||||||
for _, source in ipairs(product.sources or {}) do
|
|
||||||
total = total + (source.available or 0)
|
|
||||||
if source.capacity ~= nil and source.capacity > 0 then
|
|
||||||
local p = 100 * (source.level or 0) / source.capacity
|
|
||||||
percent = math.max(percent, p)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return total, percent
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADOutboundPlanner.evaluate(product, input)
|
|
||||||
local available, storagePercent = sumAvailable(product)
|
|
||||||
local percent = product.quote ~= nil and product.quote.percent or nil
|
|
||||||
return {
|
|
||||||
available = available,
|
|
||||||
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
|
|
||||||
storagePercent = storagePercent,
|
|
||||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
|
||||||
percent = percent,
|
|
||||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function wantsSell(policy) return policy == "sell" or policy == "sellElseStore" end
|
|
||||||
local function wantsStore(policy) return policy == "store" or policy == "sellElseStore" end
|
|
||||||
|
|
||||||
local function reasonFor(product, e, input)
|
|
||||||
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then return product.name .. ": intet på lager" end
|
|
||||||
if wantsSell(product.policy) and product.quote == nil and not (wantsStore(product.policy) and product.store ~= nil) then
|
|
||||||
return product.name .. ": intet nåeligt salgssted"
|
|
||||||
end
|
|
||||||
if wantsStore(product.policy) and product.store == nil and (e.fullLoad or e.storageHigh) and not e.priceOk then
|
|
||||||
return product.name .. ": ingen silo med plads"
|
|
||||||
end
|
|
||||||
local parts = {}
|
|
||||||
if wantsSell(product.policy) and e.percent ~= nil then
|
|
||||||
table.insert(parts, string.format("%d %% af top (kræver %d)", e.percent, input.sellAtPercent or 90))
|
|
||||||
end
|
|
||||||
table.insert(parts, string.format("lager %d %%", math.floor(e.storagePercent + 0.5)))
|
|
||||||
return product.name .. " " .. table.concat(parts, ", ")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- kandidater: {product, e, kind, targetWayPointId, targetName, score}
|
|
||||||
local function collectCandidates(input)
|
|
||||||
local sell, store, reasons = {}, {}, {}
|
|
||||||
for _, product in ipairs(input.products or {}) do
|
|
||||||
local e = ADOutboundPlanner.evaluate(product, input)
|
|
||||||
local added = false
|
|
||||||
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
|
|
||||||
if wantsSell(product.policy) and e.priceOk and product.quote ~= nil then
|
|
||||||
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
|
|
||||||
added = true
|
|
||||||
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then
|
|
||||||
table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name})
|
|
||||||
added = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not added then table.insert(reasons, reasonFor(product, e, input)) end
|
|
||||||
end
|
|
||||||
table.sort(sell, function(a, b)
|
|
||||||
if a.e.percent ~= b.e.percent then return a.e.percent > b.e.percent end
|
|
||||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
|
||||||
return a.product.name < b.product.name
|
|
||||||
end)
|
|
||||||
table.sort(store, function(a, b)
|
|
||||||
if a.e.storagePercent ~= b.e.storagePercent then return a.e.storagePercent > b.e.storagePercent end
|
|
||||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
|
||||||
return a.product.name < b.product.name
|
|
||||||
end)
|
|
||||||
return sell, store, reasons
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sortedSources(product)
|
|
||||||
local sources = {}
|
|
||||||
for _, source in ipairs(product.sources or {}) do
|
|
||||||
if (source.available or 0) >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then table.insert(sources, source) end
|
|
||||||
end
|
|
||||||
table.sort(sources, function(a, b) return a.available > b.available end)
|
|
||||||
return sources
|
|
||||||
end
|
|
||||||
|
|
||||||
local function buildTrip(primary, candidates, input)
|
|
||||||
local trip = {kind = primary.kind, fillTypes = {primary.product.fillType}, names = {primary.product.name}, sourceIds = {}, targetWayPointId = primary.targetWayPointId, targetName = primary.targetName, percent = primary.percent}
|
|
||||||
local members = {primary}
|
|
||||||
if input.collectRound and input.hasAutoload and primary.product.isPallet then
|
|
||||||
for _, candidate in ipairs(candidates) do
|
|
||||||
if candidate ~= primary and candidate.kind == primary.kind and candidate.targetWayPointId == primary.targetWayPointId and candidate.product.isPallet then
|
|
||||||
table.insert(members, candidate)
|
|
||||||
table.insert(trip.fillTypes, candidate.product.fillType)
|
|
||||||
table.insert(trip.names, candidate.product.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local seen = {}
|
|
||||||
for _, member in ipairs(members) do
|
|
||||||
local sources = sortedSources(member.product)
|
|
||||||
local limit = input.collectRound and #sources or math.min(1, #sources)
|
|
||||||
for index = 1, limit do
|
|
||||||
local id = sources[index].wayPointId
|
|
||||||
if not seen[id] then seen[id] = true; table.insert(trip.sourceIds, id) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return trip
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADOutboundPlanner.plan(input)
|
|
||||||
local sell, store, reasons = collectCandidates(input)
|
|
||||||
if sell[1] ~= nil then return buildTrip(sell[1], sell, input), reasons end
|
|
||||||
if store[1] ~= nil then return buildTrip(store[1], store, input), reasons end
|
|
||||||
return nil, reasons
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adOutboundPlanner — turvurdering, prisregel, politikker, opsamlingsrunde (ren)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: adSources — kilde-genkendelse og lagerstatus
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adSources.lua`
|
|
||||||
- Test: `tests/test_adSources.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADSources.MAX_DISTANCE = 40; ADSources.PALLET_RADIUS = 25
|
|
||||||
ADSources.identify(adEnv, wayPointId, farmId) -- {kind="factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} | nil
|
|
||||||
ADSources.getProducts(adEnv, source, farmId) -- { {fillType, name, available, level, capacity, isPallet} } (kun varer med kapacitet/indhold)
|
|
||||||
ADSources.isPalletFillType(fillType) -- fillTypeDesc.palletFilename ~= nil
|
|
||||||
ADSources.getSellMarkers(adEnv, farmId) -- { {marker, station} } salgssteder (ADPrices.isSellingStation)
|
|
||||||
ADSources.getObjectInfoIndex(source, fillType) -- objectInfo-indeks + antal for varen i et objektlager, eller nil
|
|
||||||
ADSources.spawnOne(source, objectInfoIndex) -- pcall placeable:removeAbstractObjectsFromStorage(index, 1, nil) -> ok
|
|
||||||
ADSources.countStored(source, fillType) -- antal objekter af varen i lageret
|
|
||||||
ADSources.getSourcePlaceables(sources) -- {[placeable]=true} til udelukkelse
|
|
||||||
```
|
|
||||||
Genkendelse: læssestation ved markøren (`ADSmartPickup.getLoadStationAtWayPointId`) med `owningPlaceable.spec_productionPoint` → factory; med `spec_silo` → silo; ellers nærmeste placeable med `spec_objectStorage` inden for 40 m af waypointet (`g_currentMission.placeableSystem.placeables`, `getWorldTranslation(placeable.rootNode)`, `canFarmAccess`) → objectStorage.
|
|
||||||
Fabrik-produkter: `for production in productionPoint.productions: for output in production.outputs: type` (unik); `level = productionPoint.storage:getFillLevel(ft)`, `capacity = storage:getCapacity(ft)` (fallback `storage.capacities[ft]`); paller i nærheden: `g_currentMission.vehicleSystem.vehicles` med `spec_pallet` eller `isPallet`, ejer = farm, fillUnit-varen = ft, afstand ≤ 25 m fra `productionPoint.palletSpawner.spawnPlaces[1].startNode` (fallback placeable.rootNode); `available = level + Σ palle-fyldning`.
|
|
||||||
Silo: `spec_silo.storages[*]` fillLevels > 0 → `level = getOwnStorageLevel`, `capacity = storage:getCapacity(ft)`.
|
|
||||||
Objektlager: `spec_objectStorage.objectInfos[i].objects[1]` med `palletAttributes.fillType/fillLevel` eller `baleAttributes.fillType/fillLevel`; `numObjects` → available = numObjects × fillLevel; level = `spec.numStoredObjects`, capacity = `spec.capacity`; isPallet = true.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** med fakes: (S1) markør ved fabrik → factory med SUGAR output level 150000, capacity 180000, ingen paller → available 150000; (S2) markør ved silo → silo med FORAGE 74500; (S3) markør uden station, objektlager 20 m væk → objectStorage, produkt STRAW_BASKET available 8×1000, isPallet true, `getObjectInfoIndex` = 1 og 8; (S4) paller ved spawner: 2 paller à 1000 STRAW_BASKET inden for 25 m + 1 uden for → available = storage + 2000; (S5) `getSellMarkers` giver kun markører hvis unload-station er SellingStation med isSellingPoint; (S6) `spawnOne` kalder `removeAbstractObjectsFromStorage(1, 1, nil)` (fake tæller); (S7) markør 60 m fra objektlager → nil.
|
|
||||||
- [ ] **Step 2–4:** implementér som beskrevet, kør, grønne. **Step 5: Commit** `feat: adSources — fabrik, silo og objektlager som kilder; paller ved spawner; salgssteder`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: adOutbound — hooks, hold, objektlager-udtag, runde, advarsler
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adOutbound.lua`
|
|
||||||
- Test: `tests/test_adOutbound.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADOutbound.CHECK_INTERVAL_MS = 5000; ADOutbound.NO_PROGRESS_MS = 15000; ADOutbound.SPAWN_STALL_MS = 30000
|
|
||||||
ADOutbound.isOutboundVehicle(vehicle) -- run for vognen har kind "outbound"
|
|
||||||
ADOutbound.choosePickup(adEnv, vehicle) -- kaldes fra getNextPickup-wrap; returnerer choice {markerIndex, markerName, group, station|nil, fillTypes, fillType} eller nil
|
|
||||||
ADOutbound.buildPlanInput(adEnv, run, vehicle) -- samler kilder/priser/lager til planlæggeren (spil-API)
|
|
||||||
ADOutbound.beforeLoadUpdate(adEnv, task, dt) -- true = håndteret (spring original update over)
|
|
||||||
ADOutbound.beforeLoadFinished(adEnv, task) -- true = fortsæt runde (spring original finished over)
|
|
||||||
ADOutbound.releaseInactive()
|
|
||||||
ADOutbound.getHoldFor(vehicle) -- {markerName, reasons} | nil
|
|
||||||
ADOutbound.getRunStatus(run) -- {reasons, warning} (seneste)
|
|
||||||
ADOutbound.install(adEnv) -- wrapper LoadAtDestinationTask.update/finished
|
|
||||||
```
|
|
||||||
|
|
||||||
Adfærd (fra spec afsnit 3–4):
|
|
||||||
- `choosePickup`: `trips[vehicle]=nil`; input = buildPlanInput; `trip, reasons = ADOutboundPlanner.plan(input)`; status[run.id] = reasons. Tur → `stateModule.selectedFillTypes = trip.fillTypes; setFillType(trip.fillTypes[1]); setSecondMarkerByWayPointId(trip.targetWayPointId); raiseDirtyFlag`; `trips[vehicle] = {run, trip, queue = copy(trip.sourceIds), visited = {}, current = trip.sourceIds[1]}`; log `'MT635' udkørsel: 8×STRAW_BASKET fra Straw factory → Halm-salg (95 % af top)`; returnér choice for første kilde (station = kildens læssestation hvis bulk, ellers nil). Ingen tur → `ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable=hasRouteTo)`; fundet → claim, `holds[vehicle] = {markerId, reasons}`, log én gang pr. ændret begrundelse, returnér choice for Wait-markøren (station nil); intet Wait-punkt → hold ved første kilde (warn én gang) og returnér den.
|
|
||||||
- `buildPlanInput`: for hver kilde i `run.sourceWayPointIds`: `ADSources.identify` (nil → spring over + warn én gang "kilde markør X ikke genkendt"); products = union over kilder af `ADSources.getProducts`, filtreret til `ADRuns.getProductPolicy ~= "off"` og til det vognen kan bære (`bulk: AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`, pallet: hasAutoload = `AutoDrive:hasAL(trailer)` for en af `getAllUnits`); pr. produkt med sell-politik: quotes over `ADSources.getSellMarkers` hvor `station.acceptedFillTypes[ft]` (eller `getIsFillTypeSupported`) og `ADWaitPool.hasRouteBothWays(adEnv, vehicle, marker.id, sourceId)` → `ADPrices.getQuote`; bedste = `ADPrices.bestQuote`; ingen nåelig men mindst én unåelig → warnOnce(vehicle, "sell:"..ft, "'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet"); pr. produkt med store-politik: `ADLoadSwap.findDumpMarker(adEnv, vehicle, ft, math.min(available, capacity), nil, isReachable, ADSources.getSourcePlaceables(sources))` → store; nil → warnOnce(vehicle, "store:"..ft, "'%s' finder ingen silo med plads til %s (%s)").
|
|
||||||
- `warnOnce(vehicle, key, text)`: `warned[vehicle][key] ~= text` → Logging.warning + pcall `adEnv.AutoDriveMessageEvent.sendMessageOrNotification(vehicle, adEnv.ADMessagesManager.messageTypes.ERROR, "ADSmartPickup: " .. text, 8000)`; status[run.id].warning = text. Ryddes når nøglens tilstand ændrer sig (ny tekst) eller turen lykkes.
|
|
||||||
- `beforeLoadUpdate`: hold → hvis `task.destinationID == hold.markerId` og `drivePathModule:isTargetReached()`: `specialDrivingModule:stopVehicle(); specialDrivingModule:update(dt)`; timer 5 s → `choosePickup` igen; tur → `ADWaitPool.releaseVehicle`, `holds[vehicle]=nil`, `ADWaitPool.redirect(task, trip.sourceIds[1])`, `task.activatedUALLoading=false`; returnér true. Ikke fremme → false.
|
|
||||||
Tur aktiv og `isTargetReached()`: kilde = `state.sources[task.destinationID]`; objektlager → `updateSpawn` (nedenfor); derefter fremdriftsvagt: `level = AutoDrive.getAllFillLevels(task.trailers)`; stiger → `noProgressMs=0`; ellers `+dt`; `> NO_PROGRESS_MS` og `level > 0` og ingen spawn i gang → `tryNextSourceOrFinish(task)`; returnér false (original kører altid, den aktiverer UAL og stopper vognen).
|
|
||||||
- `updateSpawn(state, task, source, dt)`: første gang: `index = ADSources.getObjectInfoIndex(source, currentFillType)`; hvis `task.activatedUALLoading` og vognen står (`vehicle.lastSpeedReal < 0.0005`): hvis ingen udestående spawn: `ualIsFull()` på en UAL-vogn → done; `countStored == 0` → skift til næste fillType i `trip.fillTypes` (nyt index) eller done; ellers `ADSources.spawnOne` → `pending = {stored = countStored, loaded = countLoaded(trailers), ms = 0}`; udestående: `countStored < pending.stored and countLoaded > pending.loaded` → pending=nil (næste); `pending.ms > SPAWN_STALL_MS` → done + warn "objektlager spawner ikke". `countLoaded` = antal nøgler i `spec_universalAutoload.loadedObjects` summeret over trailers. done → `task:finished()` (via original finished) og returnér.
|
|
||||||
- `tryNextSourceOrFinish(task)`: næste id i `state.queue` som ikke er besøgt, og fri kapacitet ≥ 10 % (`fillFreeCapacity/capacity`) → `visited[current]=true; current=next; ADWaitPool.redirect(task, next); task.activatedUALLoading=false; activePickups opdateres via ADSmartPickup.setActivePickup(vehicle, station, fillTypes)` (ny lille funktion i adSmartPickup); ellers `task:finished()`.
|
|
||||||
- `beforeLoadFinished`: kaldes fra wrap af `finished`; hvis der er tur, kilde ikke fuld (ikke `filledToUnload`) og en ubesøgt kilde med plads → redirect som ovenfor og returnér true (spring original over); ellers `trips[vehicle]=nil` og false.
|
|
||||||
- `releaseInactive`: for inaktive køretøjer: trips/holds ryddes, `ADWaitPool.releaseVehicle`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (fake AD-miljø som i `test_adRunsController.lua` + fakes for `ADSources`, `ADPrices`, `ADWaitPool`, `ADRunsController.getRunForVehicle`): (O1) tur → selectedFillTypes/2. markør sat, choice = kilde; (O2) ingen tur → Wait-markør claimet, hold registreret, choice = Wait; (O3) hold + 5 s + nu tur → redirect til kilde, hold frigivet; (O4) objektlager: spawn kaldes én ad gangen; stored falder + loaded stiger → næste spawn; `ualIsFull` → finished kaldt; (O5) spawn uden fremdrift 30 s → finished + WARN; (O6) runde bulk: kilde A fylder vognen (`filledToUnload`) → finished kaldt, ingen redirect; kilde A giver 30 % → redirect til B; (O7) intet nåeligt salgssted → `sendMessageOrNotification` kaldt én gang med ERROR, anden gang ikke; (O8) `releaseInactive` rydder hold og claim.
|
|
||||||
- [ ] **Step 2–4:** implementér, kør, grønne. **Step 5: Commit** `feat: adOutbound — udkørsels-hooks: turvalg, hold ved Wait, objektlager-udtag, opsamlingsrunde, advarsler på skærm`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Controller — udkørsels-runs
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRunsController.lua`
|
|
||||||
- Test: `tests/test_adRunsController.lua` (sektion C-out)
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADRunsController.createRun(name, kind)
|
|
||||||
ADRunsController.getSourceMarkers(adEnv, farmId) -- markører der er kilder: {marker, kind}
|
|
||||||
ADRunsController.getRunProducts(adEnv, run) -- { {fillType, name, title, policy, isPallet, available, percent|nil, stationName|nil} } sorteret på titel
|
|
||||||
ADRunsController.validateVehicle(adEnv, run, vehicle) -- outbound: reasons noSource | noTrailer | noProducts | needsAutoload | cannotCarry | farFromNetwork
|
|
||||||
ADRunsController.getOutboundStatus(adEnv, run) -- {products = getRunProducts, reasons, warning} (fra ADOutbound.getRunStatus)
|
|
||||||
```
|
|
||||||
`applyRunToVehicle`: `if run.kind == "outbound"`: mode P&D, `setFirstMarkerByWayPointId(run.sourceWayPointIds[1])`, `setSecondMarkerByWayPointId(run.sourceWayPointIds[1])`, `selectedFillTypes` = produkt-fillTypes vognen kan bære, `setFillType(first)`, loopCounter 0, useFolders true, rotateTargets ONLYPICKUP; ingen dump-logik. `getVehicleActivity`: `ADOutbound.getHoldFor(vehicle)` → `{kind="waiting", place=markerName, reason=reasons[1]}`. `updateRun` logger som før. `getRunTargets` for outbound returnerer `{}` (mål vælges pr. tur).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (C-out1) createRun kind; (C-out2) validate: ingen kilder → noSource; bulk-vogn og kun pallevarer → needsAutoload; ingen politik → noProducts; ok-tilfælde; (C-out3) startRun sætter 1./2. markør = kilde 1 og selectedFillTypes; (C-out4) getRunProducts slår politik op.
|
|
||||||
- [ ] **Step 2–4:** implementér, kør alle tests. **Step 5: Commit** `feat: controller — udkørsels-runs: start, validering, produkter, status`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Wiring i adSmartPickup, adUnloadWait, modDesc
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `adSmartPickup.lua` (source() nye gui-filer, `installHook`: outbound-gren i `getNextPickup`-wrap, `ADOutbound.install(adEnv)`, `ADSmartPickup.setActivePickup`, `update`: `ADOutbound.releaseInactive`), `adUnloadWait.lua` (`beforeUpdate`: return hvis `ADOutbound.isOutboundVehicle`), `modDesc.xml` (sourceFiles: adPrices, adOutboundPlanner, adSources, adWaitPool, adOutbound før adUnloadWait/adSmartPickup; version 1.12.0.0; beskrivelse EN udvidet med udkørsel).
|
|
||||||
|
|
||||||
`getNextPickup`-wrap, ny gren først:
|
|
||||||
```lua
|
|
||||||
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle(driver) then
|
|
||||||
local ok, choice = pcall(ADOutbound.choosePickup, adEnv, driver)
|
|
||||||
if not ok then
|
|
||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i udkørsel, bruger AutoDrives rotation: %s", tostring(choice))
|
|
||||||
activePickups[driver] = nil
|
|
||||||
return originalGetNextPickup(self, driver, forcedSkip)
|
|
||||||
end
|
|
||||||
if choice == nil then activePickups[driver] = nil; return originalGetNextPickup(self, driver, forcedSkip) end
|
|
||||||
activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType} or nil
|
|
||||||
self.pickups[choice.group] = self.pickups[choice.group] or {}
|
|
||||||
self.pickups[choice.group].lastTarget = choice.markerIndex
|
|
||||||
self.pickups[choice.group].lastVehicle = driver
|
|
||||||
return choice.markerIndex
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 1:** ret filer. **Step 2:** `luajit tests/test_adSmartPickup.lua` grøn; `for t in tests/test_*.lua; do luajit $t | tail -1; done`. **Step 3: Commit** `feat: udkørsel koblet på AutoDrive-hooks; Wait-logik springer udkørsels-traktorer over`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: GUI — tre faner
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `gui/SmartPickupFrame.lua` (faner, fælles), `gui/SmartPickupFrame.xml` (nye statiske rækker + productPrefab + sourcePrefab), `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
|
||||||
- Create: `gui/SmartPickupSupplyTab.lua` (flyt: fillTargetRows, fillNeedsRows, refreshNeedsRows, fillIngredientRows, describeTargetNeeds, onClickTarget/IngredientMode/Ingredient/Loops/ReturnBelow, `SmartPickupFrame:rebuildSupplyTab(adEnv, run)`), `gui/SmartPickupOutboundTab.lua` (`rebuildOutboundTab(adEnv, run)`: Kilder-rækker (ingredientPrefab med kind="source"), Varer-rækker (productPrefab: MultiTextOption med 4 tilstande Fra/Lager/Salg/Salg, ellers lager + titel "Sukker — 150.0k · 78 % af top (Grain Elevator)"), Regler (sellAtOption 50..100 trin 5, storeAboveOption 50..95 trin 5, collectRoundOption), status-/advarselsrække; callbacks onClickSource/onClickPolicy/onClickSellAt/onClickStoreAbove/onClickCollectRound; `describeOutboundOverview(run)` til Oversigt).
|
|
||||||
- `SmartPickupFrame`: `TAB_OVERVIEW, TAB_SUPPLY, TAB_OUTBOUND = 1, 2, 3`, `TAB_NAMES = {"spu_tab_overview","spu_tab_supply","spu_tab_outbound"}`, `tabKind = {nil, "supply", "outbound"}`; `fillRunOption` viser kun runs med fanens kind; `onNewRunName` opretter med fanens kind (Oversigt → supply); `setStaticRowsVisible(tab)` viser forsynings-rækker (ingredientMode, waitPool, loops, returnBelow) kun på fane 2, udkørsels-rækker (waitPool, sellAt, storeAbove, collectRound) kun på fane 3 (waitPool er fælles); blok-overskrifter via `addTextRow` ("— Mål og varer —" stil: brug tekstnøglerne `spu_section_*`). `refreshLive` opdaterer også produktrækkernes titler (pris-%) hvert sekund og udkørsels-status.
|
|
||||||
- l10n (da/en): `spu_tab_supply` "Forsyning"/"Supply", `spu_tab_outbound` "Udkørsel"/"Outbound", `spu_sources` "Kilder (fabrik, silo eller objektlager)", `spu_products` "Varer", `spu_policy_off/store/sell/sellElseStore` "Fra/Lager/Salg/Salg, ellers lager", `spu_sellAt` "Sælg fra (% af årets toppris)", `spu_sellAt_tooltip`, `spu_storeAbove` "Kør til lager over", `spu_storeAbove_tooltip`, `spu_collectRound` "Saml op på én tur", `spu_collectRound_tooltip`, `spu_section_run/targets/rules/wait/vehicles/status`, `spu_reason_noSource` "ingen genkendt kilde", `spu_reason_noProducts` "ingen vare valgt", `spu_reason_needsAutoload` "pallevarer kræver Universal Autoload på vognen", `spu_product_row` "%s — %s · %s", `spu_price_pct` "%d %% af top (%s)", `spu_price_none` "intet salgssted", `spu_outbound_waiting` "venter: %s", `spu_activity_holding` "holder ved %s (%s)".
|
|
||||||
- [ ] **Step 1:** XML + l10n; **Step 2:** split + ny fane; **Step 3:** `luajit` alle tests (GUI testes in-game); **Step 4: Commit** `feat(gui): faner Oversigt/Forsyning/Udkørsel; udkørsels-run med kilder, varepolitik, regler`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: README, version, byg, deploy, in-game
|
|
||||||
|
|
||||||
- [ ] README: nyt afsnit "Udkørsel (v1.12)" (kilder, politikker, prisregel, lagertærskel, runde, objektlager-krav om markørplacering, advarsler). modDesc 1.12.0.0.
|
|
||||||
- [ ] `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cp ../FS25_ADSmartPickup.zip ~/FS25-data/mods/`
|
|
||||||
- [ ] Commit `chore: v1.12.0.0 — udkørsel af produkter` + push til Gitea (`git push gitea main` med token fra vault).
|
|
||||||
- [ ] In-game-test (bruger): savegame 3 — se spec afsnit 4.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
|||||||
# Behovsstyret forsyning ("Feed the factory") — design
|
|
||||||
|
|
||||||
Dato: 2026-09-21 · Status: godkendt af bruger
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
En produktion med flere input (fx TMR-blander: hø, græs, ensilage, sukkerroer) kræver i dag
|
|
||||||
én AutoDrive-traktor pr. vare, fordi AD's Pickup&Deliver kun kører én vare fra ét sted.
|
|
||||||
Varerne ligger spredt over mange lokationer på kortet.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Én traktor holder alle input fyldt. For hver tur vælger moden den ingrediens aflæsningsmålet
|
|
||||||
har mest brug for, finder en silo der har den, skifter AD's vare og sender traktoren derhen.
|
|
||||||
|
|
||||||
### Betjening (ingen ny GUI, ingen config-fil)
|
|
||||||
|
|
||||||
1. Aflæsningsmål = produktionens markør.
|
|
||||||
2. Multi-vælg ingredienserne i AD's vare-dropdown (>1 vare = funktionen er aktiv).
|
|
||||||
3. "Brug mapper" + "Rotér mål: pålæsning" slået til (krævet for at AD kalder `getNextPickup`).
|
|
||||||
4. Start Pickup&Deliver. AD's loop-tæller virker uændret (N læs → parkér, 0 = uendeligt).
|
|
||||||
|
|
||||||
Med præcis én valgt vare er opførslen uændret (eksisterende mappe-rotation, mindst fyldte først).
|
|
||||||
|
|
||||||
### Logik pr. tur (i `getNextPickup`-wrapperen)
|
|
||||||
|
|
||||||
1. **Ingrediensliste pr. køretøj.** Ved >1 valgt vare gemmes listen i modens egen tilstand
|
|
||||||
(`vehicle → {ingredients, narrowedTo}`). Derefter indsnævres AD's `selectedFillTypes` til den
|
|
||||||
valgte vare, fordi AD ellers selv roterer vare ved triggeren
|
|
||||||
(`ADStateModule:selectPreferredFillTypeFromFillLevels`).
|
|
||||||
Ved næste kald: er AD's valg stadig `{narrowedTo}` → brug den gemte liste. Er det noget andet →
|
|
||||||
brugeren har ændret valget: ny multi-liste erstatter, enkelt-valg sletter tilstanden.
|
|
||||||
2. **Behov.** Find `UnloadingStation` nærmest anden markør (≤ 40 m). For hver ingrediens summeres
|
|
||||||
`level`, `capacity` over `station.targetStorages`; `free = capacity - level`.
|
|
||||||
Ingredienser stationen ikke understøtter, eller med `free < MIN_FREE_LITERS`, springes over.
|
|
||||||
3. **Rangering.** `ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)` → højeste score først.
|
|
||||||
4. **Kilde.** For ingredienserne i behovs-rækkefølge: blandt *alle* AD-markører (ikke kun én mappe)
|
|
||||||
findes markører med farm-tilgængelig `LoadingStation` ≤ 40 m der har varen. Aflæsningsmålets
|
|
||||||
egen placeable udelukkes som kilde. Rangering som eksisterende: eget lager før nabo-lån,
|
|
||||||
mindst fyldte først. Første ingrediens med en kilde vinder.
|
|
||||||
5. **Effekt.** `setFillType(vare)`, `selectedFillTypes = {vare}`, dirty-flag, returnér markørens
|
|
||||||
`markerIndex`. Logges som `ADSmartPickup: '<traktor>' -> <markør> (<vare>, behov …)`.
|
|
||||||
6. **Fallback.** Intet behov / ingen kilde / ingen station / fejl (`pcall`) → AutoDrives
|
|
||||||
originale `getNextPickup` (ved fejl: `Logging.warning`). Aldrig crash.
|
|
||||||
|
|
||||||
## Filer
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `FS25_ADSmartPickup/adSupplyPlanner.lua` (ny) | Rene funktioner: `scoreNeed`, `rankNeeds`, `resolveIngredients` (tilstandslogik). Ingen spil-API. |
|
|
||||||
| `FS25_ADSmartPickup/adSmartPickup.lua` | Spil-/AD-adaptere (station ved markør, lagerniveauer, kilde-søgning) + hook. Kilde-søgning generaliseres til at tage en markørliste. |
|
|
||||||
| `FS25_ADSmartPickup/modDesc.xml` | Ny sourceFile før `adSmartPickup.lua`, version 1.1.0.0, beskrivelse. |
|
|
||||||
| `tests/test_adSupplyPlanner.lua` (ny) | Enhedstests af de rene funktioner. |
|
|
||||||
| `tests/test_adSmartPickup.lua` | Integration gennem hook'en med mocks; eksisterende 11 tests skal forblive grønne. |
|
|
||||||
|
|
||||||
## Tests (luajit, mocks)
|
|
||||||
|
|
||||||
- mest trængende ingrediens vælges; vare og AD-valg skiftes; rigtig markør returneres
|
|
||||||
- ingrediens uden plads i målet springes over
|
|
||||||
- ingrediens uden kilde springes over → næste i rækken
|
|
||||||
- kilde i anden mappe end første markør findes
|
|
||||||
- aflæsningsmålets egen station bruges ikke som kilde
|
|
||||||
- gemt liste genbruges når AD's valg er indsnævret; brugerændring nulstiller/erstatter
|
|
||||||
- én vare valgt → eksisterende opførsel uændret
|
|
||||||
- intet behov / ingen aflæsningsstation / API-fejl → `ORIGINAL` (+ WARN ved fejl)
|
|
||||||
|
|
||||||
## Kendte begrænsninger
|
|
||||||
|
|
||||||
- AD læsser fuld vogn; er der mindre plads end ét læs, venter traktoren ved aflæsning (AD-standard).
|
|
||||||
- Ingredienslisten lever kun i hukommelsen — efter savegame-reload multi-vælges varerne igen.
|
|
||||||
- Forudsætter at målet er en placeable med aflæsningstrigger (produktionspunkt/silo/stald),
|
|
||||||
ikke en blandevogn. Bekræftes ved første in-game test via log.txt.
|
|
||||||
- Server-side logik; MP-klienter ser vareskiftet via AD's eget dirty-flag.
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# Runs-UI ("Cow Feed") — design
|
|
||||||
|
|
||||||
Dato: 2026-09-22 · Status: godkendt af bruger (sektion 1–3) · Version: v1.8
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Al opsætning sker i dag pr. traktor i AutoDrives HUD: mål, multi-valg af varer (tændknap for
|
|
||||||
behovsstyring), mapper + rotation, loop-tæller. Med fire ens "Fastrac 2135" til to stalde og flere
|
|
||||||
Wait-puljer på vej (Cow/Pig/Sheep Wait) mangler overblik og central kontrol: *hvad* kører *hvor*
|
|
||||||
med *hvilke varer*, og hvor venter de.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Et **run** er den centrale enhed: navn, aflæsningsmål, varer, Wait-pulje, tildelte traktorer,
|
|
||||||
loop-tæller. En ESC-menu-fane ("Smart Pickup") viser og redigerer runs og starter/stopper dem.
|
|
||||||
"Start" skriver run'et ind i hver traktors AutoDrive-tilstand og kalder AD's start — alt det
|
|
||||||
eksisterende (behovsstyring, fabriks-inputs, Wait-punkter) virker uændret ovenpå, fordi det
|
|
||||||
læser præcis den tilstand. Traktorer uden run kører som i dag.
|
|
||||||
|
|
||||||
Valgt tilgang: **run-lag oven på AutoDrives tilstand** (fravalgt: egen dispatcher der kalder AD's
|
|
||||||
lav-niveau-opgaver — genopfinder P&D og knækker ved AD-opdateringer; konfig-fil uden UI — ikke det
|
|
||||||
brugeren bad om).
|
|
||||||
|
|
||||||
## 1. Datamodel og lagring
|
|
||||||
|
|
||||||
```
|
|
||||||
Run
|
|
||||||
id løbenummer (stabilt, unikt i savegame)
|
|
||||||
name "Cow Feed"
|
|
||||||
targetWayPointId AD-waypoint-id for mål-markøren (stald/fabrik); vises som markørnavn
|
|
||||||
ingredientMode "auto" | "manual"
|
|
||||||
ingredients liste af fillType-NAVNE (kun ved manual), fx {"STRAW","FORAGE"}
|
|
||||||
waitPoolGroup AD-mappenavn (fx "Cow Wait"); tom streng = alle Wait-mapper, nærmeste ledige
|
|
||||||
loops 0 = uendeligt, N = antal ture (AD's loop-tæller)
|
|
||||||
vehicleIds liste af køretøjs-referencer (se nedenfor)
|
|
||||||
|
|
||||||
Runtime (ikke gemt):
|
|
||||||
isRunning afledt: mindst én tildelt traktor har AD aktiv og run-medlemskab
|
|
||||||
runMembership vehicle -> run.id (svagt nøglet tabel)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Køretøjs-reference:** AD's `uniqueId` (streng) når den findes på køretøjet, ellers savegame-id
|
|
||||||
(`vehicle.currentSavegameId`). Navne bruges aldrig som nøgle (fire hedder "Fastrac 2135 4WS").
|
|
||||||
- **WaitPool** er afledt, ikke gemt: alle AD-mapper med ordet "Wait" i navnet (`ADSupplyPlanner.isWaitMarker`).
|
|
||||||
Omdøbning sker i AutoDrive; vi læser kun.
|
|
||||||
- **Fil:** `savegameN/ADSmartPickup_runs.xml`, samme mappe som `AutoDrive_config.xml`. Skrives i
|
|
||||||
spillets gem-hook (som AD), læses ved `loadMap`. Ukendte køretøjs-referencer droppes med loglinje;
|
|
||||||
run'et beholdes. Runs der kørte ved gem genstartes IKKE ved load (AD gendanner selv aktive køretøjer).
|
|
||||||
- **Regler:** en traktor kan kun være i ét run (tildeles den et andet, flyttes den — UI'et siger det).
|
|
||||||
Singleplayer først; multiplayer kræver events og er uden for scope.
|
|
||||||
|
|
||||||
XML-format:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<ADSmartPickupRuns version="1">
|
|
||||||
<run id="1" name="Cow Feed" target="11066" ingredientMode="manual" ingredients="STRAW FORAGE"
|
|
||||||
waitPool="Cow Wait" loops="0">
|
|
||||||
<vehicle id="..."/>
|
|
||||||
<vehicle id="..."/>
|
|
||||||
</run>
|
|
||||||
</ADSmartPickupRuns>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. UI — ESC-fane "Smart Pickup"
|
|
||||||
|
|
||||||
Forbillede: FarmOperationsDashboard (`TabbedMenuFrameElement`, XML-layout med `SmoothList`,
|
|
||||||
`MultiTextOption`, `Button`; registreret via `g_gui:loadGui` + indsættelse i `InGameMenu.pagingElement`).
|
|
||||||
|
|
||||||
```
|
|
||||||
┌ Smart Pickup ─────────────────────────────────────────────────────────────┐
|
|
||||||
│ [Runs] [Wait-puljer] (under-faner) │
|
|
||||||
├──────────────────┬────────────────────────────────────────────────────────┤
|
|
||||||
│ RUNS │ RUN: Cow Feed ● Kører (2/2) │
|
|
||||||
│ ▶ Cow Feed 2/2 │ Mål: [Cow 1 Food ▼] (AD-markør) │
|
|
||||||
│ Cow 2 Feed 1/2 │ Varer: (•) Auto: alt målet tager / fabrikken │
|
|
||||||
│ TMR Fabrik 1/1 │ ( ) Vælg: [x] Halm [x] TMR [ ] Vand │
|
|
||||||
│ │ Wait-pulje: [Cow Wait ▼] (6 punkter)│
|
|
||||||
│ [+ Nyt run] │ Ture (loop): [0 = uendeligt ◄►] │
|
|
||||||
│ [Slet run] │ Traktorer: │
|
|
||||||
│ │ Fastrac 2135 (#1) vogn: 1.000.000 l halm+TMR ✔ │
|
|
||||||
│ │ Fastrac 2135 (#2) vogn: — INGEN VOGN ✖ │
|
|
||||||
│ │ [+ Tilføj traktor ▼] [Fjern] │
|
|
||||||
│ │ [▶ Start run] [■ Stop run] Status: 1 advarsel │
|
|
||||||
└──────────────────┴────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Runs-liste (venstre):** alle runs med "kører X af Y". Valg → detaljer til højre. `+ Nyt run`
|
|
||||||
opretter "Run N" (navn redigeres via AD-lignende tekstdialog). `Slet run` spørger ja/nej.
|
|
||||||
- **Mål:** dropdown over AD-markører der ligger ved en aflæsningsstation (`getUnloadStationAtMarker`
|
|
||||||
≠ nil) — ikke alle markører.
|
|
||||||
- **Varer:** *Auto* = det eksisterende: fabrik → ønskede produktioners inputs (v1.6); stald → alle
|
|
||||||
varer stalden tager (`getHusbandryIsFillTypeSupported`); andet mål → stationens understøttede varer.
|
|
||||||
*Vælg* = afkrydsning blandt målets mulige varer.
|
|
||||||
- **Wait-pulje:** dropdown "Alle (nærmeste ledige)" + hver mappe med "Wait" i navnet (antal punkter).
|
|
||||||
- **Ture:** 0–99.
|
|
||||||
- **Traktorer:** dropdown over farmens køretøjer med `vehicle.ad` (navn + `#løbenummer`). Pr. række:
|
|
||||||
vogn-kapacitet (AD `getAllFillLevels`), hvilke af run'ets varer vognen kan bære, og ✔/✖ med tekst:
|
|
||||||
"ingen vogn", "kan ikke bære: TMR", "N m fra vejnettet", "i run X (flyttes)".
|
|
||||||
- **Start/Stop** gælder hele run'et; ✖-traktorer springes over med besked i rækken.
|
|
||||||
- **Wait-puljer-fanen:** liste over Wait-mapper: antal punkter, hvilke traktorer holder hvor lige nu
|
|
||||||
(fra `adUnloadWait`'s reservationer). Kun visning.
|
|
||||||
- Live-opdatering hvert sekund mens siden er åben. Dansk + engelsk l10n. Ingen nye tastaturgenveje.
|
|
||||||
|
|
||||||
## 3. Start/Stop, integration, fejl og test
|
|
||||||
|
|
||||||
**Start run** (pr. tildelt traktor):
|
|
||||||
1. Validér: vogn tilkoblet og kan bære ≥1 af run'ets varer (`AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`);
|
|
||||||
≤ 30 m fra vejnettet (`ADGraphManager:getDistanceFromNetwork`); mål-markør findes. Fejl → ✖ på rækken,
|
|
||||||
traktoren springes over, run'et fortsætter med de andre.
|
|
||||||
2. Skriv AD-tilstand: `stateModule:setMode(AutoDrive.MODE_PICKUPANDDELIVER)`;
|
|
||||||
`setSecondMarkerByWayPointId(target)`; `setFirstMarkerByWayPointId(nærmeste markør med læssestation)`
|
|
||||||
(kun en gyldig start — moden vælger reel kilde pr. tur); `selectedFillTypes` = run'ets varer
|
|
||||||
(Auto: udledt liste) + `raiseDirtyFlag`; loop-tæller; per-køretøj `vehicle.ad.settings.useFolders`
|
|
||||||
= til og `rotateTargets` = kun pålæsning (`current` og `new`).
|
|
||||||
3. `vehicle:startAutoDrive()`.
|
|
||||||
4. `runMembership[vehicle] = run.id`.
|
|
||||||
|
|
||||||
**Stop run:** `vehicle:stopAutoDrive()` pr. tildelt traktor; medlemskab bevares (konfigureret, ikke kørende).
|
|
||||||
|
|
||||||
**Integration:** de eksisterende hooks læser AD-tilstanden uændret. Run'et læses to steder:
|
|
||||||
(a) `adUnloadWait`: Wait-kandidater begrænses til run'ets mappe, hvis sat; (b) `choosePickup`:
|
|
||||||
for run-traktorer tages ingredienslisten fra run'et (Auto/manual) i stedet for "≥2 valgt i dropdown",
|
|
||||||
så tændknappen ikke længere kræves. Traktorer uden run: præcis som i dag.
|
|
||||||
|
|
||||||
**Fejlhåndtering:** al UI-kode og Start/Stop i `pcall`; fejl vises i status-linjen og logges med
|
|
||||||
`ADSmartPickup:`-præfiks; aldrig crash. Manglende AD-API → fanen viser "AutoDrive-API ikke fundet",
|
|
||||||
resten af moden upåvirket.
|
|
||||||
|
|
||||||
**Test:** rene funktioner i luajit (run-model: opret/omdøb/tildel/flyt; validering; Auto-vareliste;
|
|
||||||
XML ↔ tabel; kandidat-filtrering pr. Wait-pulje). GUI og AD-skrivning verificeres in-game efter
|
|
||||||
tjekliste: opret run → tildel 2 traktorer (én uden vogn) → Start (én ✖) → Stop → gem/load → run bevaret.
|
|
||||||
|
|
||||||
**Afgrænsning (YAGNI):** singleplayer; ingen kilde-begrænsning pr. run; ingen omdøbning af
|
|
||||||
Wait-mapper; ingen auto-genstart ved load; ingen genvejstast-dialog.
|
|
||||||
|
|
||||||
## Filer (forventet)
|
|
||||||
|
|
||||||
- `FS25_ADSmartPickup/adRuns.lua` — run-model + XML (rene funktioner hvor muligt)
|
|
||||||
- `FS25_ADSmartPickup/adRunsController.lua` — Start/Stop, validering mod AD/spil-API, medlemskab
|
|
||||||
- `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` + `gui/SmartPickupFrame.xml` + `gui/guiProfiles.xml`
|
|
||||||
- `FS25_ADSmartPickup/l10n/l10n_da.xml`, `l10n_en.xml`
|
|
||||||
- `tests/test_adRuns.lua`
|
|
||||||
- Tilpasninger i `adSmartPickup.lua` (ingredienser fra run) og `adUnloadWait.lua` (pulje-filter)
|
|
||||||
|
|
||||||
## Efterfølgende (ikke i v1.8): læs-bytte fra Wait
|
|
||||||
|
|
||||||
In-game 2026-09-22 21:55: begge stalde fulde af halm, mangler TMR; alle fire traktorer venter ved
|
|
||||||
Cow Wait med vognene fulde af halm → ingen kan hente TMR (deadlock).
|
|
||||||
|
|
||||||
Design: mens en traktor venter, tjekkes målets behov. Hvis målet er fuldt for vognens vare, og en
|
|
||||||
ANDEN vare i traktorens ingrediensliste mangler så meget at et fuldt læs passer (hysterese), og
|
|
||||||
vognen kan bære den: find en silo (lager-placeable med aflæsningstrigger ved en AD-markør, fri plads
|
|
||||||
≥ rest, rute til/fra) — foretræk den moden ellers henter varen fra — og omdiriger aflæsningsopgaven
|
|
||||||
dertil. AD læsser af, vognen er tom, næste tur vælger moden den manglende vare efter behov. Log:
|
|
||||||
"bytter læs: N l STRAW tilbage i <silo>, målet mangler FORAGE". Ingen silo → bliv ved Wait.
|
|
||||||
Kræver at traktoren har begge varer i sin liste (run eller multi-valg).
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# Flow-tavle — design
|
|
||||||
|
|
||||||
Dato: 2026-09-23 · Status: godkendt af bruger (grundform valgt: flow-tavle) · Version: v1.13
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Opsætningen er markør-baseret: brugeren vælger AD-markører som kilder og mål, og moden gætter
|
|
||||||
bygningen ud fra markørens geometri (trigger bag næsen, 40 m, 12 m). Fejl viser sig som
|
|
||||||
"kilde-markør 10873 er ikke …" i stedet for "Kostald 1", først når run'et starter. Forsyning og
|
|
||||||
Udkørsel er to faner med hver sin lange rulleside, Oversigt er en tredje. Brugeren: "det er noget rod".
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Én tavle med ét flow pr. række: **Vare · Fra · Til · Regel · Traktorer · Status**. Fra og Til vælges
|
|
||||||
som **bygninger**; moden finder selv markøren ved bygningens læsse-/aflæsningstrigger og viser den.
|
|
||||||
Redigering sker i en detail-blok under tavlen for det valgte flow. Faner: **Flows** · **Lagre**.
|
|
||||||
Motoren (planlægger, hooks, XML-model) er uændret: et flow ER et run; bygninger oversættes til
|
|
||||||
markører når run'et startes og planlægges.
|
|
||||||
|
|
||||||
Fravalgt: "Bygninger først" (runs implicit → svært at se hvad der kører) og "Sætnings-run"
|
|
||||||
(mindst ændring, men fanerne forbliver adskilte).
|
|
||||||
|
|
||||||
## 1. Bygninger (`adBuildings.lua`, spil-adapter)
|
|
||||||
|
|
||||||
`ADBuildings.list(adEnv, farmId)` → liste af bygninger gården kan bruge:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ id, name, kind = "factory"|"silo"|"husbandry"|"objectStorage"|"sell",
|
|
||||||
placeable, loadMarker = {id, name, distance} | nil, unloadMarker = {…} | nil,
|
|
||||||
canBeSource = bool, canBeTarget = bool, canSell = bool }
|
|
||||||
```
|
|
||||||
|
|
||||||
- Kilde: fabrik, silo, stald (læssestation), objektlager. Mål (forsyning): stald, fabrik.
|
|
||||||
Salg: SellingStation der er et rigtigt salgssted (ikke fabrikkers/staldes indleveringer).
|
|
||||||
Siloer og palleskure er automatiske lagermål (som i dag) og vælges ikke.
|
|
||||||
- `id` = `placeable.uniqueId`; mangler det, `"pos:" .. afrundet x/z`.
|
|
||||||
- Markør-opslag: for hver AD-markør (Wait-markører udelukket) afstanden til bygningens
|
|
||||||
læsse-triggere (loadTriggers.triggerNode) og aflæsnings-triggere (unloadTriggers exactFillRootNode/
|
|
||||||
triggerNode/aiNode) samt objektlagerets/pallespawnerens node; nærmeste ≤ 40 m vinder.
|
|
||||||
Siloer bruger det strenge opslag (bag næsen) for læssemarkøren, som i dag.
|
|
||||||
- Én scanning pr. sekund højst (cache), da fanen spørger ved hver opdatering.
|
|
||||||
- `ADBuildings.find(adEnv, farmId, id)`, `ADBuildings.byMarker(adEnv, farmId, wayPointId)` (omvendt
|
|
||||||
opslag til migration).
|
|
||||||
|
|
||||||
## 2. Model
|
|
||||||
|
|
||||||
Run får `sourceBuildingIds` og `targetBuildingIds` (lister af bygnings-id). De eksisterende
|
|
||||||
`sourceWayPointIds`/`targetWayPointIds` bevares som **afledte** felter: controlleren udfylder dem fra
|
|
||||||
bygningerne ved Start og ved hver plan (`ADRunsController.resolveMarkers(adEnv, run)`), så
|
|
||||||
planlægger og hooks er uændrede. XML-version 3 skriver `#sourceBuildings`/`#targetBuildings`;
|
|
||||||
version 2-filer læses, og bygninger udledes fra markørerne ved første indlæsning
|
|
||||||
(`ADBuildings.byMarker`); kan en markør ikke oversættes, beholdes den som rå markør.
|
|
||||||
|
|
||||||
Forsyning: Fra = "alle siloer" (ingen valg; sources er alle markører som i dag). Til = bygninger.
|
|
||||||
Udkørsel: Fra = bygninger. Til = politik pr. vare (Lager/Salg/Salg, ellers lager) — målet vælges
|
|
||||||
automatisk pr. tur som i dag.
|
|
||||||
|
|
||||||
## 3. Fanen Flows
|
|
||||||
|
|
||||||
**Tavle** (øverst, tekstrækker, én pr. run, den valgte markeret med "▶"):
|
|
||||||
`Mælk · Kostald 1 + Kostald 2 → Mejeri (salg) · pris ≥ 90 % · Virtus · venter: 42 % af top`
|
|
||||||
- Vare: run'ets varer (forsyning auto = "behov"), Fra/Til = bygningsnavne (+ "!" hvis en bygning
|
|
||||||
mangler markør), Regel = kort tekst, Traktorer = navne, Status = én linje (som Oversigt i dag:
|
|
||||||
kører/holder/venter/begrundelse/advarsel).
|
|
||||||
- Under tavlen: Wait-puljer (som i dag) og en linje "Vælg flow ovenfor for at rette".
|
|
||||||
|
|
||||||
**Detail-blok for valgt flow** (efter tavlen; run-vælgeren `Flow:` er en MultiTextOption):
|
|
||||||
- *Retning*: Ind til bygning / Ud fra bygning (klonet Ja/Nej-række).
|
|
||||||
- *Fra* (kun Ud): afkrydsning pr. bygning der kan være kilde: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `— INGEN MARKØR inden for 40 m`.
|
|
||||||
- *Til* (kun Ind): afkrydsning pr. stald/fabrik med samme markørtekst.
|
|
||||||
- *Varer*: Ind: Auto/Vælg + afkrydsning; Ud: politik pr. vare med mængde og pris-% (som i dag).
|
|
||||||
- *Regler*: Ind: ture, kør hjem under; Ud: sælg fra, lager over, saml op.
|
|
||||||
- *Vent ved*: Wait-pulje.
|
|
||||||
- *Traktorer*: som i dag (låst når vognen ikke passer).
|
|
||||||
- Knapper: Nyt flow / Omdøb / Slet / Start-Stop (som i dag). Nyt flow spørger navn og starter som "Ind".
|
|
||||||
|
|
||||||
Fanen **Lagre** uændret. Oversigt og de to gamle faner udgår.
|
|
||||||
|
|
||||||
## 4. Validering og fejl
|
|
||||||
|
|
||||||
- Bygning uden markør: rækken siger det, tavlen viser "!" ved navnet, Start afviser med
|
|
||||||
`noMarker:<bygning>` i statuslinjen ("Kostald 2 mangler en AD-markør ved mælketanken").
|
|
||||||
- Bygning der er forsvundet (solgt): udgår af listen; run'et beholder id'et og viser "(mangler)".
|
|
||||||
- Alle eksisterende afvisningsgrunde uændrede.
|
|
||||||
|
|
||||||
## 5. Tests
|
|
||||||
|
|
||||||
- `adBuildings`: kind pr. placeable, markør-opslag (nærmeste ≤ 40 m, Wait udelukket, silo strengt),
|
|
||||||
byMarker, id-fallback.
|
|
||||||
- model: v3 roundtrip, v2-migration med byMarker-oversættelse, rå markør bevares.
|
|
||||||
- controller: resolveMarkers udfylder waypoint-lister; noMarker-validering; Start med bygninger.
|
|
||||||
- GUI in-game (savegame 3 og 2).
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# Udkørsel af produkter — design
|
|
||||||
|
|
||||||
Dato: 2026-09-23 · Status: godkendt af bruger (afsnit 1–4) · Version: v1.12
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Modden styrer i dag kun forsyning *ind* til stalde og fabrikker. Fabrikkernes output (sukker,
|
|
||||||
TMR, halmvarer på paller) og varer der står på lager skal køres *ud*: til en silo som
|
|
||||||
mellemlager, eller til et salgssted — men kun når prisen er god, så man ikke sælger i bund.
|
|
||||||
Uden det løber fabrikkernes udgangslager fuldt, og produktionen stopper. Paller ligger ofte i
|
|
||||||
objektlagre (fx HVF-pakkens palle-/ballelager) hvor man normalt klikker "tag ud" manuelt.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Et run får en **retning**: *Forsyning* (som i dag) eller *Udkørsel*. Et udkørsels-run har en
|
|
||||||
liste af **kilder** (fabrik, silo eller objektlager), en **vareliste** med politik pr. vare
|
|
||||||
(Lager / Salg / Salg, ellers lager / Fra), en **pristærskel** (% af årets toppris), en
|
|
||||||
**lagertærskel** (% fyldning) og valget **opsamlingsrunde**. Før hver tur vurderer en ren
|
|
||||||
planlægger om en tur er berettiget, vælger vare(r) + mål, og sætter AutoDrives tilstand;
|
|
||||||
AutoDrive kører, læsser (også paller via Universal Autoload) og læsser af. Ingen tur → traktoren
|
|
||||||
holder ved et Wait-punkt og prøver igen hvert 5. sekund.
|
|
||||||
|
|
||||||
Valgt tilgang: nyt modul der wrapper AutoDrives læsseopgave (samme mønster som Wait-modulet
|
|
||||||
wrapper aflæsseopgaven). Fravalgt: AutoDrives egen "automatisk aflæsningsmål" (ingen pristærskel,
|
|
||||||
ingen lager-mulighed, ingen ventelogik) og egen kørselsstyring med DriveTo-opgaver (genopfinder
|
|
||||||
AutoDrives læsse-/pallehåndtering).
|
|
||||||
|
|
||||||
## 1. Faner og datamodel
|
|
||||||
|
|
||||||
### Faner i ESC-siden
|
|
||||||
|
|
||||||
1. **Oversigt** (som i dag): alle runs med status, traktorernes aktivitet, behovsrække. Udkørsels-
|
|
||||||
runs vises med prislinje i stedet for behov: `SUGAR 412 kr (78 % af top) → venter`.
|
|
||||||
2. **Forsyning**: de nuværende runs, samme indhold, grupperet i blokke med overskrift:
|
|
||||||
*Run* (vælger + Nyt/Omdøb/Slet) · *Mål og varer* · *Ventepunkter og ture* · *Traktorer* ·
|
|
||||||
*Status* (+ Start/Stop).
|
|
||||||
3. **Udkørsel**: *Run* · *Kilder* (liste af markører, tilføj/fjern) · *Varer* (én række pr. vare:
|
|
||||||
navn, politik, aktuel pris i % af top) · *Regler* (pristærskel 50–100, standard 90;
|
|
||||||
lagertærskel 50–95, standard 80; opsamlingsrunde Ja/Nej) · *Ventepunkter* · *Traktorer* ·
|
|
||||||
*Status* (+ Start/Stop).
|
|
||||||
|
|
||||||
Run-vælger, Wait-pulje, traktorliste, knapper og statuslinje er fælles byggeklodser; hver fane
|
|
||||||
viser kun runs med sin egen `kind`.
|
|
||||||
|
|
||||||
### Kilder
|
|
||||||
|
|
||||||
Kilde = AD-markør. Kildetypen genkendes ved markøren (nærmeste inden for 40 m, som triggere i
|
|
||||||
dag):
|
|
||||||
|
|
||||||
- **Fabrik** (produktionspunkt): tilgængeligt pr. vare = udgangslager + paller af varen der
|
|
||||||
allerede står ved pallespawneren (inden for 25 m af spawner-noden).
|
|
||||||
- **Silo** (spec_silo med læssestation): tilgængeligt = lagerniveau for varen.
|
|
||||||
- **Objektlager** (spec_objectStorage): tilgængeligt = antal × fyldning pr. palle for varen
|
|
||||||
(`objectInfos[*].objects[*].palletAttributes`). Udtag sker automatisk, se afsnit 3.
|
|
||||||
|
|
||||||
Varelisten for et run = alle varer kilderne rummer eller producerer, som mindst én tildelt vogn
|
|
||||||
kan bære. Bulk-varer kræver tipvogn/tank; pallevarer kræver Universal Autoload.
|
|
||||||
|
|
||||||
### Datamodel (adRuns.lua, rene funktioner, ny store ved hver ændring)
|
|
||||||
|
|
||||||
```
|
|
||||||
Run (fælles)
|
|
||||||
id, name, waitPoolGroup, vehicleIds, kind = "supply" | "outbound"
|
|
||||||
Run (supply, uændret)
|
|
||||||
targetWayPointId, targetWayPointIds, ingredientMode, ingredients, loops, returnBelowPercent
|
|
||||||
Run (outbound)
|
|
||||||
sourceWayPointIds liste af AD-waypoint-id'er (1..n)
|
|
||||||
products liste af {fillType = NAVN, policy = "store"|"sell"|"sellElseStore"|"off"}
|
|
||||||
sellAtPercent 50..100, standard 90
|
|
||||||
storeAbovePercent 50..95, standard 80
|
|
||||||
collectRound bool, standard false
|
|
||||||
```
|
|
||||||
|
|
||||||
XML-version 2 i samme `ADSmartPickup_runs.xml`. Version 1-filer læses og får `kind="supply"`.
|
|
||||||
Ukendte varer i `products` (mod fjernet) ignoreres ved indlæsning med én log-linje.
|
|
||||||
|
|
||||||
## 2. Turvurdering og prisregel
|
|
||||||
|
|
||||||
`adOutboundPlanner.lua` er rent Lua (ingen spil-API). Input: run, vognens kapacitet (liter) og
|
|
||||||
om den har UAL, kildestatus pr. vare `{available, level, capacity}` pr. kilde, prisstatus pr.
|
|
||||||
vare `{percent, stationId, reachable}`. Output: en tur eller `nil` + begrundelse.
|
|
||||||
|
|
||||||
Pr. vare:
|
|
||||||
|
|
||||||
- **Tilgængeligt**: sum over kilderne.
|
|
||||||
- **Fuldt læs**: tilgængeligt ≥ vognens kapacitet.
|
|
||||||
- **Lager højt**: mindst én kilde har `level/capacity ≥ storeAbovePercent/100` for varen.
|
|
||||||
- **Pris-%**: bedst betalende *nåelige* salgssted der tager varen (adPrices). Toppris =
|
|
||||||
stationens aktuelle grundpris for varen × (årets højeste periodefaktor ÷ nuværende periodes
|
|
||||||
faktor). Pris-% = aktuel effektiv pris ÷ toppris. Stor efterspørgsel giver > 100 % og opfylder
|
|
||||||
altid reglen. Mangler stationens grundpris, bruges `fillType.pricePerLiter × højeste faktor ×
|
|
||||||
sværhedsgradens multiplikator` som toppris.
|
|
||||||
|
|
||||||
Beslutning, i rækkefølge:
|
|
||||||
|
|
||||||
1. **Salgskandidater**: politik ∈ {sell, sellElseStore}, pris-% ≥ sellAtPercent, tilgængeligt > 0.
|
|
||||||
Kører **uanset mængde**. Rangering: højeste pris-% først, derefter størst tilgængeligt.
|
|
||||||
2. **Lagerkandidater**: politik ∈ {store, sellElseStore} og (fuldt læs ELLER lager højt).
|
|
||||||
Rangering: højeste fyldningsgrad først, derefter størst tilgængeligt.
|
|
||||||
3. Ingen kandidat → hold ved Wait. Log én gang pr. ændret begrundelse:
|
|
||||||
`venter: SUGAR 78 % af top (kræver 90), lager 41 %`.
|
|
||||||
|
|
||||||
**Opsamlingsrunde** (`collectRound`):
|
|
||||||
|
|
||||||
- Bulk-vogn: én vare, men fra flere kilder efter tur indtil vognen er fuld (afsnit 3).
|
|
||||||
- UAL-vogn: alle kandidater med samme mål på én tur; vognens vareliste sættes til dem alle, og
|
|
||||||
AutoDrive skifter selv vare når der ikke kommer flere af den første.
|
|
||||||
|
|
||||||
**Mål**:
|
|
||||||
|
|
||||||
- Salg: det salgssted der betaler bedst for hovedvaren (første i rangeringen), tager alle rundens
|
|
||||||
varer, og kan nås begge veje.
|
|
||||||
- Lager: nærmeste silo-markør med læsse+aflæsningsstation der tager varen, fri plads ≥ hele læsset,
|
|
||||||
ikke selv kilde, fabrik eller stald, rute begge veje (samme regler som læs-byttet).
|
|
||||||
|
|
||||||
Prisen tjekkes kun ved turens start.
|
|
||||||
|
|
||||||
## 3. Kørsel, hold og pallelæsning
|
|
||||||
|
|
||||||
`adOutbound.lua` med to hooks:
|
|
||||||
|
|
||||||
**Start (controller).** Som forsyning: PickupAndDeliver, rotation "kun pålæsning", mapper til.
|
|
||||||
1. markør = første kilde, 2. markør = første kilde (pladsholder, sættes pr. tur). Turen
|
|
||||||
registreres i `activePickups`, så "stå og læs"-grebet og 30 s-sikkerhedsventilen gælder.
|
|
||||||
|
|
||||||
**Hook 1 — næste pålæsning** (det eksisterende `getNextPickup`-wrap): udkørsels-traktor →
|
|
||||||
planlæggeren spørges. Tur → vognens `selectedFillTypes` = turens varer, 2. markør = målet,
|
|
||||||
kilden returneres. Ingen tur → ledigt Wait-punkt fra run'ets pulje returneres (reservation +
|
|
||||||
rutetjek via adWaitPool), traktoren markeres "holder".
|
|
||||||
|
|
||||||
**Hook 2 — læsseopgaven** (prepend på `LoadAtDestinationTask.update`):
|
|
||||||
|
|
||||||
- *Holder ved Wait:* traktoren stoppes ved punktet; planlæggeren spørges hvert 5. sekund. Tur →
|
|
||||||
opgaven omdirigeres til kilden (`redirect`), varer + 2. markør sættes, "holder" slippes.
|
|
||||||
- *Ved objektlager:* når markøren er nået, traktoren står stille og AutoDrive har aktiveret UAL:
|
|
||||||
kald `placeable:removeAbstractObjectsFromStorage(objectInfoIndex, 1, nil)` for turens vare;
|
|
||||||
vent til lagerets antal falder OG UAL's antal læssede stiger; gentag. Stop når
|
|
||||||
`ualIsFull()`, lageret er tomt for varen, eller intet er sket i 30 s → `task:finished()`.
|
|
||||||
Ved runde med flere pallevarer tages varerne efter tur. `connection = nil` så
|
|
||||||
AutomaticBaleStorage-moddens kø ikke aktiveres.
|
|
||||||
- *Kør uanset mængde:* stiger vognens fyldning ikke i 15 s og vognen ikke er tom → `task:finished()`.
|
|
||||||
- *Runde, bulk:* når læsning stopper ved kilde A med fri plads ≥ 10 % og en anden kilde i listen
|
|
||||||
har samme vare, omdirigeres opgaven til kilde B i stedet for at afslutte. Hver kilde højst én
|
|
||||||
gang pr. tur.
|
|
||||||
|
|
||||||
**Aflæsning** er AutoDrives egen (tip i silo; tip eller UAL-aflæsning ved salgssted).
|
|
||||||
`adUnloadWait` springer udkørsels-traktorer over (run'ets `kind`). Efter aflæsning går AutoDrive
|
|
||||||
selv til næste pålæsning → hook 1 igen. Stop som i dag.
|
|
||||||
|
|
||||||
`adWaitPool.lua` udskilles fra `adUnloadWait.lua`: reservationer (markør-id → køretøj),
|
|
||||||
`hasRouteTo`/`hasRouteBothWays`, `redirect`, kandidatliste for en pulje. Begge moduler bruger den.
|
|
||||||
|
|
||||||
## 4. Fejl, advarsler og tests
|
|
||||||
|
|
||||||
**Uopnåeligt salgssted / ingen silo.** Kan det bedst betalende salgssted ikke nås, tages det
|
|
||||||
næstbedste nåelige. Kan intet nås (eller ingen silo med plads ved Lager), vises en rød
|
|
||||||
AutoDrive-notifikation på skærmen
|
|
||||||
(`AutoDriveMessageEvent.sendMessageOrNotification(vehicle, ERROR, …)`):
|
|
||||||
`ADSmartPickup: 'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR – tjek vejnettet`.
|
|
||||||
Samme tekst i AutoDrives notifikations-historik, log.txt (WARN) og run'ets statuslinje.
|
|
||||||
Traktoren holder ved Wait og prøver igen hvert 5. sekund. Gentages først når traktor, station
|
|
||||||
eller vare ændrer sig.
|
|
||||||
|
|
||||||
**Fejlhåndtering:** spil-API-kald i pcall; fejl → hold ved Wait + WARN én gang, aldrig
|
|
||||||
AutoDrive-stop. Validering ved Start (nye grunde): `noSource` (markør uden genkendelig kilde),
|
|
||||||
`needsAutoload` (pallevare uden UAL), `noProducts` (ingen vare med politik ≠ Fra). Objektlager
|
|
||||||
der ikke spawner → 30 s-vagt afslutter læsningen med det der er.
|
|
||||||
|
|
||||||
**Tests (luajit, uden spil):**
|
|
||||||
|
|
||||||
- planlægger: prisregel, tærskler, alle fire politikker, rangering, runde bulk/UAL, uanset mængde
|
|
||||||
- priser: toppris, pris-%, stor efterspørgsel > 100 %, fallback uden stationspris
|
|
||||||
- model + XML v2 + migration fra v1
|
|
||||||
- objektlager-udtag som tilstandsmaskine mod fake-lager (fuld, tom, stall)
|
|
||||||
- kilde-genkendelse mod fake-markører (fabrik/silo/objektlager, paller ved spawner)
|
|
||||||
- controller-validering (noSource, needsAutoload, noProducts)
|
|
||||||
|
|
||||||
**In-game (savegame 3):** sukker → salgssted ved høj pris; TMR → Farma-silo; halmkurve fra
|
|
||||||
fabrik og fra objektlager → halm-salgsstedet (UAL); uopnåeligt salgssted → advarsel på skærm.
|
|
||||||
|
|
||||||
## Filer (forventet)
|
|
||||||
|
|
||||||
```
|
|
||||||
FS25_ADSmartPickup/
|
|
||||||
adRuns.lua kind, sourceWayPointIds, products, tærskler, collectRound; XML v2
|
|
||||||
adRunsStorage.lua v2 læs/skriv, v1-migration
|
|
||||||
adOutboundPlanner.lua NY, ren
|
|
||||||
adPrices.lua NY, ren del + spiladapter
|
|
||||||
adSources.lua NY, kilde-genkendelse + lagerstatus
|
|
||||||
adOutbound.lua NY, hooks, objektlager-udtag, hold, runde
|
|
||||||
adWaitPool.lua NY, udskilt fra adUnloadWait.lua
|
|
||||||
adUnloadWait.lua bruger adWaitPool; springer outbound over
|
|
||||||
adRunsController.lua start/validering for outbound
|
|
||||||
adSmartPickup.lua source() af nye filer; getNextPickup-wrap kalder adOutbound
|
|
||||||
gui/SmartPickupFrame.lua faner + fælles blokke
|
|
||||||
gui/SupplyTab.lua NY
|
|
||||||
gui/OutboundTab.lua NY
|
|
||||||
l10n/l10n_da.xml, l10n_en.xml, README.md
|
|
||||||
tests/test_adOutboundPlanner.lua, test_adPrices.lua, test_adSources.lua,
|
|
||||||
tests/test_adOutbound.lua, test_adRuns.lua (v2), test_adRunsController.lua (outbound)
|
|
||||||
```
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# Markarbejde, etape 1: slåafgrøder (græs, kløver, lucerne)
|
|
||||||
|
|
||||||
Dato: 2026-09-25 · Mod: FS25_ADSmartPickup · Branch: `feature/markarbejde`
|
|
||||||
|
|
||||||
## Mål
|
|
||||||
|
|
||||||
Når en græs-, kløver- eller lucernemark er klar, sender Smart Pickup selv maskinerne ud. Slåmaskinen slår
|
|
||||||
marken via Courseplay. Ved **ensilage** samler en læssevogn skårene op og kører dem til den rigtige silo.
|
|
||||||
Ved **hø** vender en vender først græsset (og en river river det evt. sammen), og derefter samles høet op.
|
|
||||||
Alt kørsel på vej sker med AutoDrive. Alt arbejde på marken sker med Courseplay.
|
|
||||||
|
|
||||||
Succes: en mark sat til *Auto* går fra "klar" til "færdig" med græsset i en silo, uden at spilleren rører
|
|
||||||
noget, og menuen viser hele tiden, hvad der sker og hvorfor.
|
|
||||||
|
|
||||||
Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandling og såning, baller.
|
|
||||||
|
|
||||||
## Arbejdsdeling
|
|
||||||
|
|
||||||
| Del | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| **Smart Pickup (vores)** | Hvilken mark, hvilket trin, hvilken maskine, hvilken silo. Holder status og viser den |
|
|
||||||
| **AutoDrive** | Kørsel på vej til markens `F`-markør og til silo; overtager ved "vogn fuld" og genstarter Courseplay |
|
|
||||||
| **Courseplay** | Markgrænse, kursus, selve arbejdet på marken, "fuld"/"færdig"-signaler |
|
|
||||||
|
|
||||||
## Beslutninger (med begrundelse)
|
|
||||||
|
|
||||||
1. **Faste sæt.** Redskabet sidder fast på traktoren (eller er selvkørende, fx Krone BiG M 450). Mod'en
|
|
||||||
kobler aldrig redskaber på. *Begrundelse:* auto-påkobling var det, der aldrig blev pålideligt i AI-Bonden.
|
|
||||||
2. **Markflåde med forslag og flueben.** Mod'en foreslår sæt ud fra redskabets spec (`spec_mower`,
|
|
||||||
`spec_tedder`, `spec_windrower`, `spec_forageWagon`); kun sæt med flueben bliver sendt ud.
|
|
||||||
*Begrundelse:* mod'en må aldrig tage en maskine, spilleren selv bruger. Et sæt, spilleren sidder i
|
|
||||||
(`getIsControlled`/`getIsEntered`), springes altid over.
|
|
||||||
3. **Staging-markør pr. mark = AutoDrive-markør med navnet `F<marknummer>`** (fx `F109`), stort/småt
|
|
||||||
ligegyldigt, evt. med mellemrum (`F 109`). Mangler den, vises marken med "!" og sendes ikke ud.
|
|
||||||
4. **Auto/Manuel pr. mark.** Auto starter selv ved "klar"; Manuel venter på *Start nu*. Standard: Manuel
|
|
||||||
(intet sker uventet efter opdatering).
|
|
||||||
5. **Plan pr. mark: Ensilage eller Hø.** Standard: Ensilage. **Riv** er et valg pr. mark, standard *fra*:
|
|
||||||
læssevognene i Unrealistic Pack kan køre 40 m bredt og samler spredt hø op.
|
|
||||||
6. **Bredde.** Maskiner kører med deres *aktuelle* bredde (Unrealistic Packs `variableWorkWidth`,
|
|
||||||
`aiKeepCurrentWidth`). Courseplay kan højst 50 m → et sæt over 50 m sendes ikke ud (advarsel).
|
|
||||||
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden.
|
|
||||||
**Revideret efter in-game test 25/9:** et nyt Courseplay-kursus til læssevognen lå forskudt i forhold til
|
|
||||||
skårene (anden start/retning/forager). Derfor genbruger vender, river og læssevogn **slåmaskinens
|
|
||||||
kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra
|
|
||||||
slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus
|
|
||||||
laves et nyt som reserve (logges).
|
|
||||||
**Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med
|
|
||||||
eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥
|
|
||||||
slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til
|
|
||||||
både hø og ensilage.
|
|
||||||
**Sideriver (25/9):** mens riven arbejder, måles nedlægningsområdets midte (runtime-felt
|
|
||||||
`dropWindrowWorkAreaIndex`, ellers riverens XML) i traktorens ramme minus rivens CP-forskydning =
|
|
||||||
skårets sideforskydning (gemt pr. mark). Læssevognen på rivens kursus får `toolOffsetX = egen −
|
|
||||||
skårets` (±10 m) og får sin egen værdi tilbage bagefter.
|
|
||||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`). *Tving lager* pr. mark (som "Lager til" i
|
|
||||||
flows). **Revideret 25/9:** ensilage går først til egne **gæringssiloer** (produktion skår -> SILAGE,
|
|
||||||
fx FermentingSiloPack/GrassSilageSilo), nærmest marken, ellers nærmeste silo med plads; hø kun til
|
|
||||||
almindelige siloer (gæringssiloer laver også hø om til ensilage).
|
|
||||||
8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne
|
|
||||||
vælges (korte ture). Oversigten viser "!" ved marken, hvis intet Smart Pickup-udkørselsflow har
|
|
||||||
siloen som kilde — hjemkørslen klares af de eksisterende flows, ikke af markdelen.
|
|
||||||
9. **"Klar"** = ≥ 90 % af markens prøvepunkter står i høstvinduet for markens slåafgrøde
|
|
||||||
(`FSDensityMapUtil.getFruitArea`, samme metode som Arbejdsseddel, bevist i spillet). `field.fieldState`
|
|
||||||
bruges ikke (frossen NPC-plan for spillerejede marker).
|
|
||||||
10. **Slåafgrøde** afgøres ud fra spillets konvertere: `MOWER` giver et skår, og `TEDDER` gør skåret til en
|
|
||||||
`DRY…`-type (græs, lucerne, kløver, kløver2). Rug/bønner (→ halm) er udelukket. Ensilage-varen er
|
|
||||||
skåret (`GRASS_WINDROW` …), hø-varen er tørskåret (`DRYGRASS_WINDROW` …).
|
|
||||||
11. **Én maskine, én mark.** Flere marker kan køre samtidig. Højst 2 læssevogne pr. mark.
|
|
||||||
12. **Rækkefølge.** Næste trin starter først, når det forrige er *færdigt* på hele marken (ingen vender
|
|
||||||
bag slåmaskinen). Enklere og uden kollisioner mellem Courseplay-kurser.
|
|
||||||
13. **Fejl stopper ikke alt.** Kan Courseplay ikke lave kursus, prøves igen efter 5 min; imens står marken
|
|
||||||
med grunden. Andre marker kører videre.
|
|
||||||
14. **Genindlæsning.** Markernes trin gemmes i `ADSmartPickup_fields.xml` i savegame-mappen. Efter
|
|
||||||
indlæsning genoptages trinnet: sæt, der ikke længere kører, bliver sendt ud igen.
|
|
||||||
|
|
||||||
## Opbygning
|
|
||||||
|
|
||||||
Rene moduler (ingen spil-API, testes med `luajit`):
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `adFieldModel.lua` | Mark-plan og -status som data: plan, tilstand, overgange (`nextStage`), bredde-regler, gem/indlæs-records |
|
|
||||||
| `adFieldFleet.lua` | Rolle ud fra redskabs-specs, forslag, valg af ledigt sæt (nærmest markøren) |
|
|
||||||
| `adFieldView.lua` | Rækker og tekster til fanen Marker (sortering, filtre, statusord) |
|
|
||||||
|
|
||||||
Spil-moduler (tynde, pcall'ede):
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `adFields.lua` | Ejede marker, polygon/prøvepunkter, afgrøde, klar-andel, `F`-markør, marksiloer |
|
|
||||||
| `adCpBridge.lua` | Start Courseplay på en mark (grænse → kursus → start, async med annullering), læs aktiv/færdig |
|
|
||||||
| `adFieldJobs.lua` | Tick hvert sekund: opdag klar, send sæt ud, følg ankomst → CP → færdig, næste trin, gem |
|
|
||||||
| `gui/SmartPickupFieldTab.lua` | Fanen **Marker**: tabel, detalje, markflåde |
|
|
||||||
|
|
||||||
### Forløb pr. mark
|
|
||||||
|
|
||||||
```
|
|
||||||
Ensilage: idle → ready → mowing → collecting → done
|
|
||||||
Hø: idle → ready → mowing → tedding → [raking] → collecting → done
|
|
||||||
done → idle, når marken ikke længere er slået (vokser igen)
|
|
||||||
```
|
|
||||||
|
|
||||||
Hvert arbejdstrin har undertilstande pr. sæt: `driving` (AutoDrive til `F`) → `starting` (CP laver
|
|
||||||
kursus) → `working` (CP kører) → `returning` (AutoDrive til parkering). Trinnet er færdigt, når Courseplay
|
|
||||||
meldte færdig for mindst ét sæt, og intet sæt på marken stadig arbejder.
|
|
||||||
|
|
||||||
**Slå/vende/rive:** AutoDrive `DRIVETO` til `F`-markøren. Ved stop inden for 30 m starter
|
|
||||||
`adCpBridge` Courseplay med `fieldPosition` = et prøvepunkt inde i marken. Når CP er færdig: AutoDrive
|
|
||||||
`DRIVETO` til sættets parkering (sættets AD-parkeringsmarkør, ellers bliver det stående ved `F`).
|
|
||||||
|
|
||||||
**Opsamling:** Før CP startes sættes AutoDrive til `PICKUPANDDELIVER`, første markør = `F`, anden =
|
|
||||||
silo, `setStartHelper(true)` med hjælper Courseplay. Så klarer AutoDrive og Courseplay selv
|
|
||||||
fuld → aflæs → tilbage → `restartCP`. Ved CP færdig: AutoDrive tømmer resten (`enableParkAtJobFinished`)
|
|
||||||
og parkerer.
|
|
||||||
|
|
||||||
## Fanen Marker
|
|
||||||
|
|
||||||
- Tabel: **Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner**. Kun slåafgrøde-marker som standard
|
|
||||||
(*Vis alle marker* viser resten). Klik på en række → detalje.
|
|
||||||
- Detalje: Plan (Ensilage/Hø), Auto/Manuel, Riv, Tving lager (liste som "Lager til"), **Start nu**/**Stop**,
|
|
||||||
status med grund, markør og slåbredde.
|
|
||||||
- Markflåde: foreslåede sæt med flueben, rolle og aktuel bredde; advarsel over 50 m.
|
|
||||||
- Knapper på fanen: Start nu / Stop på valgt mark (samme placering som flows).
|
|
||||||
- Statusord: *Venter · Klar · Slås · Vendes · Rives · Samles op · Færdig* + grund ("venter på
|
|
||||||
slåmaskine", "! mangler markør F109", "læssevogn for smal: 20 m < 40,8 m", "Courseplay: intet kursus").
|
|
||||||
|
|
||||||
## Fejlhåndtering
|
|
||||||
|
|
||||||
| Situation | Hvad sker |
|
|
||||||
|---|---|
|
|
||||||
| Ingen `F`-markør | "!" i tabellen, intet sendes |
|
|
||||||
| Intet ledigt sæt med rollen | Status "venter på …", prøver hvert 5 s |
|
|
||||||
| Sæt for bredt (> 50 m) eller for smalt (< slåbredde) | Sættet springes over med grund |
|
|
||||||
| AutoDrive kan ikke finde vej | Sættet frigives, status med grund, andet sæt prøves |
|
|
||||||
| CP-grænse/kursus fejler | 5 min pause for marken, grund vises |
|
|
||||||
| Spilleren sætter sig i et sæt | Sættet frigives fra marken (arbejdet fortsætter ikke automatisk) |
|
|
||||||
| Genindlæsning | Trin genoptages fra gemt fil |
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adFieldModel.lua`, `tests/test_adFieldFleet.lua`, `tests/test_adFieldView.lua`: alle
|
|
||||||
overgange, bredde-regler, forslag, sortering/filtre, gem/indlæs.
|
|
||||||
- In-game testprotokol (manualen + denne spec): én mark Manuel → Start nu → slå → saml op → silo, derefter
|
|
||||||
Hø, derefter Auto. Loglinjer: `ADSmartPickup: F109 …`.
|
|
||||||
|
|
||||||
## Senere udvidelser (noter)
|
|
||||||
|
|
||||||
- **Flere maskiner om samme mark, maks 5** (Courseplays multi-tool-grænse, 2026-09-25): slå/vende/rive med
|
|
||||||
op til 5 sæt på én mark samtidig via Courseplays konvoj (multi-tool), og op til 5 læssevogne. Kræver at
|
|
||||||
`ADFieldModel.maxRigs` bliver en indstilling pr. mark (1–5) og at sættene får samme CP-kursus med
|
|
||||||
hver sin position i konvojen.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# Markarbejde, etape 2–3 + multi-maskine: høst, roer, jordbehandling, såning
|
|
||||||
|
|
||||||
Dato: 2026-09-26 (nat) · Bygger på `2026-09-25-markarbejde-etape1-design.md` · Branch `feature/markarbejde-2`
|
|
||||||
|
|
||||||
Brugeren gav fuldt mandat ("byg resten, ALLE PUNKTER, du træffer selv gode valg") og tester i spillet dagen
|
|
||||||
efter. Beslutningerne herunder er truffet uden dialog og skal læses igennem.
|
|
||||||
|
|
||||||
## Mål
|
|
||||||
|
|
||||||
Alle gårdens marker kan køres af Smart Pickup: slåafgrøder (etape 1) og nu **høstafgrøder** (korn, raps,
|
|
||||||
majs, roer, kartofler … alt der høstes med mejetærsker/optager), inkl. **aflæsningsvogne**, **jordbehandling**
|
|
||||||
efter høst og **såning**. Hvert trin kan køres med **1–5 maskiner** på samme mark (Courseplays multi-tool).
|
|
||||||
|
|
||||||
## Beslutninger
|
|
||||||
|
|
||||||
1. **Høst er én pipeline for alle høstafgrøder.** Roer og korn adskiller sig kun ved hvilken høster der
|
|
||||||
kan tage afgrøden. Høsteren matches på afgrøden: skærebordets/optagerens frugttyper (`spec_cutter`)
|
|
||||||
skal omfatte markens afgrøde. Finsnittere (cutter med `fruitTypeConverters`) bruges ikke til høst.
|
|
||||||
2. **Aflæsning under høst = Courseplays combine-unloader** (`COMBINE_UNLOADER_CP`, bevist i AI-Bonden):
|
|
||||||
vognen følger høsteren, tømmer den og kører til en **station**. Stationen vælges af Smart Pickups
|
|
||||||
silovalg (nærmeste silo med plads der tager afgrøden, eller *Tving lager*); findes den ikke i
|
|
||||||
Courseplays stationsliste, bruges nærmeste station. Antal vogne pr. mark: 1–3 (plan).
|
|
||||||
3. **Høsteren sendes hjem** (AD-parkering) når Courseplay er færdig og tanken er tømt. Aflæsningsvogne
|
|
||||||
stoppes når høsten er færdig; har de rest, kører AutoDrive den til lageret (samme reserve som
|
|
||||||
læssevognen), ellers parkering.
|
|
||||||
4. **Efter høst** (valg pr. mark): *Ingenting*, *Kultivér* eller *Pløj*, og derefter evt. **Så**.
|
|
||||||
Såmaskinen sættes til planens afgrøde (standard: samme afgrøde som blev høstet). Kan afgrøden ikke
|
|
||||||
sås i den nuværende periode, venter marken med grund. Tom for frø -> sættet springes over med grund
|
|
||||||
(moden fylder ikke frø på; det gør brugeren/AutoDrive-refill).
|
|
||||||
5. **Multi-maskine (1–5 pr. trin, plan pr. mark).** Trin med eget kursus (slå, vend, riv, høst,
|
|
||||||
kultivér, pløj, så) laver ét Courseplay-kursus med `multiTools = n` på første sæt; de øvrige kopierer det,
|
|
||||||
og hver får sin bane (`laneOffset`: 2 -> −1,1 · 3 -> −1,0,1 · 4 -> −2,−1,1,2 · 5 -> −2…2). Et multi-trin
|
|
||||||
startes kun når **alle n** sæt er ledige (ellers kører én bane ikke). Læssevognen på et multi-kursus
|
|
||||||
kræver også n vogne, én pr. bane.
|
|
||||||
6. **Rolle-rækkefølge** når et køretøj har flere redskaber: høster > læssevogn > river > vender >
|
|
||||||
slåmaskine > såmaskine > kultivator > plov > aflæsningsvogn (traktor+vogn som Courseplay kan bruge som
|
|
||||||
combine-unloader).
|
|
||||||
7. **Afgrødetype pr. mark:** slåafgrøde (etape 1) eller høstafgrøde (alt andet med en høstbar frugt). Plan
|
|
||||||
*Høst* er standard for høstafgrøder. Marker uden afgrøde (pløjet/stub) kan få *Start nu* for
|
|
||||||
jordbehandling/såning alene.
|
|
||||||
8. **Stadig ikke med:** gødning/kalk/sprøjtning, baller, halm efter mejetærsker, auto-påfyldning af frø.
|
|
||||||
|
|
||||||
## Opbygning (ændringer)
|
|
||||||
|
|
||||||
| Fil | Ændring |
|
|
||||||
|---|---|
|
|
||||||
| `adFieldModel.lua` | Planer `harvest`, jordbehandling, såning, `rigs`/`unloaders`; trin `harvesting/cultivating/plowing/sowing`; hjælperoller pr. trin; `lanePositions(n)` |
|
|
||||||
| `adFieldFleet.lua` | Nye roller; `pick` tager afgrøde (høster skal kunne tage den); `pickMany` til multi-trin |
|
|
||||||
| `adCpBridge.lua` | `startUnloader`, `canHarvest`, frø-valg (`setSeedFruit`, `isPlantable`, `hasSeeds`), multi-tool kursus + lane |
|
|
||||||
| `adFields.lua` | Afgrødetype (slå/høst), sidst høstede afgrøde |
|
|
||||||
| `adFieldWork.lua` (ny) | Udsendelse og opfølgning af sæt (AD/CP) — flyttet ud af `adFieldJobs.lua` der blev for stor |
|
|
||||||
| `gui/SmartPickupFieldTab.lua` | Plan-rækker pr. afgrødetype, maskiner pr. trin, aflæsningsvogne, efter høst, så + afgrøde |
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden)
|
|
||||||
-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket,
|
|
||||||
-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache.
|
|
||||||
|
|
||||||
local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7
|
|
||||||
local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}}
|
|
||||||
g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end}
|
|
||||||
Logging = {info = function() end, warning = function() end}
|
|
||||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
|
||||||
SellingStation = {}
|
|
||||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adBuildings.lua")
|
|
||||||
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local markers, wayPoints = {}, {}
|
|
||||||
local function addMarker(id, name, group, x, z)
|
|
||||||
wayPoints[id] = {id = id, x = x, z = z}
|
|
||||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
|
||||||
end
|
|
||||||
local loadStations, unloadStations = {}, {}
|
|
||||||
local adEnv = {ADGraphManager = {
|
|
||||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
|
||||||
getMapMarkers = function() return markers end,
|
|
||||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
|
||||||
}}
|
|
||||||
local strictLoadAt = {}
|
|
||||||
ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end}
|
|
||||||
g_currentMission = {
|
|
||||||
placeableSystem = {placeables = {}},
|
|
||||||
storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end},
|
|
||||||
accessHandler = {canFarmAccess = function() return true end},
|
|
||||||
getFarmId = function() return 1 end,
|
|
||||||
}
|
|
||||||
local function placeable(uid, name, x, z, extra)
|
|
||||||
local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end}
|
|
||||||
for k, v in pairs(extra or {}) do p[k] = v end
|
|
||||||
table.insert(g_currentMission.placeableSystem.placeables, p)
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
|
|
||||||
-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted
|
|
||||||
local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end})
|
|
||||||
-- som FS25_LargerAnimalBarns' cowBarnBig: ÉN læssestation med TO læssepunkter (gylle + mælk)
|
|
||||||
local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}, {triggerNode = {x = 0, z = 30}}}}
|
|
||||||
local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}}
|
|
||||||
table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload)
|
|
||||||
local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}})
|
|
||||||
local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true}
|
|
||||||
table.insert(unloadStations, millUnload)
|
|
||||||
local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}})
|
|
||||||
local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}})
|
|
||||||
local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}}
|
|
||||||
local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}}
|
|
||||||
table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station)
|
|
||||||
table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}})
|
|
||||||
local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}})
|
|
||||||
local sellPoint = placeable("sell", "Mejeri", 3000, 0, {})
|
|
||||||
local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true}
|
|
||||||
table.insert(unloadStations, sellStation)
|
|
||||||
local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end})
|
|
||||||
|
|
||||||
addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren
|
|
||||||
addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren
|
|
||||||
addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren)
|
|
||||||
addMarker(11, "Cow 1 Slurry", "Slurry", 0, 33) -- 3 m fra gylletriggeren
|
|
||||||
addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren
|
|
||||||
addMarker(5, "sugar offload", "production", 470, 0)
|
|
||||||
addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1
|
|
||||||
addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m)
|
|
||||||
strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station
|
|
||||||
addMarker(8, "Storage pickup", "Pickup", 2010, 0)
|
|
||||||
addMarker(9, "Mejeri", "selling", 3000, 20)
|
|
||||||
addMarker(10, "Langt væk", "All", 9000, 9000)
|
|
||||||
|
|
||||||
-- B1: liste, kind og markører
|
|
||||||
local list = ADBuildings.list(adEnv, 1)
|
|
||||||
local byId = {}
|
|
||||||
for _, b in ipairs(list) do byId[b.id] = b end
|
|
||||||
check("B1 antal (naboens silo udeladt)", #list, 6)
|
|
||||||
check("B1 stald", byId.barn1.kind, "husbandry")
|
|
||||||
check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1)
|
|
||||||
-- én markør pr. læssestation (mælketank OG gyllebeholder), sorteret på markørnavn
|
|
||||||
check("B1 stald har to læssemarkører", #byId.barn1.loadMarkers, 2)
|
|
||||||
check("B1 første = cow 1 milk (navnesorteret, uanset store/små)", byId.barn1.loadMarkers[1].id, 1)
|
|
||||||
check("B1 anden = Cow 1 Slurry", byId.barn1.loadMarkers[2].name, "Cow 1 Slurry")
|
|
||||||
check("B1 gyllemarkør -> stald via byMarker", ADBuildings.byMarker(adEnv, 1, 11, "load").id, "barn1")
|
|
||||||
check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2)
|
|
||||||
check("B1 stald afstand", byId.barn1.loadMarker.distance, 5)
|
|
||||||
check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4)
|
|
||||||
check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5)
|
|
||||||
check("B1 fabrik er ikke salgssted", byId.mill.canSell, false)
|
|
||||||
check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8)
|
|
||||||
check("B1 salgssted", byId.sell.kind, "sell")
|
|
||||||
check("B1 salgssted markør", byId.sell.unloadMarker.id, 9)
|
|
||||||
check("B1 salgssted canSell", byId.sell.canSell, true)
|
|
||||||
check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true)
|
|
||||||
check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true)
|
|
||||||
|
|
||||||
-- B2: siloer i række: strengt opslag giver hver sin markør
|
|
||||||
check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6)
|
|
||||||
check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7)
|
|
||||||
strictLoadAt[7] = nil
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil)
|
|
||||||
strictLoadAt[7] = silo2Station
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
|
|
||||||
-- B3: byMarker (omvendt opslag)
|
|
||||||
check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1")
|
|
||||||
check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1")
|
|
||||||
check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil)
|
|
||||||
check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil)
|
|
||||||
|
|
||||||
-- B4: id-fallback uden uniqueId
|
|
||||||
check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3")
|
|
||||||
check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1")
|
|
||||||
|
|
||||||
-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate
|
|
||||||
local first = ADBuildings.list(adEnv, 1)
|
|
||||||
check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true)
|
|
||||||
|
|
||||||
-- B6: sortering efter NAVN (naturlig talrækkefølge), dernæst første markørnavn — ikke efter slags
|
|
||||||
local order = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.name) end
|
|
||||||
check("B6 rækkefølge", table.concat(order, ","), "Kostald 1,Mejeri,Palleskur,Silo 1,Silo 2,Sukkerfabrik")
|
|
||||||
check("B6 naturlig sortering", ADBuildings.naturalLess("Silo 9", "Silo 10"), true)
|
|
||||||
check("B6 naturlig sortering, tal før tekst-lighed", ADBuildings.naturalLess("25 Silo Storage", "26 Silo Storage"), true)
|
|
||||||
check("B6 naturlig sortering ens navn -> markørnavn", ADBuildings.compare({name = "Farma", loadMarkers = {{name = "30 Silo"}}}, {name = "Farma", loadMarkers = {{name = "4 Silo"}}}), false)
|
|
||||||
|
|
||||||
-- B-DOB: silo der kun kan FYLDES (drive-on-bunker ved biogas: aflæsning, ingen læssestation) = mål, ikke kilde
|
|
||||||
do
|
|
||||||
local dob = placeable("dob1", "DOB 115", 5000, 0, {spec_silo = {}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = dob, unloadTriggers = {{exactFillRootNode = {x = 5000, z = 5}}}})
|
|
||||||
addMarker(21, "Biogas Drop 1", "Biogas", 5000, 12)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byIdDob = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byIdDob[b.id] = b end
|
|
||||||
check("B-DOB kan være mål", byIdDob.dob1.canBeTarget, true)
|
|
||||||
check("B-DOB ikke kilde (intet at hente)", byIdDob.dob1.canBeSource, false)
|
|
||||||
check("B-DOB aflæsningsmarkør", byIdDob.dob1.unloadMarker.id, 21)
|
|
||||||
check("B-DOB almindelig silo stadig ikke mål", byIdDob.silo1.canBeTarget, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SEP: bygning med læssestation men uden silo/fabrik/stald-spec (Pumps&Hoses separatorhal: digestat-udlevering)
|
|
||||||
do
|
|
||||||
local sep = placeable("sep1", "Separator Hall", 7000, 0, {spec_manureSeparator = {}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = sep, loadTriggers = {{triggerNode = {x = 7000, z = 5}}}})
|
|
||||||
addMarker(41, "Digestate pickup", "Biogas", 7000, 10)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local bySep = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do bySep[b.id] = b end
|
|
||||||
check("B-SEP findes som lager", bySep.sep1 and bySep.sep1.kind, "storage")
|
|
||||||
check("B-SEP kan være kilde", bySep.sep1 and bySep.sep1.canBeSource, true)
|
|
||||||
check("B-SEP læssemarkør", bySep.sep1 and bySep.sep1.loadMarker and bySep.sep1.loadMarker.id, 41)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-DROP: fabrik med pickup-markør TÆT på aflæsningstriggeren og en separat drop-markør længere væk
|
|
||||||
-- (Sugarbeet Crusher): aflæsningsmarkøren skal være drop, ikke bygningens egen læssemarkør
|
|
||||||
do
|
|
||||||
local crusher = placeable("crusher1", "Sugarbeet Crusher", 8000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = crusher, loadTriggers = {{triggerNode = {x = 8000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = crusher, unloadTriggers = {{exactFillRootNode = {x = 8003, z = 2}}}})
|
|
||||||
addMarker(51, "Sugar Beet Cut Pickup", "Fabrication", 8000, 8) -- 3 m fra læs, ~6 m fra aflæsning
|
|
||||||
addMarker(52, "Sugar Beet Cut Beet Drop", "Fabrication", 8012, 2) -- 9 m fra aflæsning
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byC = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
|
||||||
check("B-DROP læssemarkør = pickup", byC.crusher1.loadMarker.id, 51)
|
|
||||||
check("B-DROP aflæsningsmarkør = drop (ikke pickup)", byC.crusher1.unloadMarker.id, 52)
|
|
||||||
-- kun én fælles markør (AIO Load / Unload): bruges til begge
|
|
||||||
local aio = placeable("aio1", "AIO", 9000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = aio, loadTriggers = {{triggerNode = {x = 9000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = aio, unloadTriggers = {{exactFillRootNode = {x = 9002, z = 5}}}})
|
|
||||||
addMarker(53, "AIO Load / Unload", "Fabrication", 9000, 9)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
|
||||||
check("B-DROP fælles markør bruges til aflæsning", byC.aio1.unloadMarker.id, 53)
|
|
||||||
check("B-DROP silo beholder sin egen markør (ikke nabosiloens)", byC.silo1.unloadMarker.id, 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-COOP: hønsehus — foder aflæsses i den ene ende, æg-paller spawnes i den anden (ingen læssestation)
|
|
||||||
do
|
|
||||||
local coop = placeable("coop1", "Chicken Coop", 10000, 0, {getHusbandryFillLevel = function() return 0 end,
|
|
||||||
spec_husbandryFood = {triggers = {{someTrigger = {exactFillRootNode = {x = 9970, z = 0}}}}}, -- ukendt feltsti: søges
|
|
||||||
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[99] = {spawnPlaces = {{startNode = {x = 10030, z = 0}}}}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = coop, unloadTriggers = {}}) -- som chickenBarnBig: tom station
|
|
||||||
addMarker(61, "Chicken Food", "Chicken", 9965, 0)
|
|
||||||
addMarker(62, "Egg Pickup", "Chicken", 10035, 0)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byCoop = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byCoop[b.id] = b end
|
|
||||||
check("B-COOP læssemarkør ved æggene", byCoop.coop1.loadMarker and byCoop.coop1.loadMarker.id, 62)
|
|
||||||
check("B-COOP aflæsningsmarkør ved foderet", byCoop.coop1.unloadMarker and byCoop.coop1.unloadMarker.id, 61)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-FUEL: brændstoftank (læssestation kun med DIESEL) er ikke et lager/en kilde
|
|
||||||
do
|
|
||||||
local fmBefore = g_fillTypeManager
|
|
||||||
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return index == 900 and "DIESEL" or "X" end}
|
|
||||||
local tank = placeable("fuel1", "Small Petrol Tank", 11000, 0, {})
|
|
||||||
table.insert(loadStations, {owningPlaceable = tank, supportedFillTypes = {[900] = true}, loadTriggers = {{triggerNode = {x = 11000, z = 3}}}})
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byFuel = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byFuel[b.id] = b end
|
|
||||||
check("B-FUEL brændstoftank udeladt", byFuel.fuel1, nil)
|
|
||||||
g_fillTypeManager = fmBefore
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SCAN: søgning efter trigger-noder i indlejrede tabeller (dybde ≤ 3, ingen løkker)
|
|
||||||
do
|
|
||||||
local loop = {}
|
|
||||||
loop.self = loop
|
|
||||||
local nodes = ADBuildings.findTriggerNodes({a = {b = {exactFillRootNode = "n1"}}, c = {triggerNode = "n2"}, d = loop, e = {f = {g = {h = {exactFillRootNode = "for-dybt"}}}}})
|
|
||||||
table.sort(nodes)
|
|
||||||
check("B-SCAN finder n1 og n2, ikke for dybt", table.concat(nodes, ","), "n1,n2")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SHED: palleskur læsser og aflæsser samme sted — aflæsningsmarkøren er den nærmeste (ikke "den anden")
|
|
||||||
do
|
|
||||||
local shedX = placeable("shedX", "Bale & Pallet Storage", 12000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 4000}})
|
|
||||||
addMarker(71, "Chicken Storage Unload", "Chicken", 12010, 0) -- 10 m
|
|
||||||
addMarker(72, "Chicken Storage Pickup", "Chicken", 12018, 0) -- 18 m
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byShed = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byShed[b.id] = b end
|
|
||||||
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
|
|
||||||
end
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
|
|
||||||
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
|
|
||||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function withMarkers(width)
|
|
||||||
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
|
|
||||||
local wagon = withMarkers(2.4)
|
|
||||||
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
|
|
||||||
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
|
|
||||||
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
|
|
||||||
-- W2: ugyldig variabel bredde -> AI-markører
|
|
||||||
local plain = withMarkers(9.5)
|
|
||||||
plain.getVariableWorkWidth = function() return 0, nil, false end
|
|
||||||
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
|
|
||||||
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
|
|
||||||
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
|
|
||||||
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
|
|
||||||
|
|
||||||
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
|
|
||||||
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
|
|
||||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
|
|
||||||
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
|
|
||||||
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
|
|
||||||
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
|
|
||||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
|
|
||||||
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
|
|
||||||
|
|
||||||
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
|
|
||||||
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
|
|
||||||
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
|
|
||||||
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
|
|
||||||
local function rake(dropIndex, runtime)
|
|
||||||
local areas = {
|
|
||||||
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
|
|
||||||
{start = 10, width = 11, height = 12},
|
|
||||||
{start = 20, width = 21, height = 22},
|
|
||||||
}
|
|
||||||
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
|
|
||||||
end
|
|
||||||
local sideRake = rake(2, true)
|
|
||||||
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
|
|
||||||
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
|
|
||||||
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
|
|
||||||
local centerRake = rake(3, true)
|
|
||||||
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
|
|
||||||
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
|
|
||||||
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
|
|
||||||
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
|
|
||||||
local xmlRake = rake(2, false)
|
|
||||||
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
|
|
||||||
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
|
|
||||||
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
|
|
||||||
|
|
||||||
-- SR: sprøjte/spreder -> rolle ud fra fyldet
|
|
||||||
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
|
|
||||||
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
|
|
||||||
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
|
|
||||||
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
|
|
||||||
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
|
|
||||||
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
|
|
||||||
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
|
|
||||||
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
|
|
||||||
|
|
||||||
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
|
|
||||||
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
|
|
||||||
check("HF liste", list[3] and list[5], true)
|
|
||||||
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
|
|
||||||
check("HF opslag", lookup[3], true)
|
|
||||||
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
|
|
||||||
check("HF finsnitter tager intet", next(forage), nil)
|
|
||||||
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
|
|
||||||
|
|
||||||
-- PL: plantbar i perioden
|
|
||||||
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
|
|
||||||
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
|
||||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
|
||||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldFleet.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
|
||||||
local F = ADFieldFleet
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FL1: rolle ud fra redskabets spec (læssevogn vinder over andet på samme køretøj)
|
|
||||||
check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
|
|
||||||
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
|
|
||||||
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
|
|
||||||
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
|
|
||||||
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
|
|
||||||
|
|
||||||
local function rig(id, role, x, extra)
|
|
||||||
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
|
|
||||||
for k, v in pairs(extra or {}) do r[k] = v end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FL2: nærmeste ledige med flueben og rigtig rolle
|
|
||||||
local picked = F.pick({rig("far", "mower", 500), rig("near", "mower", 50), rig("wagon", "collector", 10)}, "mower", 0, 0, nil)
|
|
||||||
check("FL2 nærmeste slåmaskine", picked.id, "near")
|
|
||||||
|
|
||||||
-- FL3: uden flueben / optaget / spiller i sædet springes over
|
|
||||||
local _, why = F.pick({rig("a", "mower", 1, {enabled = false})}, "mower", 0, 0)
|
|
||||||
check("FL3 intet med flueben", why, "noRig")
|
|
||||||
local _, whyBusy = F.pick({rig("a", "mower", 1, {busy = true}), rig("b", "mower", 2, {controlled = true})}, "mower", 0, 0)
|
|
||||||
check("FL3 alle optaget", whyBusy, "allBusy")
|
|
||||||
check("FL3 ingen med rollen", select(2, F.pick({rig("a", "tedder", 1)}, "mower", 0, 0)), "noRig")
|
|
||||||
|
|
||||||
-- FL4: læssevogn smallere end slåbredden springes over, bredere vælges
|
|
||||||
local wide = F.pick({rig("smal", "collector", 1, {width = 20}), rig("bred", "collector", 900, {width = 40.8})}, "collector", 0, 0, 40.8)
|
|
||||||
check("FL4 bred valgt selv om længere væk", wide.id, "bred")
|
|
||||||
-- FL5: kun for smalle -> grund tooNarrow med tal
|
|
||||||
local none, whyNarrow, args = F.pick({rig("smal", "collector", 1, {width = 20})}, "collector", 0, 0, 40.8)
|
|
||||||
check("FL5 ingen", none, nil); check("FL5 grund", whyNarrow, "tooNarrow"); check("FL5 bredde", args[1], 20)
|
|
||||||
-- FL6: over 50 m -> tooWide
|
|
||||||
check("FL6 for bred", select(2, F.pick({rig("x", "mower", 1, {width = 60})}, "mower", 0, 0)), "tooWide")
|
|
||||||
|
|
||||||
-- FL7: toggle giver ny tabel
|
|
||||||
local enabled = {a = true}
|
|
||||||
local toggled = F.toggle(enabled, "b")
|
|
||||||
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
|
|
||||||
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
|
|
||||||
|
|
||||||
-- FL8: nye roller i prioriteret rækkefølge
|
|
||||||
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
|
|
||||||
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
|
|
||||||
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
|
|
||||||
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
|
|
||||||
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
|
|
||||||
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
|
|
||||||
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
|
|
||||||
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
|
|
||||||
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
|
|
||||||
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
|
|
||||||
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
|
|
||||||
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
|
|
||||||
|
|
||||||
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
|
|
||||||
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
|
|
||||||
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
|
|
||||||
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
|
||||||
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
|
|
||||||
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
|
||||||
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
|
|
||||||
|
|
||||||
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
|
|
||||||
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
|
|
||||||
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
|
|
||||||
check("FL10 to valgt", #two, 2)
|
|
||||||
check("FL10 nærmeste først", two[1].id, "m2")
|
|
||||||
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
|
|
||||||
check("FL10 for få -> ingen", none, nil)
|
|
||||||
check("FL10 grund", whyMany, "notEnough")
|
|
||||||
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
|
|
||||||
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
|
|
||||||
|
|
||||||
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
|
|
||||||
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
|
|
||||||
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
|
|
||||||
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
|
|
||||||
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
|
|
||||||
|
|
||||||
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
|
|
||||||
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
|
|
||||||
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
|
|
||||||
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
|
|
||||||
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldJobs.lua")
|
|
||||||
local J = ADFieldJobs
|
|
||||||
local M = ADFieldModel
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
|
|
||||||
|
|
||||||
-- J1: klar auto-mark med markør -> start; uden markør -> vent
|
|
||||||
local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
|
|
||||||
check("J1 start", J.decide(auto, info, {}, 0).action, "start")
|
|
||||||
check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
|
|
||||||
|
|
||||||
-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
|
|
||||||
local mowing = M.start(auto)
|
|
||||||
local wanted = J.decide(mowing, info, {}, 0)
|
|
||||||
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
|
|
||||||
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
|
|
||||||
local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
|
|
||||||
check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
|
|
||||||
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
|
|
||||||
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
|
|
||||||
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
|
|
||||||
check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
|
|
||||||
-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
|
|
||||||
local collecting = M.advance(mowing)
|
|
||||||
check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
|
|
||||||
check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J6: Courseplay efterlader et hjørne -> efter 2 færdige slå-ture går marken videre alligevel
|
|
||||||
local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
|
|
||||||
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
|
|
||||||
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
|
|
||||||
|
|
||||||
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
|
|
||||||
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
|
|
||||||
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
|
||||||
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
|
|
||||||
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
|
|
||||||
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
|
|
||||||
local d = J.decide(harvesting, harvestInfo, {}, 0)
|
|
||||||
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
|
|
||||||
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
|
|
||||||
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
|
||||||
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
|
||||||
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
|
||||||
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
|
||||||
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
|
||||||
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
|
||||||
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
|
|
||||||
local cultivating = M.advance(harvesting, {})
|
|
||||||
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
|
|
||||||
|
|
||||||
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
|
|
||||||
local multiMow = M.with(mowing, {plan = {rigs = 3}})
|
|
||||||
local dMulti = J.decide(multiMow, info, {}, 0)
|
|
||||||
check("J9 tre slåmaskiner", dMulti.count, 3)
|
|
||||||
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
|
|
||||||
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
|
|
||||||
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
|
|
||||||
local care = J.decide(autoField, growingInfo, {}, 0)
|
|
||||||
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
|
|
||||||
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
|
|
||||||
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
|
|
||||||
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
|
||||||
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
|
||||||
|
|
||||||
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
|
|
||||||
local rain = {raining = true}
|
|
||||||
local dRain = J.decide(mowing, info, {}, 0, rain)
|
|
||||||
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
|
|
||||||
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
|
|
||||||
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
|
|
||||||
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
|
|
||||||
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
|
|
||||||
local silageCollect = M.advance(mowing)
|
|
||||||
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
|
|
||||||
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
|
||||||
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
|
||||||
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
|
||||||
|
|
||||||
-- J12: bar mark med mål-afgrøde starter selv når målet kan sås; ellers venter den
|
|
||||||
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
|
|
||||||
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
|
|
||||||
local canSow = {plantable = function(name) return name == "WHEAT" end}
|
|
||||||
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
|
|
||||||
check("J12 bar + ikke såtid -> intet", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "none")
|
|
||||||
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
|
|
||||||
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldModel.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
local M = ADFieldModel
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- M1: ny mark = venter, manuel ensilage uden riv
|
|
||||||
local f = M.newField(109)
|
|
||||||
check("M1 stage", f.stage, "idle")
|
|
||||||
check("M1 mode", f.plan.mode, "manual")
|
|
||||||
check("M1 product auto", f.plan.product, "auto")
|
|
||||||
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
|
|
||||||
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
|
|
||||||
check("M1 rake", f.plan.rake, false)
|
|
||||||
|
|
||||||
-- M2: klar ved >= 90 %
|
|
||||||
check("M2 89 % ikke klar", M.onReadiness(f, 0.89).stage, "idle")
|
|
||||||
local ready = M.onReadiness(f, 0.9)
|
|
||||||
check("M2 90 % klar", ready.stage, "ready")
|
|
||||||
check("M2 immutabel", f.stage, "idle")
|
|
||||||
|
|
||||||
-- M3: klar mark slået af spilleren -> tilbage til venter
|
|
||||||
check("M3 slået manuelt", M.onReadiness(ready, 0.3).stage, "idle")
|
|
||||||
check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "ready")
|
|
||||||
|
|
||||||
-- M4: trin pr. plan
|
|
||||||
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
|
|
||||||
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
|
|
||||||
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
|
|
||||||
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
|
|
||||||
|
|
||||||
-- M5: start: auto starter selv, manuel kun på forespørgsel
|
|
||||||
check("M5 manuel starter ikke", M.shouldStart(ready), false)
|
|
||||||
local auto = M.onReadiness(M.newField(5), 1)
|
|
||||||
auto.plan.mode = "auto"
|
|
||||||
check("M5 auto starter", M.shouldStart(auto), true)
|
|
||||||
local requested = M.newField(6); requested.stage = "ready"; requested.startRequested = true
|
|
||||||
check("M5 Start nu", M.shouldStart(requested), true)
|
|
||||||
local started = M.start(ready)
|
|
||||||
check("M5 start -> slås", started.stage, "mowing")
|
|
||||||
check("M5 start rydder forespørgsel", started.startRequested, false)
|
|
||||||
local oldMow = M.with(ready, {mowVehicleId = "old"})
|
|
||||||
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
|
|
||||||
|
|
||||||
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
|
|
||||||
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
|
|
||||||
hay = M.advance(hay); check("M6 hø -> vendes", hay.stage, "tedding")
|
|
||||||
hay = M.advance(hay); check("M6 -> samles op", hay.stage, "collecting")
|
|
||||||
hay = M.advance(hay); check("M6 -> færdig", hay.stage, "done")
|
|
||||||
check("M6 færdig forbliver ved lav andel", M.onReadiness(hay, 0.1).stage, "done")
|
|
||||||
check("M6 færdig -> klar når vokset", M.onReadiness(hay, 0.95).stage, "ready")
|
|
||||||
check("M6 arbejdende mark ignorerer andel", M.onReadiness(started, 0).stage, "mowing")
|
|
||||||
check("M6 isWorking", M.isWorking(started), true)
|
|
||||||
check("M6 stop -> klar", M.stop(started).stage, "ready")
|
|
||||||
|
|
||||||
-- M7: roller, maks sæt, bredde
|
|
||||||
check("M7 rolle slå", M.roleFor("mowing"), "mower")
|
|
||||||
check("M7 rolle saml", M.roleFor("collecting"), "collector")
|
|
||||||
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
|
|
||||||
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
|
|
||||||
check("M7 maks slå", M.maxRigs("mowing"), 1)
|
|
||||||
local okWide, whyWide = M.checkWidth("mower", 60, nil)
|
|
||||||
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
|
|
||||||
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
|
|
||||||
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
|
|
||||||
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
|
|
||||||
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
|
|
||||||
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
|
|
||||||
|
|
||||||
-- wantsRig: kun arbejdende trin, under maks, efter pause
|
|
||||||
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
|
|
||||||
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
|
|
||||||
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
|
|
||||||
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
|
|
||||||
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
|
|
||||||
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
|
|
||||||
|
|
||||||
-- M8: gem/indlæs bevarer trin, plan og slåbredde
|
|
||||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
|
|
||||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}}
|
|
||||||
local back = M.fromRecord(M.toRecord(saved))
|
|
||||||
check("M8 id", back.id, 109)
|
|
||||||
check("M8 stage", back.stage, saved.stage)
|
|
||||||
check("M8 mode", back.plan.mode, "auto")
|
|
||||||
check("M8 product", back.plan.product, "hay")
|
|
||||||
check("M8 rake", back.plan.rake, true)
|
|
||||||
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
|
|
||||||
check("M8 slåbredde", back.mowWidth, 40.8)
|
|
||||||
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
|
|
||||||
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
|
|
||||||
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
|
|
||||||
|
|
||||||
-- M9: with() kopierer og fletter planen
|
|
||||||
local base = M.start(M.onReadiness(M.newField(1), 1))
|
|
||||||
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
|
|
||||||
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
|
|
||||||
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
|
|
||||||
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
|
|
||||||
|
|
||||||
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
|
|
||||||
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
|
|
||||||
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
|
|
||||||
|
|
||||||
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
|
|
||||||
local cut = M.with(M.newField(3), {mowWidth = 40.8})
|
|
||||||
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
|
|
||||||
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
|
|
||||||
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
|
|
||||||
local raked = M.with(cut, {plan = {rake = true}})
|
|
||||||
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
|
|
||||||
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
|
|
||||||
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
|
|
||||||
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
|
|
||||||
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
|
|
||||||
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
|
|
||||||
-- gem/indlæs rivemaskine
|
|
||||||
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
|
|
||||||
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
|
|
||||||
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
|
|
||||||
|
|
||||||
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
|
|
||||||
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
|
|
||||||
check("M12 midterrive", M.collectorOffset(0, 0), 0)
|
|
||||||
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
|
|
||||||
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
|
|
||||||
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
|
|
||||||
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
|
|
||||||
local side = M.with(M.newField(11), {windrowOffset = 3.2})
|
|
||||||
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
|
|
||||||
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
|
|
||||||
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
|
|
||||||
|
|
||||||
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
|
|
||||||
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
|
|
||||||
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
|
|
||||||
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
|
|
||||||
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
|
|
||||||
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
|
|
||||||
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
|
|
||||||
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
|
|
||||||
|
|
||||||
-- M14: høstkæde + klargøring efter behov
|
|
||||||
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
|
|
||||||
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
|
|
||||||
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
|
|
||||||
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
|
|
||||||
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
|
|
||||||
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
|
|
||||||
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
|
|
||||||
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
|
|
||||||
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
|
|
||||||
check("M14 start = høst", harvestField.stage, "harvesting")
|
|
||||||
check("M14 husker art", harvestField.kind, "harvest")
|
|
||||||
local afterHarvest = M.advance(harvestField, {})
|
|
||||||
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
|
|
||||||
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
|
|
||||||
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
|
|
||||||
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
|
|
||||||
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
|
|
||||||
|
|
||||||
-- M15: hjælpere under høst og multi-tool-baner
|
|
||||||
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
|
|
||||||
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
|
|
||||||
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
|
|
||||||
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
|
|
||||||
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
|
|
||||||
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
|
|
||||||
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
|
|
||||||
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
|
|
||||||
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
|
|
||||||
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
|
|
||||||
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
|
|
||||||
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
|
|
||||||
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
|
|
||||||
|
|
||||||
-- M16: pleje under vækst
|
|
||||||
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
|
|
||||||
local idleField = M.newField(30)
|
|
||||||
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
|
|
||||||
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
|
|
||||||
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
|
|
||||||
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
|
|
||||||
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
|
|
||||||
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
|
|
||||||
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
|
|
||||||
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
|
|
||||||
local cared = M.startCare(idleField, "fertilizing")
|
|
||||||
check("M16 pleje-trin", cared.stage, "fertilizing")
|
|
||||||
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
|
|
||||||
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
|
|
||||||
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
|
|
||||||
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
|
|
||||||
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
|
|
||||||
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
|
|
||||||
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
|
|
||||||
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
|
|
||||||
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
|
|
||||||
|
|
||||||
-- M17: såafgrøde og gem/indlæs af den udvidede plan
|
|
||||||
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
|
|
||||||
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
|
|
||||||
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
|
|
||||||
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
|
|
||||||
local round = M.fromRecord(M.toRecord(full))
|
|
||||||
check("M17 trin", round.stage, "weeding")
|
|
||||||
check("M17 sæt", round.plan.rigs, 3)
|
|
||||||
check("M17 vogne", round.plan.unloaders, 2)
|
|
||||||
check("M17 stub", round.plan.mulch, true)
|
|
||||||
check("M17 kalk fra", round.plan.lime, false)
|
|
||||||
check("M17 jord", round.plan.soil, "plow")
|
|
||||||
check("M17 så", round.plan.sow, true)
|
|
||||||
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
|
|
||||||
check("M17 tromle", round.plan.roll, true)
|
|
||||||
check("M17 gødning fra", round.plan.fertilize, false)
|
|
||||||
check("M17 ukrudt fra", round.plan.weed, false)
|
|
||||||
check("M17 art", round.kind, "harvest")
|
|
||||||
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
|
|
||||||
check("M17 pleje-tæller", round.careRuns.weeding, 1)
|
|
||||||
check("M17 produkt", round.plan.product, "harvest")
|
|
||||||
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
|
|
||||||
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
|
|
||||||
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
|
||||||
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
|
||||||
|
|
||||||
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
|
|
||||||
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
|
|
||||||
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
|
|
||||||
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
|
|
||||||
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
|
||||||
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
|
||||||
|
|
||||||
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
|
|
||||||
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
|
|
||||||
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
|
|
||||||
check("M19 mål -> auto", wheatNow.mode, "auto")
|
|
||||||
check("M19 høst byg -> så hvede", wheatNow.sow, true)
|
|
||||||
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
|
|
||||||
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
|
|
||||||
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
|
|
||||||
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
|
|
||||||
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
|
|
||||||
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
|
|
||||||
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
|
|
||||||
local bareTarget = M.applyTarget(target.plan, "bare", nil)
|
|
||||||
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
|
|
||||||
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
|
|
||||||
local started = M.start(target, "harvest", {}, "BARLEY")
|
|
||||||
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
|
|
||||||
check("M19 start: høst først", started.stage, "harvesting")
|
|
||||||
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
|
|
||||||
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
|
|
||||||
-- M20: skal en bar mark med mål starte? (kun når målafgrøden kan sås nu)
|
|
||||||
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
|
|
||||||
check("M20 bar + ikke såtid -> vent", M.shouldStartBare(target, "bare", false), false)
|
|
||||||
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
|
|
||||||
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
|
|
||||||
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
|
|
||||||
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
|
|
||||||
|
|
||||||
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
|
|
||||||
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
|
|
||||||
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
|
|
||||||
check("M21 færdig -> venter", chosen.stage, "idle")
|
|
||||||
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
|
|
||||||
local cleared = M.setTarget(chosen, "")
|
|
||||||
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
|
|
||||||
|
|
||||||
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
|
|
||||||
local strawBase = {soil = "none", sow = false, lime = false}
|
|
||||||
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
|
|
||||||
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
|
|
||||||
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
|
|
||||||
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
|
|
||||||
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
|
|
||||||
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
|
|
||||||
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
|
|
||||||
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
|
|
||||||
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
|
|
||||||
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
|
|
||||||
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
|
|
||||||
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
|
|
||||||
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
|
|
||||||
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
|
|
||||||
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
|
|
||||||
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
|
|
||||||
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- falsk XMLFile: gemmer værdier pr. nøgle
|
|
||||||
local function fakeXml()
|
|
||||||
local values = {}
|
|
||||||
local xml = {}
|
|
||||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
|
||||||
xml["set" .. kind] = function(_, key, value) values[key] = value end
|
|
||||||
xml["get" .. kind] = function(_, key) return values[key] end
|
|
||||||
end
|
|
||||||
return xml
|
|
||||||
end
|
|
||||||
|
|
||||||
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
|
|
||||||
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
|
|
||||||
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
|
|
||||||
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
|
|
||||||
})
|
|
||||||
local record = ADFieldModel.toRecord(field)
|
|
||||||
for key in pairs(record) do
|
|
||||||
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
|
|
||||||
end
|
|
||||||
local xml = fakeXml()
|
|
||||||
ADFieldStorage.writeRecord(xml, "k", record)
|
|
||||||
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
|
|
||||||
local again = ADFieldModel.toRecord(back)
|
|
||||||
for key, value in pairs(record) do
|
|
||||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
|
||||||
end
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldView.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldView.lua")
|
|
||||||
local V = ADFieldView
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local infos = {
|
|
||||||
{id = 109, mowable = true, markerId = 5},
|
|
||||||
{id = 12, mowable = true, markerId = nil},
|
|
||||||
{id = 90, mowable = false, markerId = 7},
|
|
||||||
}
|
|
||||||
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
|
|
||||||
|
|
||||||
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
|
|
||||||
local rows = V.rows(infos, fields)
|
|
||||||
check("V1 tre rækker", #rows, 3)
|
|
||||||
check("V1 sorteret", rows[1].info.id, 12)
|
|
||||||
check("V1 model med", rows[3].field.id, 109)
|
|
||||||
-- V2: mangler model -> ny standardmark
|
|
||||||
check("V2 standardmodel", rows[2].field.stage, "idle")
|
|
||||||
-- V3: status — manglende markør vinder over trin
|
|
||||||
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
|
|
||||||
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
|
|
||||||
|
|
||||||
-- V4: planens tekstnøgler pr. markart
|
|
||||||
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
|
|
||||||
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
|
|
||||||
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
|
|
||||||
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
|
|
||||||
-- V5: jordtilstand som korte tekstdele
|
|
||||||
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
|
|
||||||
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
|
|
||||||
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFields.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
|
||||||
local F = ADFields
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- F1/F2: markørnavne
|
|
||||||
check("F1 F109", F.parseMarkerName("F109"), 109)
|
|
||||||
check("F1 småt", F.parseMarkerName("f7"), 7)
|
|
||||||
check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12)
|
|
||||||
check("F2 andet navn", F.parseMarkerName("Field 109"), nil)
|
|
||||||
check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil)
|
|
||||||
check("F2 nil", F.parseMarkerName(nil), nil)
|
|
||||||
|
|
||||||
-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket
|
|
||||||
local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"}
|
|
||||||
local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}}
|
|
||||||
local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}}
|
|
||||||
local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end)
|
|
||||||
check("F3 græs", mowable[1] ~= nil, true)
|
|
||||||
check("F3 græs skår", mowable[1].windrow, 10)
|
|
||||||
check("F3 græs hø", mowable[1].hay, 11)
|
|
||||||
check("F3 rug (halm) ikke", mowable[2], nil)
|
|
||||||
check("F3 kløver", mowable[3].hay, 31)
|
|
||||||
check("F3 bønnehalm -> halm ikke", mowable[4], nil)
|
|
||||||
|
|
||||||
-- F4: markens art og om den vokser
|
|
||||||
local kind, growing = F.classify(3, false, 0.4)
|
|
||||||
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
|
|
||||||
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
|
|
||||||
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
|
|
||||||
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
|
|
||||||
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
|
|
||||||
|
|
||||||
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
|
|
||||||
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
|
|
||||||
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
|
|
||||||
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
|
|
||||||
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
|
|
||||||
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
|
|
||||||
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
|
|
||||||
|
|
||||||
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
|
|
||||||
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
|
|
||||||
check("F6 intet", F.growthPhase(0, wheat), "none")
|
|
||||||
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
|
|
||||||
check("F6 klar", F.growthPhase(6, wheat), "ready")
|
|
||||||
check("F6 visnet", F.growthPhase(8, wheat), "withered")
|
|
||||||
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
|
|
||||||
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
|
|
||||||
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
|
|
||||||
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
|
|
||||||
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
|
|
||||||
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
|
|
||||||
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
|
|
||||||
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
|
|
||||||
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFlowView.lua (fra repo-roden)
|
|
||||||
-- Ren oprydning af Flows-fanen: tavlens rækkefølge, hvilke traktorer og bygninger der vises.
|
|
||||||
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
|
||||||
local V = ADFlowView
|
|
||||||
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then
|
|
||||||
print("OK " .. name)
|
|
||||||
else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local function ids(list, field)
|
|
||||||
local out = {}
|
|
||||||
for _, item in ipairs(list) do table.insert(out, tostring(item[field or "id"])) end
|
|
||||||
return table.concat(out, ",")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FV1: tavlen — forsyning (Ind) først, så udkørsel (Ud); hver gruppe sorteret efter navn (naturligt)
|
|
||||||
local runs = {
|
|
||||||
{id = 1, name = "Pig Food", kind = "supply"},
|
|
||||||
{id = 2, name = "Milk Sale", kind = "outbound"},
|
|
||||||
{id = 3, name = "Cow Food", kind = "supply"},
|
|
||||||
{id = 4, name = "Biogas 10", kind = "supply"},
|
|
||||||
{id = 5, name = "Biogas 9", kind = "supply"},
|
|
||||||
{id = 6, name = "Digestate", kind = "outbound"},
|
|
||||||
}
|
|
||||||
local groups = V.boardGroups(runs)
|
|
||||||
check("FV1 to grupper", #groups, 2)
|
|
||||||
check("FV1 Ind først", groups[1].kind, "supply")
|
|
||||||
check("FV1 Ind sorteret (Biogas 9 før 10)", ids(groups[1].runs), "5,4,3,1")
|
|
||||||
check("FV1 Ud sorteret", ids(groups[2].runs), "6,2")
|
|
||||||
check("FV1 samlet rækkefølge til vælgeren", ids(V.orderedRuns(runs)), "5,4,3,1,6,2")
|
|
||||||
check("FV1 tom gruppe udelades", #V.boardGroups({{id = 1, name = "A", kind = "supply"}}), 1)
|
|
||||||
|
|
||||||
-- FV2: traktorer — egne først, så ledige brugbare; andre flows og låste kun med "vis alle"
|
|
||||||
local vehicles = {
|
|
||||||
{id = "a", label = "Zetor", state = "free"},
|
|
||||||
{id = "b", label = "Fendt", state = "in"},
|
|
||||||
{id = "c", label = "Case", state = "other"},
|
|
||||||
{id = "d", label = "Claas", state = "blocked"},
|
|
||||||
{id = "e", label = "Deutz", state = "free"},
|
|
||||||
}
|
|
||||||
check("FV2 standard: egne + ledige, sorteret", ids(V.vehicleRows(vehicles, false)), "b,e,a")
|
|
||||||
check("FV2 vis alle: + andre flows + låste til sidst", ids(V.vehicleRows(vehicles, true)), "b,e,a,c,d")
|
|
||||||
check("FV2 antal skjulte", V.hiddenCount(vehicles, V.vehicleRows(vehicles, false)), 2)
|
|
||||||
|
|
||||||
-- FV3: bygninger — valgte først; uden markør skjult medmindre valgt eller "vis alle"
|
|
||||||
local buildings = {
|
|
||||||
{id = "s2", name = "Silo 2", hasMarker = true},
|
|
||||||
{id = "s10", name = "Silo 10", hasMarker = true},
|
|
||||||
{id = "x", name = "Gammel lade", hasMarker = false},
|
|
||||||
{id = "y", name = "Anden uden", hasMarker = false},
|
|
||||||
{id = "a", name = "Anlæg", hasMarker = true},
|
|
||||||
}
|
|
||||||
local chosen = {s10 = true, y = true}
|
|
||||||
check("FV3 standard: valgte først, så med markør", ids(V.buildingRows(buildings, chosen, false)), "y,s10,a,s2")
|
|
||||||
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,s10,a,s2,x")
|
|
||||||
|
|
||||||
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
|
|
||||||
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
|
|
||||||
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
|
|
||||||
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adLoadSwap.lua (fra repo-roden)
|
|
||||||
-- Ren rangering af aflæsnings-siloer (lagermål for udkørsel og læs-bytte).
|
|
||||||
|
|
||||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
|
|
||||||
local L = ADLoadSwap
|
|
||||||
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then
|
|
||||||
print("OK " .. name)
|
|
||||||
else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- LS1: markører et andet forløb HENTER fra (fx "Milk Storage Load") vælges sidst; ellers har-varen, så afstand
|
|
||||||
local candidates = {
|
|
||||||
{marker = {id = 1, name = "Milk Storage Load"}, hasFillType = true, distance = 100},
|
|
||||||
{marker = {id = 2, name = "Milk Storage unload"}, hasFillType = true, distance = 400},
|
|
||||||
{marker = {id = 3, name = "Silo tom"}, hasFillType = false, distance = 10},
|
|
||||||
}
|
|
||||||
local ranked = L.rankDumpCandidates(candidates, {[1] = true})
|
|
||||||
check("LS1 afhentningsmarkør undgås", ranked[1].marker.id, 2)
|
|
||||||
check("LS1 derefter tom silo før afhentning", ranked[2].marker.id, 3)
|
|
||||||
check("LS1 afhentning sidst (stadig mulig)", ranked[3].marker.id, 1)
|
|
||||||
local plain = L.rankDumpCandidates(candidates, nil)
|
|
||||||
check("LS1 uden undgå-liste: har varen + nærmest", plain[1].marker.id, 1)
|
|
||||||
|
|
||||||
-- LS2: lagermål — rene siloer altid; egne fabrikker der bruger varen som input kun når tilladt (udkørsel)
|
|
||||||
local SLURRY, MILK = 20, 21
|
|
||||||
g_currentMission = {getFarmId = function() return 1 end}
|
|
||||||
local biogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 1 end}
|
|
||||||
local foreignBiogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 0 end}
|
|
||||||
local silo = {spec_silo = {}}
|
|
||||||
local barn = {spec_silo = {}, getHusbandryFillLevel = function() return 0 end}
|
|
||||||
check("LS2 silo", L.isStoreTarget({owningPlaceable = silo}, SLURRY, nil, false), true)
|
|
||||||
check("LS2 stald aldrig", L.isStoreTarget({owningPlaceable = barn}, SLURRY, nil, true), false)
|
|
||||||
check("LS2 biogas kun når fabrikker tilladt", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, false), false)
|
|
||||||
check("LS2 biogas tager gylle", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, true), true)
|
|
||||||
check("LS2 biogas tager ikke mælk", L.isStoreTarget({owningPlaceable = biogas}, MILK, nil, true), false)
|
|
||||||
check("LS2 fremmed anlæg aldrig", L.isStoreTarget({owningPlaceable = foreignBiogas}, SLURRY, nil, true), false)
|
|
||||||
check("LS2 hjemmet selv aldrig", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, {owningPlaceable = biogas}, true), false)
|
|
||||||
g_currentMission = nil
|
|
||||||
|
|
||||||
-- LS3: tvunget lagermål — kun de valgte markører, i rækkefølge; første der tager varen, har plads og kan nås
|
|
||||||
do
|
|
||||||
local stations = {
|
|
||||||
[101] = {accepts = {[SLURRY] = true}, free = 0}, -- BGA fuld
|
|
||||||
[102] = {accepts = {[SLURRY] = true}, free = 900000}, -- gylletank
|
|
||||||
[103] = {accepts = {[MILK] = true}, free = 900000}, -- tager ikke gylle
|
|
||||||
}
|
|
||||||
for _, st in pairs(stations) do
|
|
||||||
st.getIsFillTypeSupported = function(self, ft) return self.accepts[ft] == true end
|
|
||||||
st.getFreeCapacity = function(self) return self.free end
|
|
||||||
end
|
|
||||||
ADSmartPickup = {getFarmId = function() return 1 end, getUnloadStationAtWayPointId = function(_, id) return stations[id] end}
|
|
||||||
local adEnv = {ADGraphManager = {getMapMarkerByWayPointId = function(_, id) return {id = id, name = "m" .. id} end}}
|
|
||||||
local vehicle = {}
|
|
||||||
local all = function() return true end
|
|
||||||
check("LS3 fuld BGA springes over -> tank", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 102)
|
|
||||||
stations[101].free = 500000
|
|
||||||
check("LS3 BGA med plads vælges først", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 101)
|
|
||||||
local none, why = L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {103}, all)
|
|
||||||
check("LS3 intet valgt mål tager varen -> nil", none, nil)
|
|
||||||
check("LS3 forklaring", why ~= nil and why:find("valgt", 1, true) ~= nil, true)
|
|
||||||
check("LS3 ikke nåelig -> næste", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, function(id) return id ~= 101 end).id, 102)
|
|
||||||
ADSmartPickup = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user