178 Commits

Author SHA1 Message Date
masterdraco 089fb98782 docs: download og installation i README 2026-09-29 20:52:28 +02:00
masterdraco 1739b01d92 feat: flow-guide tildeler ledige vogne til solo-traktorer
Solo-traktorer (ingen vogn bagpå) er valgbare når en frakoblet vogn med
Connect-markør kan bære flowets varer. Guiden foreslår en vogn (navnematch,
så Disponibel) med vognvalg pr. traktor; flowet husker parret ved gem, så
vognens parkering ikke skal omdøbes.
2026-09-29 20:48:04 +02:00
masterdraco 87b030d492 fix(fields): tydelig linje øverst i Fields når Stop alt-pausen er aktiv (gemmes med savegamet) (v1.60.9.0) 2026-09-29 20:01:52 +02:00
masterdraco fbd3b2c643 fix(fields): gemt produkt (silage/hø) gælder kun slåafgrøder — F22 vinmark fortsatte som 'Silage + så' (v1.60.8.0) 2026-09-29 20:01:33 +02:00
masterdraco 0a4ef9624d fix(pulje): traktor med redskab på sin egen parkering er et parkeret sæt, ikke forældreløs (vinmarks-REX blev skilt ad) (v1.60.7.0) 2026-09-29 19:56:25 +02:00
masterdraco 781c81752c fix(flows): tømning bruger aldrig marksiloer; kan en silo ikke tage imod, prøves den næste (op til 3) før parkering med last (v1.60.6.0) 2026-09-29 19:11:17 +02:00
masterdraco 8b4df6b15e fix(pulje): efter indlæsning følges traktorer med puljeredskab uden ejer som forældreløse — sættes tilbage efter 2 min uden opgave (v1.60.5.0) 2026-09-29 19:04:10 +02:00
masterdraco 1c6cda3c10 fix(pulje/flows): redskab koblet på uden ejer (hentet før genindlæsning) sættes tilbage efter 2 min; flow husker kun vogn når et kørende flow stoppes, ikke ved oprydning efter indlæsning (v1.60.4.0) 2026-09-29 19:03:49 +02:00
masterdraco 02bd9b8fd4 chore(diag): DEBUG-linje også når vognen står ved målet med plads og læsset ikke falder i 30 s (EDK 650 ved fabriks-aflæsning) (v1.60.3.0) 2026-09-29 18:54:41 +02:00
masterdraco 5a4be10996 fix(fields): vin og oliven er aldrig slåafgrøder (et mod satte druer på MOWER-listen → F22 fik 'Silage + så' og en skårlægger) (v1.60.2.0) 2026-09-29 18:51:25 +02:00
masterdraco da9edaaabc feat(gui): Smart Pickup-fanen ligger lige over spillets Indstillinger i ESC-menuen (flyttes på alle tre lister som FarmOperationsDashboard) (v1.60.1.0) 2026-09-29 18:40:40 +02:00
masterdraco 3702073c8c feat(lagre): 'Kræver handling' → flow: løber tør = åbn/opret forsyningsflow (eller 'producér/køb' når et flow ikke hjælper), sælg nu = åbn/opret salgsflow; hint på hver linje (v1.60.0.0) 2026-09-29 18:30:54 +02:00
masterdraco ccd019d32a fix(lagre): statuslinjen viser ikke længere det sidst valgte flows status i lagerfanerne 2026-09-29 18:28:36 +02:00
masterdraco 6486d77cb4 fix(måned): egen række-skabelon med bredere Vindue-kolonne — 'OBS: sidste såmåned' klippes ikke (v1.59.11.0) 2026-09-29 18:24:24 +02:00
masterdraco 9c55859853 fix(marksilo): kolonneoverskrift 'En route' klippes ikke; liter vises kompakt (v1.59.10.0) 2026-09-29 18:23:43 +02:00
masterdraco fd3f002275 fix(fields): markvinduet viser ikke længere marksilo-advarsler (gælder ikke marken; Field Silos-fanen tømmer selv) (v1.59.9.0) 2026-09-29 18:22:34 +02:00
masterdraco f893b5e8ca feat(fields): Pleje har Automatisk/Manuelt som Klargøring — Automatisk = gødning, ukrudt og kalk; resuméet viser 'Automatisk' (v1.59.8.0) 2026-09-29 18:21:29 +02:00
masterdraco fc646ab00b feat(flows): besked på skærmen når solgte traktorer fjernes fra et flow; rød når flowet står uden traktorer (v1.59.7.0) 2026-09-29 18:18:27 +02:00
masterdraco 45f58abcf0 fix(flows): traktorer der ikke findes længere fjernes fra flowet 2 min efter indlæsning (ikke hvis alle mangler); resuméet viser aldrig rå køretøjs-id (v1.59.6.0) 2026-09-29 18:13:30 +02:00
masterdraco 3d11ee9748 fix(ui): redigeringsvinduet i menuens mørke tone (lineær farve 0.016) og så bredt som rækkerne (v1.59.5.0) 2026-09-29 18:10:27 +02:00
masterdraco 985114c9bf fix(ui): tekstrækker op til 90 tegn pr. linje (før 66); 'need' → 'efter behov — laveste først' + forklaring af farver og behov i Flows-listens hint (v1.59.4.0) 2026-09-29 18:08:50 +02:00
masterdraco 0cac934480 fix: Fields under Stop alt-pause vises stoppet med [Start], [Start] genoptager kun den mark; listen skjules bag redigeringsvinduet (tekst skinnede igennem) (v1.59.3.0) 2026-09-29 18:01:44 +02:00
masterdraco 9b5d71b180 fix(flows): flow der ikke er stoppet og har kørende traktorer tæller som kørende efter genindlæsning — listen viste [Start] ved kørende flows (v1.59.2.0) 2026-09-29 17:59:11 +02:00
masterdraco 07dada58a9 chore: v1.59.1.0 — review-rettelser til Flows/Fields-liste og redigeringsvindue 2026-09-29 17:52:32 +02:00
masterdraco f64959e2c3 fix(gui): review — vinduets placering (størrelse før position, genanvendes hvert sekund, profil uden midt-anker), fokus ved sletning af vinduets rækker, ESC via requestClose, [Start] kræver markør, forældet rød status, markvindue uden Slet 2026-09-29 17:52:23 +02:00
masterdraco e892348374 chore: v1.59.0.0 — Fields-liste + redigeringsvindue (etape 2) 2026-09-29 17:45:47 +02:00
masterdraco 19ed87ce0f feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet 2026-09-29 17:45:47 +02:00
masterdraco 44f1204be9 feat(fields): ren statusfunktion til Fields-listen 2026-09-29 17:44:16 +02:00
masterdraco e9fd920f7e chore: v1.58.0.0 — Flows-liste + redigeringsvindue (etape 1) 2026-09-29 17:43:52 +02:00
masterdraco 9435db929d feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet 2026-09-29 17:43:52 +02:00
masterdraco f068c910c7 feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet 2026-09-29 17:42:35 +02:00
masterdraco 0cfb983506 refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue) 2026-09-29 17:40:35 +02:00
masterdraco 345e7303c1 feat(flows): ren statusfunktion + farver til Flows-listen 2026-09-29 17:40:06 +02:00
masterdraco 1ca1a30b0e docs(plan): Flows/Fields liste + redigeringsvindue (8 opgaver, 2 etaper) 2026-09-29 17:37:55 +02:00
masterdraco 21f2e6fee6 docs(spec): Flows/Fields som farvet liste med Start/Stop pr. række + redigeringsvindue 2026-09-29 17:28:30 +02:00
masterdraco 5649c6c59d fix: AutoDrive tælles ikke som Courseplay (flow-traktor med AD aktiv ventede på sig selv ved Start efter indlæsning); venteårsag logges kun ved ændring (v1.57.4.0) 2026-09-29 17:10:07 +02:00
masterdraco 3f7b85ba54 fix(flows): manuel påkobling starter også traktoren i et flow hvis Start blev afvist (ikke stoppet af spilleren) (v1.57.3.0) 2026-09-29 16:04:31 +02:00
masterdraco 6ec6a75f95 fix(udkørsel): 'All'-pulje gemmes som tom streng — ankerpunkt ved kilderne gælder nu også dér (v1.57.2.0) 2026-09-29 15:23:29 +02:00
masterdraco a694d17a67 fix(udkørsel): 'Wait at: All' vælger det ledige Wait-punkt nærmest flowets kilder, ikke nærmest traktoren (v1.57.1.0) 2026-09-29 15:23:14 +02:00
masterdraco 793f792f8b fix(forsyning): mindst fyldte kilde først kun blandt kilder med >= 25 % af et læs (ellers fyldeste) — foder-traktorer hentede 2-10k l i en gæringssilo der fyldes løbende; knappen viser Start for et flow der aldrig kom i gang, selv med vogn på (v1.57.0.0) 2026-09-29 15:20:48 +02:00
masterdraco 2454dd17e7 fix(ui): Start med afviste traktorer viser grunden som besked i højre side; 'ingen vogn' forklarer hvad der skal til (v1.56.5.0) 2026-09-29 15:14:28 +02:00
masterdraco 7922b8d4d0 fix(flows): flowet husker vognen bag traktoren før et stop sætter den af — ellers 'noTrailer' ved næste start (Milk Store) (v1.56.4.0) 2026-09-29 15:13:49 +02:00
masterdraco 3d6a3761e5 feat(kobling): spillerens AD 'Kør til' til et puljeredskabs egen parkering kobler det af (flow-vogne og puljeredskaber; faste sæt røres ikke) (v1.56.3.0) 2026-09-29 15:01:36 +02:00
masterdraco fece920028 feat(flows): traktor i et kørende flow startes når spilleren selv har kørt den til Connect og koblet dens vogn på (v1.56.2.0) 2026-09-29 14:58:10 +02:00
masterdraco be11283ba7 fix(kobling): stoppedAway logger afstanden til markøren; flow prøver én gang mere at hente vognen efter stoppedAway; puljens skyld-regel matcher årsagen med afstand (v1.56.1.0) 2026-09-29 14:54:38 +02:00
masterdraco 3e7e3d588e feat(pulje): bedste pasform — mindste traktor med hk nok vælges; afstand kun inden for ±15 %; over 2,5x redskabets behov kun som nødløsning (v1.56.0.0) 2026-09-29 14:33:43 +02:00
masterdraco 6af89c3034 fix(flows): tømning ved stop giver ikke længere 10 min ved et fuldt mål — 45 s ved målet uden at læsset falder -> silo, ellers parkering med last; Wait-pulje/læs-bytte slået fra under tømning (v1.55.1.0) 2026-09-29 14:18:11 +02:00
masterdraco 5cf093c3f1 feat(stop): redskaber slukkes, løftes og foldes til transport når en mark, et flow eller 'stop alt' stoppes — alle påkoblede, også forrest og bag redskaber (v1.55.0.0) 2026-09-29 14:13:37 +02:00
masterdraco f472f5dd80 fix(pulje): flow-vogne holdes altid af deres flow (også stoppet), stoppet flow-traktor kun ledig solo; markredskaber kun bag traktorer (butikskategori tractors*, ikke lastbil/Unimog); præcis venteårsag i loggen (v1.54.0.0) 2026-09-29 14:09:12 +02:00
masterdraco acbbe04a11 fix(kobling): afkobling bekræftes — først 'koblet af' når intet sidder bag traktoren, ellers nyt forsøg i op til 20 s og turen stoppes; før kørte traktoren videre med vognen (v1.53.2.0) 2026-09-29 14:03:41 +02:00
masterdraco db9fdf2178 fix(flows): tømning ved stop afleverer kun ved flowets egne mål — AD's 2. markør kunne være forældet (æg-markør) eller et Wait-punkt (v1.53.1.0) 2026-09-29 13:46:26 +02:00
masterdraco 417a44841d feat(kobling): rummelig koblingsrækkevidde (3 m) og bakning de sidste 3-8 m hen til vognens trækøje ved Connect-markører; lagervisning: bredere mængde-/rate-kolonner så teksten ikke klippes (v1.53.0.0) 2026-09-29 13:44:21 +02:00
masterdraco a305de25a5 fix(flows): flow startet igen under tømning/hjemtur afbryder hjemturen og kører videre med vognen — før tømte den i op til 10 min ved forkert mål, satte vognen af og kørte i garage (v1.52.1.0) 2026-09-29 13:37:47 +02:00
masterdraco 5b26415d1e feat(lagre): vareoversigt med nøgletal, 'kræver handling' og fyldningsbjælker — klik på en vare folder den ud til bygningerne; bygningstabellen bag 'Vis: Varer | Bygninger' (v1.52.0.0) 2026-09-29 09:23:15 +02:00
masterdraco 82db248775 feat(kursus): nye Courseplay-kurser får altid 2 forager-runder og starter på forageren (ellers ind i træer i hjørnerne); smal mark -> ét forsøg uden; genbrugt kursus uden forager beregnes på ny (v1.51.5.0) 2026-09-29 01:11:38 +02:00
masterdraco a5c52cee09 feat(flows): stoppet flow tømmer vognen før den parkeres — først ved flowets mål, rest til nærmeste silo/egen fabrik der tager varen, så på plads og hjem (v1.51.4.0) 2026-09-29 00:24:18 +02:00
masterdraco a390a05e8c fix(flows): flow-traktor uden husket vogn finder sin vogn på parkeringsnavnet (flowets navn, helst samme nummer) — Cow Food blev sprunget over med noTrailer (v1.51.3.0) 2026-09-29 00:09:27 +02:00
masterdraco 368a4784f7 fix(flows): review v1.51 — start af stoppet flow venter hvis traktoren er lånt af puljen, stop/slet af et stoppet flow rører ikke lånte traktorer, oprydning efter indlæsning springer lånte over, vogn holdt af ET kørende flow, flyttet traktor glemmer sin vogn, Stop-knap også for flows der holder stille, fejlet start sætter vognen tilbage, bygning væk kun for bygninger set i sessionen, spuNameFleet rører kun egne ikke-delte parkeringer (v1.51.2.0) 2026-09-28 23:26:01 +02:00
masterdraco d2ac2fb36d feat: spuNameFleet giver traktorer uden for flows butiksnavne (+ parkering 'P <navn>'); stoppede flows' traktorer der holder med vogn efter indlæsning sætter den på plads (v1.51.1.0) 2026-09-28 23:20:10 +02:00
masterdraco b0d22236b0 feat(flows): flow-vogne — flowets vogn huskes pr. traktor, solo-traktor henter sin vogn ved start (venter hvis den er lånt ud), stop/slet/bygning væk sætter vognen på plads og kører traktoren hjem; stoppede flows er ledige i maskinpuljen (v1.51.0.0) 2026-09-28 23:18:38 +02:00
masterdraco 1759d62600 fix(marksilo): fuld fabrik — lånt vogn med last der holder ved et lager uden plads i 60 s, kører resten til et andet lager (gårdsilo) (v1.50.5.0) 2026-09-28 20:06:03 +02:00
masterdraco e778ab44b4 fix(marksilo): tom silo — lånt vogn med last ved en tom marksilo kører resten til fabrik (plads til hele resten) ellers gårdlager; AD-ture fra marksiloer overtages igen efter genindlæsning (v1.50.4.0) 2026-09-28 20:00:34 +02:00
masterdraco 59e18acc9c fix(markarbejde): kultivering/pløjning måles på jorden — er ≥95 % af marken klargjort/pløjet, går trinnet videre uden at sende maskinen ud igen (også efter genindlæsning; F17 blev kultiveret 3 gange) (v1.50.3.0) 2026-09-28 19:57:02 +02:00
masterdraco 8d33499a37 fix(kobling): plov o.l. foldes ud før afkobling (spillet kræver det) med 20 s frist; foldbare redskaber foldes sammen til vejen efter påkobling (v1.50.2.0) 2026-09-28 19:08:22 +02:00
masterdraco 5a2701fc62 fix(pulje): review v1.50 — egne tømmevogne virkede ikke under høst (global 'implement'), hentning kan afbrydes (høst slut/Stop/spiller), hjemtur ved tømmevogns-fejl, marksilo-lån kan ikke hænge i 'henter' (tidsgrænse, spiller, fejl i start), ingen ekstra parkeringstur med puljevogn, Stop holder en bar mark; regressionstest for fejlagtige globale (v1.50.1.0) 2026-09-28 19:02:28 +02:00
masterdraco 6f46bb543a feat(pulje): etape 3 — marksilo-flowet låner også ledig solo-traktor + puljevogn: henter vognen, kører Hent og aflever, sætter vognen af og kører hjem; høstreserven tæller puljevogne (v1.50.0.0) 2026-09-28 18:56:06 +02:00
masterdraco 473f6110bf feat(pulje): etape 2 — tømmevogne ved høst fra puljen: ledig traktor henter en puljevogn der kan bære afgrøden, følger høsteren til høsten er færdig, sætter vognen af og kører hjem (v1.49.0.0) 2026-09-28 18:54:44 +02:00
masterdraco 760fbac328 fix(pulje): review-minors — mark forsvundet under hentning, hjemtur ved kursus-/CP-fejl og Stop alt, ventende marker på fælles taktslag (kø holder), redskabets navn i 'for svag', hk-cache (v1.48.4.0) 2026-09-28 18:53:34 +02:00
masterdraco 6eff1dcae8 feat(pulje): flådefanen viser puljetraktorer og -redskaber med status og hvorfor de ikke er med; puljetraktorer omdøbes ikke efter lånte redskaber (v1.48.3.0) 2026-09-28 18:52:43 +02:00
masterdraco 7730f68dec fix(flåde): plov før kultivator i rolle-tabellen (FS25-plove har også spec_cultivator); en plov må også tage kultivering (v1.48.2.0) 2026-09-28 18:46:13 +02:00
masterdraco 0e714d2968 feat(markarbejde): bar mark med mål-afgrøde klargøres straks uanset årstid — kun såningen venter på såtiden (v1.48.1.0) 2026-09-28 18:33:52 +02:00
masterdraco 3f19738743 chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde) 2026-09-28 18:12:23 +02:00
masterdraco 2eec23add7 fix(pulje): review — puljeredskab sættes altid af (også påfyldte sæt), kun rolleløse traktorer henter, traktor uden vejnet springes over og får skylden ved stoppedAway, påfyldning efter koblingen, faste maskiner respekteres 2026-09-28 18:12:16 +02:00
masterdraco 3ac53b7d1f feat(pulje): marker der venter på en maskine betjenes længst-ventende først 2026-09-28 18:06:12 +02:00
masterdraco 2dc39b7849 feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem 2026-09-28 18:05:58 +02:00
masterdraco 2d170a30ff feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching) 2026-09-28 18:05:42 +02:00
masterdraco cdd253ee5f feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens 2026-09-28 18:04:49 +02:00
masterdraco 4072445105 feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk 2026-09-28 18:04:31 +02:00
masterdraco 4760ee5e19 feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne 2026-09-28 18:04:18 +02:00
masterdraco 4863e422ad docs: maskinpulje etape 1 — implementeringsplan (v1.48.0.0) 2026-09-28 18:02:01 +02:00
masterdraco 127f4cc9d9 docs: maskinpulje — design (traktorer og redskaber som fælles pulje, kø, hent/arbejd/parkér/hjem) 2026-09-28 17:56:12 +02:00
masterdraco e7b0e5c8f5 fix(kobling): ankomst måles fra nærmeste led i sættet — bakker AD med vogn, ender vognen på markøren og traktoren et vognslæb væk (stoppedAway ved Bredal) (v1.47.4.0) 2026-09-28 17:23:12 +02:00
masterdraco b8704f1f95 fix(kobling): Connect kobler kun på; i en tur sættes vognen af ved sin egen AD-parkering (advd.parkDestination) — kun i turen, så almindelig AD-parkering ikke afkobler (v1.47.3.0) 2026-09-28 17:18:14 +02:00
masterdraco f2ebc54b25 feat(kobling): Connect-markøren virker begge veje — med redskab bag sig kobles det af på sin parkeringsplads (v1.47.2.0) 2026-09-28 17:16:34 +02:00
masterdraco b57940a933 feat(kobling): spuTrip kører en tur i flere trin — Connect kobler på, Afkobl kobler af, almindelige markører holder 10 s undervejs (v1.47.1.0) 2026-09-28 17:15:49 +02:00
masterdraco be043ac220 feat(kobling): Afkobl-markører kobler redskaber bag traktoren af (frontvægt bliver); 'hent vogn og kør til' som ADHitch.trip + konsol spuTrip/spuDetach (v1.47.0.0) 2026-09-28 17:14:20 +02:00
masterdraco fde3fd4399 feat(kobling): Connect-markører — traktor der kører AD 'Kør til' en 'Connect <navn>'-markør kobler redskabet bag sig på ved ankomst; ADHitch.fetch til flows, konsol spuConnect (v1.46.0.0) 2026-09-28 17:07:07 +02:00
masterdraco 5a715bc3e9 fix(marksilo): høsten først — tømmevogne holdes tilbage til høstere der kører (planlagt antal minus udsendte); fabrik kun med plads til hele læsset (v1.45.1.0) 2026-09-28 00:51:15 +02:00
masterdraco 28d2f2c086 feat(genstart): husk ved hvert gem hvilke maskiner Courseplay kører (uden AD) og start netop dem igen efter indlæsning fra nærmeste waypoint via CP's HUD-start — også CP-jobs uden markrolle (v1.45.0.0) 2026-09-27 23:55:10 +02:00
masterdraco 6aeb8e4179 feat(marksilo): 'Kør overskud til produktion' — foder først: staldenes forbrug × valgt antal døgn bliver på gården, overskud køres til gårdens fabrikker (v1.44.0.0) 2026-09-27 22:19:26 +02:00
masterdraco 5e1d6a3659 fix(markarbejde): CP får markens punkt nærmest traktoren som position (kursets startsted) — F17-kurset startede 280 m væk, og kultivatoren kørte gennem skoven (v1.43.1.0) 2026-09-27 20:06:18 +02:00
masterdraco 5bb1ad6ae0 feat(markarbejde): høsten er færdig men høsteren har korn i tanken -> CP genstartes og venter ved kursets ende, tømmevogn sendes ud, marken går først videre når tanken er tom (én gang pr. mark) (v1.43.0.0) 2026-09-27 19:57:43 +02:00
masterdraco 521dc10df5 fix(tømmevogn): sæt retning på CP-tømmejobbets positioner når CP ikke selv nåede det — 'Angle missing' afviste tømmevognen til F15 i en time (v1.42.6.0) 2026-09-27 19:23:03 +02:00
masterdraco f3db8e4e5c feat(gui): Flows-fanen viser det valgte flow øverst og en kort liste til at vælge et andet — tavlen står kun i Oversigt (v1.42.5.0) 2026-09-27 17:02:31 +02:00
masterdraco df514fb6ec fix(markarbejde): vent op til 5 min mens Courseplay beregner kursus (store marker) i stedet for at opgive efter 60 s — F90 (140 ha) blev opgivet midt i beregningen (v1.42.4.0) 2026-09-27 16:51:39 +02:00
masterdraco 3ec4aaa179 fix(markarbejde): høst er først færdig når under 5 % af hele marken står (standingRatio); klargøring med mere stående afgrøde spoles tilbage — en halvt høstet mark kunne gå til kultivering (v1.42.3.0) 2026-09-27 16:47:57 +02:00
masterdraco 5ceb7da87d fix(markarbejde): højst én maskine sendt ud hvert 10. sekund på tværs af markerne — 6 kørte fra parkeringen samtidig efter Start alle; marksilo-navn uden dobbelt F-nummer (v1.42.2.0) 2026-09-27 16:43:12 +02:00
masterdraco c1060cddf9 fix(parkering): parkeringspunkt findes også på vognen/redskabet (advd.parkDestination) — AD kopierer det først til traktoren ved tilkobling; efter indlæsning manglede 4 af 9 (v1.42.1.0) 2026-09-27 15:32:15 +02:00
masterdraco 9c1c87b8dd feat(oversigt): 'Stop alt og parkér' — flows, markarbejde og marksilo-lån stoppes, køretøjer til AD-parkering (manuelle høstere nævnes), pause der gemmes; 'Start alle' ophæver pausen og starter traktorerne én ad gangen hvert 10. sekund (v1.42.0.0) 2026-09-27 15:20:41 +02:00
masterdraco 9348443f61 fix(navne): navnefilen skrives i spillets gem-hook — FS25 genskriver savegame-mappen og slettede ADSmartPickup_names.xml; startkø (én traktor ad gangen) forberedt (v1.41.1.0) 2026-09-27 15:18:50 +02:00
masterdraco 331d192f78 feat(navne): markmaskiner der stadig har modelnavnet navngives efter rollen (Læssevogn 5, Roeoptager 2, Mejetærsker 1 …); givne navne og flow-traktorer røres ikke (v1.41.0.0) 2026-09-27 15:12:40 +02:00
masterdraco 7d372eb5ab feat(navne): nye bygninger navngives automatisk (stald efter dyretype, gårdsilo/marksilo, palleskur, fabrik efter butiksnavn; numre fortsætter) + Omdøb bygning-knap i bygningsfanerne (v1.40.0.0) 2026-09-27 15:10:24 +02:00
masterdraco 9a284c875e feat(navne): egne bygningsnavne fra ADSmartPickup_names.xml bruges overalt (FS25 kan ikke omdøbe bygninger); marksilo: siloer i samme klynge som et gårdlager er gårdens (v1.39.0.0) 2026-09-27 15:05:14 +02:00
masterdraco 7e0a991ce6 feat(marksilo): marknummer foran silonavnet; statuslinje fortæller hvorfor ingen tømmevogn lånes (uden parkering/optaget/spiller i/ikke i flåden) (v1.38.7.0) 2026-09-27 14:58:10 +02:00
masterdraco 25867e2d88 fix(bygninger): pallespawner får aldrig aflæsningens nærmeste markør — i fårestald 3 og 5 stod spawneren tættest (5 mod 9 m), så Goat Food kun kunne levere til 3 af 5 stalde (v1.38.6.0) 2026-09-27 14:54:54 +02:00
masterdraco 490eecc25e fix(markarbejde): genstart efter indlæsning kun når maskinen står på en mark i gang med dens trin eller ≤30 m fra sit huskede waypoint — to ROPA'er kørte ad vejene mod et gammelt kursus (v1.38.5.0) 2026-09-27 14:47:52 +02:00
masterdraco bcfa237321 fix(markarbejde): CP startes ved sidste punkt (eget kursus + husket waypoint) eller nærmeste punkt (påbegyndt høst/slåning) i stedet for CP's standard 'first' — F111 blev høstet forfra (v1.38.4.0) 2026-09-27 14:43:21 +02:00
masterdraco f1b8b64668 fix(manuel transport): manuel høster springer kun skærebord-tjekket over når intet er monteret — F111 (hvede) valgte roeoptageren (v1.38.3.0) 2026-09-27 14:24:17 +02:00
masterdraco de88846166 fix(markarbejde): 'ingen maskine sendt' logger hver maskine med rollen og hvorfor (spiller i, AD/CP kører, opgave, marksilo-lån, ikke i flåden); manuel-transport-høstere afvises ikke for afstand til vejnettet (v1.38.2.0) 2026-09-27 14:20:46 +02:00
masterdraco 3a1d49ccef fix(gui): 'Kører selv på vejen' Ja/Nej i stedet for 'Manuel transport' — Ja betyder nu må sendes ud, som resten af flåden (v1.38.1.0) 2026-09-27 14:17:17 +02:00
masterdraco 6afcae6fe1 feat(markarbejde): manuel transport af brede høstere — flueben pr. høster i markflåden; moden beder spilleren køre den til marken og montere skærebordet (dialog + påmindelse hvert 5. min), overtager når den står på marken med passende skærebord og spilleren er steget ud (eller ved Start nu), tømmevogne først når høsten kører, ingen AD-parkering bagefter (v1.38.0.0) 2026-09-27 14:05:27 +02:00
masterdraco 12647087ef fix(markarbejde): høsteren sendes ikke selv til lager med rest i tanken (for stor) — CP venter ved kursets ende til tømmevognene har tømt den; kun læssevogne kører selv hjem (v1.37.2.0) 2026-09-27 13:53:59 +02:00
masterdraco a6a0dbf121 fix(markarbejde): genoptaget høster/læssevogn får lager til resten som ved normal start — ellers stod høsteren med korn i tanken efter kurset (v1.37.1.0) 2026-09-27 13:52:51 +02:00
masterdraco 565d7923cd feat(markarbejde): efter genindlæsning genstartes CP-maskiner der var midt i et job fra sidste waypoint (CP's eget fortsæt-job; HUD'ens startpunkt urørt) og meldes ind på marken igen, hvis trinnet passer til rollen (v1.37.0.0) 2026-09-27 13:51:44 +02:00
masterdraco 2b3793eeaa chore(markarbejde): log hvorfor ingen maskine sendes ud (alle optaget/ingen med rollen/…) én gang pr. ændring — før kun i fanen (v1.36.7.0) 2026-09-27 13:44:26 +02:00
masterdraco 29a142d3df feat(flow-guide): listen over traktorer der ikke kan bruges er foldet sammen som standard; linjen med antallet folder ud/ind (v1.36.6.0) 2026-09-27 13:34:23 +02:00
masterdraco a626fcdd95 fix(bygninger): pallespawnere viger for aflæsningen når begge vil have samme markør og aflæsningen står tættere — fårestaldens foderindkast fik 'Sheep Milk' som mål; gælder alle bygningstyper, rigtige læssetriggere beholder forrang (v1.36.5.0) 2026-09-27 13:31:22 +02:00
masterdraco 4ae268376d chore(diag): log pr. stald hvilke markører læsse-/aflæsningspunkterne ligger nærmest — Goat Food fik Sheep Milk-markører som mål (v1.36.4.0) 2026-09-27 13:13:52 +02:00
masterdraco 19893aaf41 feat(runs): ny grund 'AutoLoad er slået til på vognen' når en UAL-aktiv tipvogn afvises i et forsyningsflow (AutoDrive ser den kun som palle-/ballevogn) (v1.36.3.0) 2026-09-27 12:42:11 +02:00
masterdraco 37768c1507 feat(flow-guide): traktorer der ikke kan bruges i flowet vises med navn og grund (før kun et antal); fejl ved opbygning af traktorlisten logges i stedet for at skjule traktoren (v1.36.2.0) 2026-09-27 12:33:45 +02:00
masterdraco d7f4d0b3d3 fix(marksilo): siloer inden for 100 m af et gårdlager er gårdens (Farma-klynge deler lager) — sukkerroer blev kørt fra én Farma-silo til nabosiloen (v1.36.1.0) 2026-09-27 12:19:06 +02:00
masterdraco c2ab20d9aa merge: marksilo-flow (v1.36.0.0) 2026-09-27 11:50:51 +02:00
masterdraco 2e64ad537c docs(spec): marksilo-flow — flow-siloer udelades, vogne skal have parkering (fra review) 2026-09-27 10:50:55 +02:00
masterdraco fca26f5b4d fix(marksilo): review — flerværdi-valg som MultiTextOption, lagerreservation på tværs af runder, vagt kun før start, rutetjek + straks-fejl pauses, rotation fra på lånte vogne, kræver parkering, flow-siloer udelades, slettede vogne og små rester håndteres 2026-09-27 10:50:48 +02:00
masterdraco 31c993c6eb feat(gui): fanen Marksiloer — til/fra, maks. vogne, mindste mængde, gårdlagre, status pr. silo (v1.36.0.0) 2026-09-27 10:42:42 +02:00
masterdraco ccc6b608dc feat(marksilo): låner ledige tømmevogne hvert minut, AD hent-og-aflever til gårdlager, frigiver ved færdig/spiller/2 min stilstand; opsætning gemmes 2026-09-27 10:41:46 +02:00
masterdraco e9272b8b8f feat(marksilo): opsætningsmodel (til/fra, gårdlagre, x 1-10, tærskel) + records 2026-09-27 10:40:24 +02:00
masterdraco 1a8931116b feat(marksilo): ren planlægger — kvalificering, fuldeste først, x-loft, vogn- og lagervalg med reservation 2026-09-27 10:40:12 +02:00
masterdraco 8a0d7f2489 docs(plan): marksilo-flow — 4 opgaver (planlægger, opsætning, spil-lag, fane) 2026-09-27 10:39:48 +02:00
masterdraco f93ae798e3 docs(spec): marksilo-flow — låner op til x ledige vogne og kører marksiloernes varer hjem til nærmeste gårdlager 2026-09-27 10:34:51 +02:00
masterdraco 33b4f754b0 fix(runs): fyldte traktorer registreres 'på vej' ved run-start og efter AutoDrives genstart ved indlæsning — ellers så mål-valget 0 vogne på vej og sendte alle til samme biogas-drop; startloggen viser det valgte mål (v1.35.3.0) 2026-09-27 10:15:34 +02:00
masterdraco 6fbf05273e chore(diag): log hvorfor aktive AutoDrive-køretøjer står stille >20 s (opgave, forhindring, AD-trafik + blokerer, pause, Smart Pickup-hold) — flows kører ikke efter genstart (v1.35.2.0) 2026-09-27 09:32:58 +02:00
masterdraco 2beca0924a fix(måned): fremdrift for høst/slåning ud fra hele arealet — slået græs faldt ud af readyRatio's nævner, så halvt slået F90 viste 0 %; Courseplays kursus-position bruges kun til øvrige trin (v1.35.1.0) 2026-09-27 09:25:34 +02:00
masterdraco 5df4fdc7c3 feat(gui): fanen Måned — marker i gang med fremdrift, næste trin og om høst-/såvinduet lukker ved månedsskiftet; dom 'du kan sove' + marker der kan sås nu (v1.35.0.0) 2026-09-27 09:12:49 +02:00
masterdraco 5274925f2a fix(markarbejde): utoppede kartofler (forberedelige stadier) tæller som høstklare; klargøring på en mark med uhøstet afgrøde spoles tilbage til klar (F16) (v1.34.2.0) 2026-09-27 09:09:08 +02:00
masterdraco 307676004e fix(markarbejde): slåmarker regnes først klar i fuld højde; afbrudt slåning venter på genvækst i stedet for at klippe græs i mellemhøjde (F90) (v1.34.1.0) 2026-09-27 08:57:32 +02:00
masterdraco 50301b61b9 feat(markarbejde): genbrug Courseplay-kurser — markform-signatur + kursus-register pr. maskine; genbrug/kopi i stedet for ny beregning, glemmes ved ændret mark (v1.34.0.0) 2026-09-26 22:27:16 +02:00
masterdraco 39fa72ba0d test: O14 tæller rutetjek markør->markør (efter perf-ændringen i v1.33.4.0) 2026-09-26 22:14:22 +02:00
masterdraco ac3db0d826 perf: rutetjek frem+tilbage går markør->markør og gemmes 5 min (var 30-39 ms pr. kald, genberegnet hver 10 m); bygningsliste 30 s — hak på 44-67 ms (v1.33.4.0) 2026-09-26 22:13:50 +02:00
masterdraco 4aa221820d fix(markarbejde): intet sendes ud de første 2 min efter indlæsning, så Courseplay/AutoDrive kan genoptage egne jobs (kultivatoren blev taget midt i spillerens CP-job) (v1.33.3.0) 2026-09-26 22:04:32 +02:00
masterdraco 6a835b9efb fix(markarbejde): maskiner over 60 m fra AD-vejnettet sendes ikke ud (AutoDrive 'Could not calculate path'); ingen genforsøg når AD stopper uden at flytte sig (v1.33.2.0) 2026-09-26 22:00:45 +02:00
masterdraco f9d875fc3e fix(markarbejde): vinmark — sprøjter/spredere springer 1 vinrække over (arbejder begge sider af gangen) (v1.33.1.0) 2026-09-26 21:40:18 +02:00
masterdraco 954dd25c51 feat(udkørsel): 'Sælg altid når kilden er over X %' (sælger uanset pris, bedste salgssted der kan nås); fix: flydende varer lagres ikke automatisk i en fabriks rist (gyllevogn holdt ved AIO) (v1.33.0.0) 2026-09-26 21:37:20 +02:00
masterdraco 10838779bc fix(markarbejde): vinmark — Courseplays vinkursus lægges til venstre for vinen (i gangen) for sprøjte/spreder/harve, på vinen for høsteren (sprøjten kørte ind i stolperne) (v1.32.5.0) 2026-09-26 21:33:27 +02:00
masterdraco 59b6ac1e47 fix(markarbejde): forkert vare hældes kun ud som lille rest (≤10 %, ≤2000 l); fuld sprøjte/spreder bruges ikke til en anden opgave (100.000 l gødning blev hældt ud) (v1.32.4.0) 2026-09-26 21:28:17 +02:00
masterdraco a04c16552a fix(gui): advarsel øverst i Fields når markflåden står på 'kun med flueben' uden flueben (intet kan sendes ud) (v1.32.3.0) 2026-09-26 21:21:16 +02:00
masterdraco dde6f4b8eb fix(markarbejde): høsteren får lager ved høststart; korn i tanken efter høsten køres af AutoDrive til lageret, ellers tydelig besked (v1.32.2.0) 2026-09-26 21:19:07 +02:00
masterdraco 151954bd37 refactor(gui): fjern død kode fra den gamle flow-detaljeliste (retning/bygninger/varer/kilder-rækker) efter review; marklisten til guidens trin 1 hentes kun når den bruges (v1.32.1.0) 2026-09-26 20:39:23 +02:00
masterdraco 9b51a68731 feat(gui): guiden "Opsæt flow" — Flows-fanen får tavle + resumé (klik = ret ét trin, Navn = omdøb) og trin-for-trin-guide på samme ramme som markguiden; Nyt flow oprettes først ved Gem (v1.32.0.0) 2026-09-26 20:28:12 +02:00
masterdraco 92af260a79 feat(gui): guiden "Opsæt mark" — markfanen får oversigt med resumé (klik på en linje = ret ét trin), trin-for-trin-guide på fælles guide-ramme (ADGuide/ADFieldGuide) og egen markflåde-visning (v1.31.0.0) 2026-09-26 20:20:56 +02:00
masterdraco 7cbd8fb878 docs: spec — guider til opsætning af marker og flows (etape A+B) 2026-09-26 19:33:13 +02:00
masterdraco f174c10da5 fix(gui): klik på markens faste maskiner blev sendt til flowets traktorliste (GUI-fejl adRunsController:29, valget blev aldrig gemt); flowets traktorklik kræver et køretøj (v1.30.4.0) 2026-09-26 19:21:40 +02:00
masterdraco a95cb9e71d fix(gui): maskiner der er faste på en anden mark vises ikke ved den valgte mark (hverken faste maskiner eller markflåden) — 'N maskiner er faste på andre marker (F22)' (v1.30.3.0) 2026-09-26 19:08:21 +02:00
masterdraco 780b6a0daa fix(gui): marktabellens status er kort (trin + kort årsag, fx 'Gødskning: bredde ukendt'); hele forklaringen står i markens detaljer; kortere hint (v1.30.2.0) 2026-09-26 19:01:58 +02:00
masterdraco 61e85e1402 fix(gui): markflåden er tydeligt fælles for alle marker og viser 'fast på F…' / 'i gang på F…'; 50,0 m markeres ikke som over 50 m; rollen hedder gødskning (v1.30.1.0) 2026-09-26 18:45:25 +02:00
masterdraco 4c60fd555f feat(markarbejde): faste maskiner pr. mark (bruges kun dér; marken venter på sin egen); fix: AD-påfyldning får varen sat (kom tom tilbage); AD der stopper undervejs startes igen 2x, ellers hjem til parkering (v1.30.0.0) 2026-09-26 18:09:12 +02:00
masterdraco 9ef1eb6655 fix(markarbejde): påfyldning fra gårdens eget lager før købestation (gylle blev købt trods 7 mio. l ved BGA) (v1.29.6.0) 2026-09-26 18:02:30 +02:00
masterdraco 77253b5d2b feat(gui): markens lagervalg viser markørnavn og afstand, marksilo/gæringssilo først, kun de 8 nærmeste (34 ens Farma-rækker) (v1.29.5.0) 2026-09-26 17:37:10 +02:00
masterdraco 1b0a2c152b feat(gui): lange tekstrækker ombrydes over flere linjer i stedet for at blive afkortet; markdetaljen viser hele statusteksten (v1.29.4.0) 2026-09-26 16:29:21 +02:00
masterdraco 813ee6d0b9 fix(markarbejde): påfyldning også ved købestationer (buyingStation, ingen lagerbeholdning) — 'Liquid Buy Station' blev aldrig fundet; markør inden for 30 m af trigger eller 40 m af bygningen (v1.29.3.0) 2026-09-26 15:45:05 +02:00
masterdraco 455c9c7598 fix(markarbejde): tom sprøjte/spreder/såmaskine fyldes af AutoDrive (LOAD) før marken; uden påfyldning med AD-markør sendes den ikke ud; Courseplay startes aldrig med tom tank (sprøjten kørte F108 tom) (v1.29.2.0) 2026-09-26 15:08:47 +02:00
masterdraco 92e65a6431 fix(markarbejde): sæt med arbejdsbredde under 1,5 m sendes aldrig ud (spreder uden AI-markører gav 0,5 m -> CP-kursus hver halve meter på 72 ha -> spillet frøs); advarsel i markflåden (v1.29.1.0) 2026-09-26 14:52:47 +02:00
masterdraco e02d8af3d5 feat(markarbejde): bygget-over marker (groundType NONE / ingen markjord — stald, biogas, gårdsplads) er ikke med i oversigten eller arbejdet (v1.29.0.0) 2026-09-26 12:59:43 +02:00
masterdraco 5d06c46fa9 perf(markarbejde): markscanning kører op til 8 opgaver pr. frame inden for 0,5 ms — marklisten fyldes hurtigt efter genstart (v1.28.9.0) 2026-09-26 12:51:50 +02:00
masterdraco 6138b6b874 feat(markarbejde): tom spreder/sprøjte kan bruges (roller efter hvad den kan tage; påfyldning med rollens vare); spreder med forkert vare tømmes først og vælges kun hvis ingen anden kan (v1.28.8.0) 2026-09-26 12:51:03 +02:00
masterdraco abbe40da01 fix(markarbejde): afgrøde og vækststadie fra field.fieldState (spillets marktilstand); getFieldDataAtWorldPosition's 2. værdi er jord-bits, ikke vækst — F108 majs = bar jord; høstet mark vises som 'stub (afgrøde)' (v1.28.7.0) 2026-09-26 12:38:25 +02:00
masterdraco 36f381fc36 fix(markarbejde): markens omrids fra getDensityMapPolygon (FS25 har ingen polygonPoints) — flere prøvepunkter, markør-afstand til kanten; F108 majs sås som bar jord og F<nr>-markører 'manglede' på store marker (v1.28.6.0) 2026-09-26 12:28:27 +02:00
masterdraco 9be8fa98f0 fix(markarbejde): traktorer i et almindeligt flow er ikke markflåde (vises ikke og sendes ikke ud) (v1.28.5.0) 2026-09-26 12:15:00 +02:00
masterdraco 08d983800c fix(gui): bygningsrækker i fast rækkefølge (navn, så markør) — et tryk flyttede valgte drops op i toppen; fjernet bygning vises som 'Fjernet bygning' i stedet for rå id (v1.28.4.0) 2026-09-26 12:03:56 +02:00
masterdraco 41e50ccec8 fix: forsyning med flere mål vælger målet med færrest andre vogne på vej før mindst fyldte (3 biogas-vogne i kø ved samme drop) (v1.28.3.0) 2026-09-26 11:54:14 +02:00
masterdraco 725ad7b459 fix: æg-markør genkendes som hønsehus når det er nærmere end en løs læssestation (Pickup 1+2 blev Pigsty); palletildeling måler til hele spawn-området (alle rækker, start-slut) (v1.28.2.0) 2026-09-26 11:52:22 +02:00
masterdraco cb68c81a3f fix: æg-paller tælles kun for nærmeste spawner — tomt hønsehus så naboernes paller (hus 1+2 blev aldrig hentet) (v1.28.1.0) 2026-09-26 11:29:49 +02:00
masterdraco 6fd917caca perf: hak-audit — spredte timere, fælles cache (markør->station, salgssteder, identify, Wait-punkter, lagre, positioner), bygningsliste 10 s, profiler, simulering (v1.28.0.0)
Audit (2 reviewere) + simulering bench/bench_hak.lua: værste frame 124,7 ms -> 0,9 ms.
- Holdende vognes genplanlægning (5 s) spredes tilfældigt over hele intervallet (før: alle i samme frame).
- ADCache: korttids-cache m. spredt udløb (+0-50 %), kun aktiv i spillet; stationsopslag 30 s.
- Bygningsliste 10 s (før 1 s: bygninger x markører hvert sekund). Markarbejde-tick forskudt 0,5 s fra menuen.
- ADProfiler: logger top-6 langsomste indgange pr. minut ved > 2 ms.
2026-09-26 11:24:05 +02:00
masterdraco 8a18187103 feat(perf): indbygget profiler — måler alle indgange (hooks, update, GUI), logger top-6 pr. minut ved > 2 ms 2026-09-26 11:19:35 +02:00
masterdraco 0fca6e8a9a perf+fix: rutetjek gemmes 30 s (hak hvert 5. s); kilde der gav lidt hviler 3 min (æg-vognen til samme hønsehus 3x) (v1.27.3.0)
In-game 26/9: hak ca. hvert 5. s efter 1.27.2 = holdende vognes genplanlægning (ADOutbound/ADLoadSwap hvert
5 s) med AD-stifinding + reserve-BFS gennem 13.550 waypoints pr. kandidat. Rutesvar caches pr. mål og 10 m-
område i 30 s (max 2000 poster); langsomme rutetjek logges. Æg: 'ingen mere last' markerer nu kilden som
hvilende 3 min, så runden roterer mellem hønsehusene.
2026-09-26 11:07:16 +02:00
masterdraco 038cb5be54 docs: kommentar — arbejdskø, ikke coroutine 2026-09-26 10:39:01 +02:00
masterdraco 05832603e4 perf(marker): markscanning som arbejdskø — højst ét prøvepunkt pr. frame (v1.27.2.0)
In-game 26/9: hakket kom sjældnere da scanningen gik fra 1 s til 2 s -> scanningen af én mark i ét frame
var årsagen (opslag gennemgår alle ~40 afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants
fjernede dem, jf. Courseplays HybridAStar), derfor en eksplicit kø. Tidsmåling bruger netGetTime.
Test (test_adFieldsScan) beviser: færdig, korrekt resultat, <= 3 opslag pr. frame.
2026-09-26 10:38:51 +02:00
masterdraco c34a472b1c perf(marker): hak hvert sekund — ingen kopier når intet ændrer sig, passive marker i tiendedele, scan hvert 2. s, cachet markliste/markører, tick-tidsmåling i loggen (v1.27.1.0)
In-game 26/9: lille hak ca. hvert sekund. Markarbejdets tick kopierede alle markers data hvert sekund
(affald -> GC-pause), hentede alle kortets marker og alle AD-markører.
2026-09-26 02:03:31 +02:00
122 changed files with 18572 additions and 1096 deletions
+97 -4
View File
@@ -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
+53
View File
@@ -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
+238
View File
@@ -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
+196 -18
View File
@@ -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)
@@ -164,8 +240,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 +250,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
@@ -291,8 +377,12 @@ function ADCpBridge.getWorkWidth(vehicle)
return best return best
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 +455,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 +495,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}
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 +538,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 +557,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,11 +576,22 @@ 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)
job:onClickGenerateFieldWorkCourse(function(generated) -- kursusberegningen kører over mange frames på store marker: moden venter længere imens
if handle.cancelled then return end local function generate(headlands)
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end ADCpBridge.setHeadlands(vehicle, headlands)
startWithCourse() handle.generating = true
end) job:onClickGenerateFieldWorkCourse(function(generated)
handle.generating = false
if handle.cancelled then return end
if not generated and headlands > 0 then
Logging.info("ADSmartPickup: kursus med %d forager-runder kunne ikke laves — prøver uden forager", headlands)
return generate(0)
end
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
startWithCourse()
end)
end
generate(ADCpBridge.HEADLANDS)
end end
handle.onBoundary = function(_, success, message) handle.onBoundary = function(_, success, message)
if handle.fired then return end if handle.fired then return end
@@ -462,6 +625,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)
+117
View File
@@ -0,0 +1,117 @@
-- 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}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste)
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 then table.insert(ids, vehicle.id) end
end
return ids
end
-- ---- spil ----
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,
}
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()
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
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
+85 -4
View File
@@ -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
+488
View File
@@ -0,0 +1,488 @@
-- AD Field Guide
-- "Opsæt mark": markguidens trin oven på ADGuide (ren Lua, ingen spil-API). Kladden er en kopi af
-- markens plan (+ faste maskiner), så intet ændres før [Gem]. Alt spil-afhængigt kommer i `context`:
-- {info, sowable = {{name, title}}, mowable = {[name] = true}, straw = {[name] = true} | nil,
-- rigs = {{id, text, role}}, rigIdsByField = {[fieldId] = ids}, fieldSilos = {{id, text}},
-- stores = {{id, text}} | nil (nil = markens vare kendes ikke), storeHidden, fields = {{id, text}}}
-- Rækker er data: {type = "choice"|"toggle"|"check"|"text"|"link", id, title/label, ...}; GUI'et tegner dem.
ADFieldGuide = {}
ADFieldGuide.ORDER = {"field", "crop", "cropChoice", "prep", "care", "machines", "store", "summary"}
ADFieldGuide.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
ADFieldGuide.STRAW_VALUES = {"chop", "collect", "bale"}
local function L(key, args) return {key = key, args = args} end
local function T(text) return {text = text} end
local YES_NO = {off = L("ui_no"), on = L("ui_yes")}
-- ---- afledte egenskaber ----
local function target(draft)
return draft.plan ~= nil and draft.plan.targetFruit or ""
end
local function isMowable(context, name)
return name ~= nil and name ~= "" and (context.mowable or {})[name] == true
end
-- Ukendt halm-tabel (tom/nil): alle høstafgrøder regnes med halm (som før guiden).
local function hasStraw(context, name)
if context.straw == nil or next(context.straw) == nil then return true end
return name ~= nil and context.straw[name] == true
end
local function info(guide) return guide.context.info or {} end
local function mowOptions(guide)
local t = target(guide.draft)
return info(guide).kind == "mowable" or isMowable(guide.context, t)
end
local function strawOptions(guide)
local t, i = target(guide.draft), info(guide)
if t ~= "" then
if isMowable(guide.context, t) then return false end
return hasStraw(guide.context, t)
end
return i.kind == "harvest" and hasStraw(guide.context, i.fruitName)
end
-- Der høstes (mejetærsker + aflæsningsvogne) nu eller senere.
local function harvestInvolved(guide)
local t = target(guide.draft)
return info(guide).kind == "harvest" or (t ~= "" and not isMowable(guide.context, t))
end
local function prepVisible(guide)
local t, i = target(guide.draft), info(guide)
return i.kind ~= "mowable" or (t ~= "" and t ~= i.fruitName)
end
local function careVisible(guide)
return info(guide).kind ~= "bare" or target(guide.draft) ~= ""
end
-- Ukrudt giver kun mening på afgrøder der ikke slås (samme regel som før).
local function weedVisible(guide)
local t = target(guide.draft)
if t ~= "" then return not isMowable(guide.context, t) end
return info(guide).kind ~= "mowable"
end
local VISIBLE = {
field = function(guide) return guide.draft.pickField == true end,
crop = function(guide) return guide.draft.fieldId ~= nil end,
cropChoice = function(guide) return guide.draft.fieldId ~= nil and (mowOptions(guide) or strawOptions(guide)) end,
prep = function(guide) return guide.draft.fieldId ~= nil and prepVisible(guide) end,
care = function(guide) return guide.draft.fieldId ~= nil and careVisible(guide) end,
machines = function(guide) return guide.draft.fieldId ~= nil end,
store = function(guide) return guide.draft.fieldId ~= nil and guide.context.stores ~= nil and info(guide).kind ~= "bare" end,
summary = function(guide) return guide.draft.fieldId ~= nil end,
}
function ADFieldGuide.isVisible(step, guide)
local rule = VISIBLE[step]
return rule ~= nil and rule(guide) == true
end
ADFieldGuide.DEF = {order = ADFieldGuide.ORDER, isVisible = ADFieldGuide.isVisible}
function ADFieldGuide.stepKey(step) return "spu_guide_field_step_" .. tostring(step) end
function ADFieldGuide.hintKey(step) return "spu_guide_field_hint_" .. tostring(step) end
-- ---- kladde ----
local function isPrepAuto(plan)
return (plan.soil or "auto") == "auto" and plan.mulch ~= true and plan.lime ~= false and plan.roll ~= true
end
-- Pleje "Automatisk" = al pleje slået til (gødning, ukrudt, kalk). Kalk tæller kun her når den ikke hører
-- under klargøring — ellers afgør klargøringens valg den.
local function isCareAuto(plan)
return plan.fertilize ~= false and plan.weed ~= false and plan.lime ~= false
end
local function storeModeOf(ids, fieldSilos)
if #ids == 0 then return "auto" end
local silo = {}
for _, entry in ipairs(fieldSilos or {}) do silo[entry.id] = true end
for _, id in ipairs(ids) do
if not silo[id] then return "manual" end
end
return "fieldSilo"
end
local function draftFrom(field, context)
local plan = ADGuide.deepCopy(field.plan or {})
plan.storeBuildingIds = plan.storeBuildingIds or {}
plan.rigIds = plan.rigIds or {}
local mode = storeModeOf(plan.storeBuildingIds, context.fieldSilos)
return {
fieldId = field.id, plan = plan, originalTarget = plan.targetFruit or "",
prepAuto = isPrepAuto(plan), careAuto = isCareAuto(plan), useDedicated = #plan.rigIds > 0,
storeMode = mode, storeIds = mode == "manual" and ADGuide.deepCopy(plan.storeBuildingIds) or {},
}
end
function ADFieldGuide.open(field, context, options)
return ADGuide.new(ADFieldGuide.DEF, draftFrom(field, context), context, options)
end
-- Guiden åbnet uden valgt mark: trin 1 "Mark".
function ADFieldGuide.openPicker(context)
return ADGuide.new(ADFieldGuide.DEF, {pickField = true}, context, {step = "field"})
end
-- Mark valgt i trin 1: kladden bygges fra markens plan; trin 1 tæller stadig med.
function ADFieldGuide.pickField(guide, field, context)
local draft = draftFrom(field, context)
draft.pickField = true
local picked = ADGuide.withContext(ADGuide.withDraft(guide, draft), context)
return (ADGuide.next(picked))
end
-- ---- valg ----
local PLAN_VALUES = {
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
}
function ADFieldGuide.choose(guide, id, value)
local draft = ADGuide.deepCopy(guide.draft)
if PLAN_VALUES[id] then
draft.plan[id] = value
elseif id == "product" then
draft.plan.product = value and "hay" or "silage"
elseif id == "prepAuto" or id == "careAuto" or id == "useDedicated" then
draft[id] = value == true
elseif id == "rigIds" then
draft.plan.rigIds = ADGuide.toggleItem(draft.plan.rigIds, value.item, value.on)
elseif id == "useFieldSilo" then
draft.storeMode = value and "fieldSilo" or "auto"
elseif id == "storeAuto" then
draft.storeMode = value and "auto" or "manual"
elseif id == "storeBuildingIds" then
draft.storeIds = ADGuide.toggleItem(draft.storeIds, value.item, value.on)
else
return guide
end
return ADGuide.withDraft(guide, draft)
end
-- ---- rækker pr. trin ----
local function numberOptions(from, to)
local options = {}
for value = from, to do table.insert(options, {label = T(tostring(value)), value = value}) end
return options
end
local function choice(id, title, options, value)
return {type = "choice", id = id, title = title, options = options, value = value}
end
local function toggle(id, title, value, off, on)
return {type = "toggle", id = id, title = title, off = off or YES_NO.off, on = on or YES_NO.on, value = value == true}
end
local function rowsField(guide)
local rows = {}
for _, entry in ipairs(guide.context.fields or {}) do
table.insert(rows, {type = "link", id = "field", value = entry.id, label = T(entry.text)})
end
if #rows == 0 then table.insert(rows, {type = "text", label = L("spu_field_scanning")}) end
return rows
end
local function rowsCrop(guide)
local plan = guide.draft.plan
local options = {{label = L("spu_guide_crop_keep"), value = ""}}
for _, fruit in ipairs(guide.context.sowable or {}) do
table.insert(options, {label = T(fruit.title), value = fruit.name})
end
local rows = {choice("targetFruit", L("spu_guide_crop"), options, plan.targetFruit or "")}
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
end
return rows
end
local function rowsCropChoice(guide)
local plan, rows = guide.draft.plan, {}
if mowOptions(guide) then
table.insert(rows, toggle("product", L("spu_field_plan"), ADFieldModel.product(plan, "mowable") == "hay",
L("spu_field_plan_silage"), L("spu_field_plan_hay")))
table.insert(rows, toggle("rake", L("spu_field_rake"), plan.rake))
end
if strawOptions(guide) then
local options = {}
for _, value in ipairs(ADFieldGuide.STRAW_VALUES) do
table.insert(options, {label = L("spu_field_straw_" .. value), value = value})
end
table.insert(rows, choice("straw", L("spu_field_straw"), options, plan.straw or "chop"))
end
return rows
end
local function rowsPrep(guide)
local draft, plan = guide.draft, guide.draft.plan
local rows = {choice("prepAuto", L("spu_guide_prep"), {
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.prepAuto)}
if draft.prepAuto then
table.insert(rows, {type = "text", label = L("spu_guide_prep_autoText")})
return rows
end
local soil = {}
for _, value in ipairs(ADFieldGuide.SOIL_VALUES) do
table.insert(soil, {label = L("spu_field_soil_" .. value), value = value})
end
table.insert(rows, choice("soil", L("spu_field_soil"), soil, plan.soil or "auto"))
table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false))
table.insert(rows, toggle("mulch", L("spu_field_mulch"), plan.mulch))
if target(draft) ~= "" or plan.sow then
table.insert(rows, toggle("roll", L("spu_field_roll"), plan.roll))
end
return rows
end
local function rowsCare(guide)
local draft, plan = guide.draft, guide.draft.plan
local rows = {choice("careAuto", L("spu_guide_care"), {
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.careAuto)}
if draft.careAuto then
table.insert(rows, {type = "text", label = L("spu_guide_care_autoText")})
return rows
end
table.insert(rows, toggle("fertilize", L("spu_field_fertilize"), plan.fertilize ~= false))
if weedVisible(guide) then table.insert(rows, toggle("weed", L("spu_field_weed"), plan.weed ~= false)) end
if not prepVisible(guide) then table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false)) end
return rows
end
-- Faste maskiner: kun ledige og dem der allerede er faste her; faste på andre marker skjules med en linje.
local function dedicatedRows(guide)
local draft, rows = guide.draft, {}
local byField = {}
for fieldId, ids in pairs(guide.context.rigIdsByField or {}) do byField[fieldId] = ids end
byField[draft.fieldId] = draft.plan.rigIds
local own = {}
for _, id in ipairs(draft.plan.rigIds) do own[id] = true end
local visible, hidden, fields = ADFieldFleet.visibleForField(guide.context.rigs or {}, draft.fieldId, byField)
local sorted = {}
for _, rig in ipairs(visible) do table.insert(sorted, rig) end
table.sort(sorted, function(a, b)
if (own[a.id] == true) ~= (own[b.id] == true) then return own[a.id] == true end
if (a.role or "") ~= (b.role or "") then return (a.role or "") < (b.role or "") end
return ADFlowView.naturalLess(tostring(a.text), tostring(b.text))
end)
for _, rig in ipairs(sorted) do
table.insert(rows, {type = "check", id = "rigIds", item = rig.id, title = T(rig.text), value = own[rig.id] == true})
end
if #sorted == 0 then table.insert(rows, {type = "text", label = L("spu_field_noRigs")}) end
if hidden > 0 then
local names = {}
for _, fieldId in ipairs(fields) do table.insert(names, "F" .. tostring(fieldId)) end
table.insert(rows, {type = "text", label = L("spu_field_hiddenDedicated", {hidden, table.concat(names, ", ")})})
end
return rows
end
local function rowsMachines(guide)
local draft, plan = guide.draft, guide.draft.plan
local rows = {choice("rigs", L("spu_guide_rigs"), numberOptions(1, ADFieldModel.MAX_RIGS), plan.rigs or 1)}
if harvestInvolved(guide) then
table.insert(rows, choice("unloaders", L("spu_field_unloaders"), numberOptions(1, ADFieldModel.MAX_UNLOADERS), plan.unloaders or 1))
end
table.insert(rows, toggle("useDedicated", L("spu_guide_dedicated"), draft.useDedicated))
if draft.useDedicated then
for _, row in ipairs(dedicatedRows(guide)) do table.insert(rows, row) end
end
return rows
end
local function rowsStore(guide)
local draft, context, rows = guide.draft, guide.context, {}
local silos = context.fieldSilos or {}
if #silos > 0 then
local names = {}
for _, silo in ipairs(silos) do table.insert(names, silo.text) end
table.insert(rows, toggle("useFieldSilo", L("spu_guide_useFieldSilo"), draft.storeMode == "fieldSilo"))
table.insert(rows, {type = "text", label = T(" " .. table.concat(names, ", "))})
if draft.storeMode == "fieldSilo" then return rows end
end
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
{label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeMode ~= "manual"))
if draft.storeMode ~= "manual" then
table.insert(rows, {type = "text", label = L("spu_guide_store_autoText")})
return rows
end
local chosen = {}
for _, id in ipairs(draft.storeIds) do chosen[id] = true end
for _, store in ipairs(context.stores or {}) do
table.insert(rows, {type = "check", id = "storeBuildingIds", item = store.id, title = T(store.text), value = chosen[store.id] == true})
end
if #(context.stores or {}) == 0 then table.insert(rows, {type = "text", label = L("spu_field_noStore")}) end
if (context.storeHidden or 0) > 0 then
table.insert(rows, {type = "text", label = L("spu_field_storeHidden", {context.storeHidden})})
end
return rows
end
local function rowsSummary(guide)
local rows = {}
for _, line in ipairs(ADFieldGuide.summary(guide)) do
table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
end
return rows
end
local ROWS = {
field = rowsField, crop = rowsCrop, cropChoice = rowsCropChoice, prep = rowsPrep, care = rowsCare,
machines = rowsMachines, store = rowsStore, summary = rowsSummary,
}
function ADFieldGuide.rows(guide)
local builder = ROWS[guide.step]
return builder ~= nil and builder(guide) or {}
end
-- ---- resumé ----
local function fruitTitle(context, name)
for _, fruit in ipairs(context.sowable or {}) do
if fruit.name == name then return fruit.title end
end
return tostring(name)
end
local function storeText(guide)
local draft, context = guide.draft, guide.context
if draft.storeMode == "fieldSilo" then return {L("spu_guide_sum_fieldSilo")} end
if draft.storeMode ~= "manual" or #draft.storeIds == 0 then return {L("spu_guide_sum_auto")} end
local names = {}
for _, id in ipairs(draft.storeIds) do
for _, store in ipairs(context.stores or {}) do
if store.id == id then table.insert(names, store.text) end
end
end
if #names == 0 or #names > 2 or #names ~= #draft.storeIds then
return {L("spu_guide_sum_stores", {#draft.storeIds})}
end
return {T(table.concat(names, " + "))}
end
local SUMMARY = {
crop = function(guide)
local plan, t = guide.draft.plan, target(guide.draft)
if t ~= "" then return {L("spu_guide_sum_target", {fruitTitle(guide.context, t)})} end
local parts = {L("spu_guide_crop_keep")}
if plan.sow and info(guide).kind ~= "mowable" then table.insert(parts, L("spu_guide_sum_sowAgain")) end
return parts
end,
cropChoice = function(guide)
local plan, parts = guide.draft.plan, {}
if mowOptions(guide) then
table.insert(parts, L(ADFieldModel.product(plan, "mowable") == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage"))
table.insert(parts, L(plan.rake and "spu_guide_sum_rake" or "spu_guide_sum_noRake"))
end
if strawOptions(guide) then table.insert(parts, L("spu_guide_sum_straw_" .. (plan.straw or "chop"))) end
return parts
end,
prep = function(guide)
local plan = guide.draft.plan
if guide.draft.prepAuto then return {L("spu_guide_sum_auto")} end
local parts = {L("spu_field_soil_" .. (plan.soil or "auto"))}
if plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
if plan.mulch then table.insert(parts, L("spu_guide_sum_mulch")) end
if plan.roll and (plan.sow or target(guide.draft) ~= "") then table.insert(parts, L("spu_guide_sum_roll")) end
return parts
end,
care = function(guide)
if guide.draft.careAuto then return {L("spu_guide_sum_auto")} end
local plan, parts = guide.draft.plan, {}
if plan.fertilize ~= false then table.insert(parts, L("spu_guide_sum_fert")) end
if weedVisible(guide) and plan.weed ~= false then table.insert(parts, L("spu_guide_sum_weed")) end
if not prepVisible(guide) and plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
if #parts == 0 then parts = {L("spu_guide_sum_noCare")} end
return parts
end,
machines = function(guide)
local draft, plan = guide.draft, guide.draft.plan
local parts = {}
if draft.useDedicated and #plan.rigIds > 0 then
table.insert(parts, L("spu_guide_sum_dedicated", {#plan.rigIds}))
else
table.insert(parts, L("spu_guide_sum_freeRigs"))
end
table.insert(parts, L("spu_guide_sum_rigs", {plan.rigs or 1}))
if harvestInvolved(guide) then table.insert(parts, L("spu_guide_sum_unloaders", {plan.unloaders or 1})) end
return parts
end,
store = storeText,
}
-- Resumé-linjer {step, title, parts} for de synlige trin (ikke "Mark" og "Oversigt").
function ADFieldGuide.summary(guide)
local lines = {}
for _, step in ipairs(ADGuide.visibleSteps(guide)) do
local build = SUMMARY[step]
if build ~= nil then
table.insert(lines, {step = step, title = L(ADFieldGuide.stepKey(step)), parts = build(guide)})
end
end
return lines
end
-- ---- gem ----
-- start: true = [Kør automatisk], false = [Gem uden at starte], nil = [Ret] (tilstanden bevares).
-- Mål-afgrøde kører altid automatisk (ADFieldModel.setTarget).
function ADFieldGuide.result(guide, start)
local draft = guide.draft
local plan = ADGuide.deepCopy(draft.plan)
local t = plan.targetFruit or ""
if t ~= "" then
plan.mode = "auto"
elseif start ~= nil then
plan.mode = start and "auto" or "manual"
end
if draft.prepAuto and ADFieldGuide.isVisible("prep", guide) then
plan.soil, plan.mulch, plan.lime, plan.roll = "auto", false, true, false
end
if draft.careAuto and ADFieldGuide.isVisible("care", guide) then
plan.fertilize, plan.weed = true, true
if not prepVisible(guide) then plan.lime = true end
end
if ADFieldGuide.isVisible("store", guide) then
if draft.storeMode == "fieldSilo" then
local ids = {}
for _, silo in ipairs(guide.context.fieldSilos or {}) do table.insert(ids, silo.id) end
plan.storeBuildingIds = ids
elseif draft.storeMode == "manual" then
plan.storeBuildingIds = ADGuide.deepCopy(draft.storeIds)
else
plan.storeBuildingIds = {}
end
end
local rigIds = draft.useDedicated and ADGuide.deepCopy(plan.rigIds) or {}
plan.rigIds = nil
return {fieldId = draft.fieldId, targetFruit = t, targetChanged = t ~= (draft.originalTarget or ""), plan = plan, rigIds = rigIds}
end
-- Faste maskiner for alle marker efter gem: marken får rigIds, og de fjernes fra andre marker.
function ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds)
local mine = {}
for _, id in ipairs(rigIds or {}) do mine[id] = true end
local result = {}
for otherId, ids in pairs(rigIdsByField or {}) do
if otherId ~= fieldId then
local copy = {}
for _, id in ipairs(ids or {}) do if not mine[id] then table.insert(copy, id) end end
result[otherId] = copy
end
end
result[fieldId] = ADGuide.deepCopy(rigIds or {})
return result
end
+268 -21
View File
@@ -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
@@ -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
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end -- 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 end
local env = ADFieldJobs.environment()
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
local stepped = 0
local infos = ADFields.list()
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
for index, info in ipairs(infos) do
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
if (not paused or resumed[info.id]) and (not ADFieldModel.isPassive(ADFieldJobs.getField(info.id)) or index % ADFieldJobs.PASSIVE_SLICES == slice) then
stepped = stepped + 1
local ok, err = pcall(stepField, adEnv, info, env)
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
end
end
local t3 = clockMs()
if t0 ~= nil and t3 ~= nil then reportSlow(t3 - t0, t1 - t0, t2 - t1, t3 - t2, stepped) end
end end
+38 -6
View File
@@ -61,6 +61,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 +89,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
@@ -157,6 +161,7 @@ end
function ADFieldModel.setTarget(field, fruitName) function ADFieldModel.setTarget(field, fruitName)
local copy = copyField(field) local copy = copyField(field)
copy.plan.targetFruit = fruitName or "" copy.plan.targetFruit = fruitName 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 +169,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,16 +230,27 @@ 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
return copy
end end
return copy return field
end
-- Passiv mark: manuel, uden mål, venter/færdig, ingen Start nu — der sker intet uden brugeren.
function ADFieldModel.isPassive(field)
local plan = field.plan
return plan.mode ~= "auto" and (plan.targetFruit == nil or plan.targetFruit == "") and not field.startRequested
and (field.stage == "idle" or field.stage == "done")
end end
function ADFieldModel.shouldStart(field) function ADFieldModel.shouldStart(field)
@@ -263,6 +281,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 +325,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 +370,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,6 +458,7 @@ 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),
@@ -459,6 +490,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)
+94
View File
@@ -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
+195
View File
@@ -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
+720
View File
@@ -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
+42 -1
View File
@@ -13,6 +13,12 @@ ADFieldStorage.FIELD_TYPES = {
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",
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 +42,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 +68,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 +105,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
+72
View File
@@ -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
+689 -36
View File
@@ -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
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 return
end 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,10 +525,35 @@ 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,
} }
ADFieldJobs.state.assignments[rig.id] = assignment if role == "harvester" and rig.manual and not assignment.helper then
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil) -- for bred til vejene: spilleren kører den selv til marken og monterer skærebordet
log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0, assignment.phase, assignment.manual = "awaitingPlayer", true
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "") assignment.startX, assignment.startZ = rig.x, rig.z
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)
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,
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)
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
if wayPoint == nil then return end
local distance = (wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2
if distance < bestDistance and ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId) then
best, bestDistance = wayPointId, distance
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 for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {} local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {}
if g_currentMission.accessHandler:canFarmAccess(farmId, station) and supported[fillType] 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 and (station:getFillLevel(fillType, farmId) or 0) > 0 then
local wayPointId = adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 10) consider(markerAtStation(adEnv, vehicle, station, station.owningPlaceable))
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
if wayPoint ~= nil then
local distance = (wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2
if distance < bestDistance and ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId) then
best, bestDistance = wayPointId, distance
end
end
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
+420 -125
View File
@@ -11,6 +11,11 @@ ADFields.cache = {}
ADFields.order = {} ADFields.order = {}
ADFields.scanIndex = 0 ADFields.scanIndex = 0
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 +50,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 +60,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 +102,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}
-- 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 +165,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 +175,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 +189,141 @@ 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
cx, cz = 0, 0
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
cx, cz = cx / #verts, cz / #verts
end
if cx == nil then return {}, verts end
local candidates = {{x = cx, z = cz}}
local step = math.max(1, math.floor(#verts / 8))
for index = 1, #verts, step do
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
end
local points = {}
for _, p in ipairs(candidates) do
local ok, onField = pcall(isOnField, p.x, p.z)
if ok and onField then table.insert(points, p) end
end
return points, verts
end end
local function fruitAt(points) -- Markens form-fingeraftryk: antal hjørner, areal (shoelace, nærmeste 10 m²) og tyngdepunkt (nærmeste meter).
local votes, best, bestVotes = {}, nil, 0 -- Samme mark giver samme streng trods små float-forskelle; en ændret mark (hjørne/kant) giver en ny.
for _, p in ipairs(points) do -- Bruges til at genbruge Courseplay-kurser (ADCourseCache). nil uden en flade (under 3 hjørner).
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, p.x, p.z) ADFields.SHAPE_AREA_STEP = 10
if ok and type(index) == "number" and index > 0 then local function roundTo(value, step)
votes[index] = (votes[index] or 0) + 1 return math.floor(value / step + 0.5) * step
if votes[index] > bestVotes then best, bestVotes = index, votes[index] end end
end function ADFields.shapeSignature(verts)
if type(verts) ~= "table" or #verts < 3 then return nil end
local n = #verts
local twiceArea, sx, sz = 0, 0, 0
for index = 1, n do
local a, b = verts[index], verts[index % n + 1]
local cross = a.x * b.z - b.x * a.z
twiceArea = twiceArea + cross
sx, sz = sx + (a.x + b.x) * cross, sz + (a.z + b.z) * cross
end
local cx, cz
if math.abs(twiceArea) > 1e-6 then
cx, cz = sx / (3 * twiceArea), sz / (3 * twiceArea)
else
cx, cz = ADFields.polygonCenter(verts)
end
return string.format("n=%d;a=%d;c=%d,%d", n, roundTo(math.abs(twiceArea) / 2, ADFields.SHAPE_AREA_STEP),
roundTo(cx, 1), roundTo(cz, 1))
end
-- Hjørnernes gennemsnit (nil uden hjørner).
function ADFields.polygonCenter(verts)
if verts == nil or #verts == 0 then return nil end
local cx, cz = 0, 0
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
return cx / #verts, cz / #verts
end
-- Afstand fra (x, z) til omridsets nærmeste kant; nil uden omrids.
function ADFields.distanceToPolygon(verts, x, z)
if verts == nil or #verts == 0 then return nil end
local best = math.huge
for index = 1, #verts do
local a, b = verts[index], verts[index % #verts + 1]
local dx, dz = b.x - a.x, b.z - a.z
local len2 = dx * dx + dz * dz
local t = len2 > 0 and math.max(0, math.min(1, ((x - a.x) * dx + (z - a.z) * dz) / len2)) or 0
local d = math.sqrt((a.x + dx * t - x) ^ 2 + (a.z + dz * t - z) ^ 2)
if d < best then best = d end
end 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 +334,302 @@ 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 end
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
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 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
local id = field.getId ~= nil and field:getId() or field.fieldId table.insert(all, field)
if id ~= nil and fieldOwner(field) == farmId then
table.insert(result, {id = id, field = field})
end
end end
table.sort(result, function(a, b) return a.id < b.id end) local scan = {owned = true, queue = {}, head = 1, tail = 0, result = {}}
return result for start = 1, #all, 25 do
push(scan, function()
for index = start, math.min(start + 24, #all) do
local field = all[index]
local id = field.getId ~= nil and field:getId() or field.fieldId
if id ~= nil and fieldOwner(field) == farmId then
table.insert(scan.result, {id = id, field = field})
end
end
end)
end
return scan
end 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.listCache = nil
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
local entry = owned[ADFields.scanIndex]
local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
if ok then
ADFields.cache[entry.id] = info
else
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
end
end end
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
ADFields.SCAN_INTERVAL_MS = 2000
ADFields.MAX_TASKS_PER_FRAME = 8
ADFields.FRAME_BUDGET_MS = 0.5
function ADFields.clock()
if netGetTime == nil then return nil end
local ok, ms = pcall(netGetTime)
return ok and type(ms) == "number" and ms or nil
end
ADFields.lastScanStart = nil
function ADFields.scanStep(adEnv, farmId, now)
ADFields.now = now or ADFields.now
local scan = ADFields.activeScan
if scan == nil then
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
ADFields.ownedAt = ADFields.now
scan = newOwnedScan(farmId)
else
if ADFields.lastScanStart ~= nil and ADFields.now - ADFields.lastScanStart < ADFields.SCAN_INTERVAL_MS then return end
local owned = ADFields.order
if #owned == 0 then return end
ADFields.lastScanStart = ADFields.now
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
scan = newScan(adEnv, owned[ADFields.scanIndex])
end
ADFields.activeScan = scan
end
local task = scan.queue[scan.head]
if task ~= nil then
-- flere små opgaver pr. frame inden for et tidsbudget (efter genstart skal alle marker scannes;
-- én pr. frame gjorde listen fuld først efter flere minutter). Uden ur (tests): én pr. frame.
local started = ADFields.clock()
for _ = 1, ADFields.MAX_TASKS_PER_FRAME do
task = scan.queue[scan.head]
if task == nil then return end
scan.queue[scan.head] = nil
scan.head = scan.head + 1
local ok, err = pcall(task)
if not ok then
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
ADFields.activeScan = nil
return
end
local current = started ~= nil and ADFields.clock() or nil
if current == nil or current - started >= ADFields.FRAME_BUDGET_MS then return end
end
return
end
-- køen er tom: saml resultatet
ADFields.activeScan = nil
local ok, err
if scan.owned then
ok, err = pcall(finishOwnedScan, scan)
else
ok, err = pcall(finishScan, adEnv, scan)
end
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
end
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
function ADFields.list() 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
+91
View File
@@ -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
+556
View File
@@ -0,0 +1,556 @@
-- AD Flow Guide
-- "Opsæt flow": flowguidens trin oven på ADGuide (ren Lua, ingen spil-API) — samme trin-skærm og knapper
-- som markguiden. Kladden er en kopi af flowets felter, så intet ændres før [Gem]/[Start flow]. Det
-- spil-afhængige kommer i `context` (GUI'et henter det ud fra kladden, se previewRun):
-- {buildings = {{id, name, label, hasMarker, text}} -- kandidater til Fra (udkørsel) / Til (forsyning)
-- fillTypes = {{name, title}} -- varer målet tager (forsyning, Vælg selv)
-- products = {{name, title}} -- varer kilderne har (udkørsel)
-- storeBuildings = {{id, name, text}} -- lagre der kan tage flowets lagervarer
-- waitPools = {{group, count}}
-- vehicles = {{id, label, text, otherRunId, otherRunName, blocked, solo}} -- label = kort navn (resumé)
-- wagons = {{id, label}}} -- ledige vogne til solo-traktorer (bedste først)
-- Solo-traktor (ingen vogn bagpå) får en ledig vogn fra context.wagons (draft.wagons = {[traktorId] = vognId}),
-- som den henter ved "Connect <vognens parkering>" — uden at vognens parkering skal omdøbes.
-- Gem: ADRunsController.createRun/updateRun + assignVehicle/unassignVehicle (GUI'et).
ADFlowGuide = {}
ADFlowGuide.ORDER = {"name", "direction", "buildings", "goods", "store", "rules", "wait", "tractors", "summary"}
ADFlowGuide.POLICIES = {"off", "store", "sell", "sellElseStore"}
ADFlowGuide.MAX_NAMED_TRACTORS = 3
ADFlowGuide.MAX_NAMED_GOODS = 3
local function L(key, args) return {key = key, args = args} end
local function T(text) return {text = text} end
local function isOut(guide) return guide.draft.kind == "outbound" end
local function hasStorePolicy(draft)
for _, product in ipairs(draft.products or {}) do
if product.policy == "store" or product.policy == "sellElseStore" then return true end
end
return false
end
local VISIBLE = {
name = function(guide) return guide.draft.isNew == true end,
store = function(guide) return isOut(guide) and hasStorePolicy(guide.draft) end,
}
function ADFlowGuide.isVisible(step, guide)
local rule = VISIBLE[step]
if rule ~= nil then return rule(guide) == true end
for _, id in ipairs(ADFlowGuide.ORDER) do
if id == step then return true end
end
return false
end
ADFlowGuide.DEF = {order = ADFlowGuide.ORDER, isVisible = ADFlowGuide.isVisible}
-- Overskrift pr. trin; "Fra"/"Til" følger retningen.
function ADFlowGuide.stepKey(step, guide)
if step == "buildings" then
return (guide ~= nil and isOut(guide)) and "spu_guide_flow_step_from" or "spu_guide_flow_step_to"
end
return "spu_guide_flow_step_" .. tostring(step)
end
local KIND_HINTS = {buildings = true, goods = true, rules = true}
function ADFlowGuide.hintKey(step, guide)
if KIND_HINTS[step] then
return "spu_guide_flow_hint_" .. step .. ((guide ~= nil and isOut(guide)) and "_out" or "_in")
end
return "spu_guide_flow_hint_" .. tostring(step)
end
-- ---- kladde ----
local RUN_FIELDS = {
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
}
local function draftFrom(run, isNew)
local draft = {}
for _, key in ipairs(RUN_FIELDS) do draft[key] = ADGuide.deepCopy(run[key]) end
for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products", "ingredients", "vehicleIds"}) do
draft[key] = draft[key] or {}
end
draft.kind = draft.kind == "outbound" and "outbound" or "supply"
draft.ingredientMode = draft.ingredientMode == "manual" and "manual" or "auto"
draft.sellAtPercent = draft.sellAtPercent or ADRuns.DEFAULT_SELL_AT_PERCENT
draft.storeAbovePercent = draft.storeAbovePercent or ADRuns.DEFAULT_STORE_ABOVE_PERCENT
draft.sellAbovePercent = draft.sellAbovePercent or ADRuns.DEFAULT_SELL_ABOVE_PERCENT
draft.returnBelowPercent = draft.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT
draft.loops = draft.loops or 0
draft.waitPoolGroup = draft.waitPoolGroup or ""
draft.wagons = draft.wagons or {}
draft.collectRound = draft.collectRound == true
draft.isNew = isNew == true
draft.showAllBuildings = false
draft.storeAuto = #draft.storeBuildingIds == 0
return draft
end
function ADFlowGuide.open(run, context, options)
return ADGuide.new(ADFlowGuide.DEF, draftFrom(run, false), context, options)
end
-- Nyt flow: intet oprettes før [Gem]; kladden har standardværdierne fra ADRuns.createRun.
function ADFlowGuide.openNew(name, context)
local _, run = ADRuns.createRun(ADRuns.newStore(), name, "supply")
local draft = draftFrom(run, true)
draft.id = nil
return ADGuide.new(ADFlowGuide.DEF, draft, context)
end
-- Run-formet tabel til controllerens opslag (varer, vogne) før noget er gemt: markører fra de valgte
-- bygninger. markerIds(buildingId, role) -> {wayPointId, ...}.
function ADFlowGuide.previewRun(draft, markerIds)
local run = ADGuide.deepCopy(draft)
local function resolve(ids, role)
local result, seen = {}, {}
for _, buildingId in ipairs(ids or {}) do
for _, wayPointId in ipairs(markerIds(buildingId, role) or {}) do
if not seen[wayPointId] then
seen[wayPointId] = true
table.insert(result, wayPointId)
end
end
end
return result
end
run.sourceWayPointIds = resolve(draft.sourceBuildingIds, "load")
run.targetWayPointIds = resolve(draft.targetBuildingIds, "unload")
run.targetWayPointId = run.targetWayPointIds[1]
return run
end
-- ---- vogne til solo-traktorer ----
local function soloIds(context)
local set = {}
for _, vehicle in ipairs((context or {}).vehicles or {}) do
if vehicle.solo then set[vehicle.id] = true end
end
return set
end
-- Ny vogn-tabel: fravalgte traktorer slipper deres vogn; valgte solo-traktorer uden (gyldig) vogn får den første
-- ledige i context.wagons, som ingen anden traktor i flowet har. Vogne til ikke-solo-traktorer bevares (husket).
function ADFlowGuide.assignWagons(draft, context)
local chosen, solo, offered = {}, soloIds(context), {}
for _, id in ipairs(draft.vehicleIds or {}) do chosen[id] = true end
for _, wagon in ipairs((context or {}).wagons or {}) do offered[wagon.id] = true end
local wagons, used = {}, {}
for tractorId, wagonId in pairs(draft.wagons or {}) do
local valid = chosen[tractorId] and (not solo[tractorId] or offered[wagonId])
if valid and not used[wagonId] then
wagons[tractorId] = wagonId
used[wagonId] = true
end
end
for _, tractorId in ipairs(draft.vehicleIds or {}) do
if solo[tractorId] and wagons[tractorId] == nil then
for _, wagon in ipairs((context or {}).wagons or {}) do
if not used[wagon.id] then
wagons[tractorId] = wagon.id
used[wagon.id] = true
break
end
end
end
end
return wagons
end
-- ---- valg ----
local SET_VALUES = {
kind = true, ingredientMode = true, sellAtPercent = true, storeAbovePercent = true, sellAbovePercent = true, loops = true,
returnBelowPercent = true, waitPoolGroup = true,
}
local BOOL_VALUES = {showAllBuildings = true, storeAuto = true, collectRound = true}
local LIST_VALUES = {ingredients = true, storeBuildingIds = true, vehicleIds = true}
local function setPolicy(products, name, policy)
local result = {}
for _, product in ipairs(products or {}) do
if product.fillType ~= name then table.insert(result, {fillType = product.fillType, policy = product.policy}) end
end
if policy ~= "off" then table.insert(result, {fillType = name, policy = policy}) end
return result
end
function ADFlowGuide.choose(guide, id, value)
if id == "showBlocked" then
-- visning, ikke en del af flowet: fold listen over traktorer der ikke kan bruges ud/ind
local copy = {}
for key, field in pairs(guide) do copy[key] = field end
copy.showBlocked = not guide.showBlocked
return copy
end
local draft = ADGuide.deepCopy(guide.draft)
if SET_VALUES[id] then
draft[id] = value
elseif BOOL_VALUES[id] then
draft[id] = value == true
elseif LIST_VALUES[id] then
draft[id] = ADGuide.toggleItem(draft[id], value.item, value.on)
if id == "vehicleIds" then draft.wagons = ADFlowGuide.assignWagons(draft, guide.context) end
elseif id == "wagon" then
draft.wagons = ADGuide.deepCopy(draft.wagons or {})
draft.wagons[value.item] = value.value
elseif id == "buildings" then
local key = draft.kind == "outbound" and "sourceBuildingIds" or "targetBuildingIds"
draft[key] = ADGuide.toggleItem(draft[key], value.item, value.on)
elseif id == "policy" then
draft.products = setPolicy(draft.products, value.item, value.value)
elseif id == "name" then
if type(value) ~= "string" or not value:match("%S") then return guide end
draft.name = value
else
return guide
end
return ADGuide.withDraft(guide, draft)
end
-- ---- rækker ----
local function choice(id, title, options, value, item)
return {type = "choice", id = id, title = title, options = options, value = value, item = item}
end
local function toggle(id, title, value)
return {type = "toggle", id = id, title = title, off = L("ui_no"), on = L("ui_yes"), value = value == true}
end
local function check(id, item, text, value)
return {type = "check", id = id, item = item, title = T(text), value = value == true}
end
local function text(key, args) return {type = "text", label = L(key, args)} end
local function percentOptions(from, to, step)
local options = {}
for percent = from, to, step do table.insert(options, {label = T(string.format("%d %%", percent)), value = percent}) end
return options
end
-- "Sælg altid over": Fra, 50-100 %
local function sellAboveOptions()
local options = {{label = L("spu_sellAbove_off"), value = 0}}
for _, option in ipairs(percentOptions(50, ADRuns.MAX_SELL_ABOVE_PERCENT, 5)) do table.insert(options, option) end
return options
end
local function chosenSet(list)
local set = {}
for _, value in ipairs(list or {}) do set[value] = true end
return set
end
local function buildingIds(draft)
return draft.kind == "outbound" and draft.sourceBuildingIds or draft.targetBuildingIds
end
local function rowsName(guide)
return {
{type = "text", label = T(guide.draft.name or "")},
{type = "link", id = "rename", value = true, label = L("spu_guide_flow_rename")},
}
end
local function rowsDirection(guide)
return {choice("kind", L("spu_direction"), {
{label = L("spu_direction_in"), value = "supply"}, {label = L("spu_direction_out"), value = "outbound"}}, guide.draft.kind)}
end
-- Bygninger i fast rækkefølge (navn, markør); uden markør kun med "Vis alle" (eller når valgt).
local function rowsBuildings(guide)
local draft, rows = guide.draft, {}
local chosen = chosenSet(buildingIds(draft))
local candidates = guide.context.buildings or {}
local shown = ADFlowView.buildingRows(candidates, chosen, draft.showAllBuildings)
local hidden = ADFlowView.hiddenCount(candidates, shown)
if hidden > 0 or draft.showAllBuildings then
table.insert(rows, toggle("showAllBuildings", L("spu_showAllBuildings", {hidden}), draft.showAllBuildings))
end
for _, building in ipairs(shown) do
table.insert(rows, check("buildings", building.id, building.text or building.name, chosen[building.id]))
end
if #shown == 0 then table.insert(rows, text("spu_noBuildings")) end
return rows
end
local function rowsGoods(guide)
local draft, context, rows = guide.draft, guide.context, {}
if isOut(guide) then
local products = context.products or {}
if #products == 0 then return {text("spu_guide_flow_noProducts")} end
local options = {}
for _, policy in ipairs(ADFlowGuide.POLICIES) do table.insert(options, {label = L("spu_policy_" .. policy), value = policy}) end
for _, product in ipairs(products) do
table.insert(rows, choice("policy", T(product.text or product.title), options, ADRuns.getProductPolicy(draft, product.name), product.name))
end
return rows
end
table.insert(rows, choice("ingredientMode", L("spu_guide_flow_goods"), {
{label = L("spu_guide_flow_goodsAutoChoice"), value = "auto"}, {label = L("spu_guide_manual"), value = "manual"}}, draft.ingredientMode))
if draft.ingredientMode ~= "manual" then
table.insert(rows, text("spu_guide_flow_goodsAutoText"))
return rows
end
local chosen = chosenSet(draft.ingredients)
for _, fillType in ipairs(context.fillTypes or {}) do
table.insert(rows, check("ingredients", fillType.name, " " .. tostring(fillType.title), chosen[fillType.name]))
end
if #(context.fillTypes or {}) == 0 then table.insert(rows, text("spu_guide_flow_noGoodsTo")) end
return rows
end
local function rowsStore(guide)
local draft, rows = guide.draft, {}
table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
{label = L("spu_guide_flow_storeAutoChoice"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeAuto))
if draft.storeAuto then
table.insert(rows, text("spu_guide_flow_storeAutoText"))
return rows
end
local chosen = chosenSet(draft.storeBuildingIds)
for _, building in ipairs(guide.context.storeBuildings or {}) do
table.insert(rows, check("storeBuildingIds", building.id, building.text, chosen[building.id]))
end
if #(guide.context.storeBuildings or {}) == 0 then table.insert(rows, text("spu_noBuildings")) end
return rows
end
local function rowsRules(guide)
local draft = guide.draft
if isOut(guide) then
return {
choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent),
choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent),
choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent),
toggle("collectRound", L("spu_collectRound"), draft.collectRound),
}
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 parts = {L("spu_guide_flow_sumSellAt", {draft.sellAtPercent}), L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})}
if (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
+53 -6
View File
@@ -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,41 @@ 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"
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
+741
View File
@@ -0,0 +1,741 @@
-- 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
-- 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 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 ADRunsController.getVehicleId(rear[1].object) or nil
if wagonId == nil or tractorId == nil then return end
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
if updated ~= run.wagons then
run.wagons = updated
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name),
tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object)))
end
end
-- Én traktor hjem (fra et stoppet/slettet flow): tøm først, så parkering.
-- reason: "stop" (et kørende flow stoppes: vognen huskes) | "afterLoad" (oprydning: intet huskes)
function ADFlowWagons.homeVehicle(adEnv, vehicle, runName, reason)
waiting[vehicle] = nil
if ADFlowWagons.shouldRememberOnHome(reason or "stop") then
rememberWagonOf(vehicle) -- vognen sættes af nu: flowet skal kunne finde den igen ved næste start
end
ADHitch.stow(vehicle, "flowet stoppet")
if fetching[vehicle] ~= nil then
fetching[vehicle] = nil
ADHitch.cancel(vehicle)
end
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
local second = module ~= nil and module.getSecondMarker ~= nil and module:getSecondMarker() or nil
local run = ADRunsController.getRunForVehicle(vehicle)
local targetId = ADFlowWagons.emptyTarget(second ~= nil and second.id or nil, run ~= nil and run.targetWayPointIds or nil)
local target = targetId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
local cargo, fillType = cargoOf(adEnv, vehicle)
local action = ADFlowWagons.emptyStep("start", {cargo = cargo, hasTarget = target ~= nil})
if action == "home" then return goHome(adEnv, vehicle) end
local state = {stage = action, since = nowMs, runName = runName,
markerId = target ~= nil and target.id or nil, markerName = target ~= nil and target.name or nil}
emptying[vehicle] = state
if action == "target" then
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, target.id, nil, fillType)
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' afleverer sit læs (%d l %s) ved %s før parkering",
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(target.name))
else
startDump(adEnv, vehicle, state)
end
end
-- ADHitch: spilleren har selv kørt traktoren til Connect og koblet på.
function ADFlowWagons.onManualAttach(adEnv, vehicle, wagon)
local run = ADRunsController.getRunForVehicle(vehicle)
orphans[vehicle] = 0 -- ryddes af tickOrphans når nogen ejer opgaven (flow, markopgave, AD, spilleren)
if run == nil then return end
local tractorId = ADRunsController.getVehicleId(vehicle)
local wagonId = wagon ~= nil and ADRunsController.getVehicleId(wagon) or nil
local remembered = (run.wagons or {})[tractorId]
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
-- ikke stoppet af spilleren = gælder også et flow hvis Start blev afvist ('ingen vogn') — det er derfor han henter vognen
if not ADFlowWagons.startAfterManualAttach({flowRunning = not run.stopped,
ownWagon = wagonId ~= nil and (remembered == nil or remembered == wagonId),
adActive = module ~= nil and module:isActive()}) then return end
fetchRetries[vehicle], waiting[vehicle], orphans[vehicle] = nil, nil, nil
Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har selv hentet sin vogn — startes", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)))
ADRunsController.startVehicle(adEnv, run, vehicle)
end
-- Traktorer der har koblet et redskab på uden ejer: efter ORPHAN_MS sættes redskabet tilbage og traktoren kører hjem.
local function tickOrphans(adEnv, elapsed)
for vehicle, idle in pairs(orphans) do
local id = ADRunsController.getVehicleId(vehicle)
local run = ADRunsController.getRunForVehicle(vehicle)
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
local owned = (module ~= nil and module:isActive()) or (vehicle.getIsControlled ~= nil and vehicle:getIsControlled())
or (run ~= nil and not run.stopped) or (ADFieldJobs ~= nil and id ~= nil and ADFieldJobs.state.assignments[id] ~= nil)
or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle)) or #ADHitch.rearImplementsOf(vehicle) == 0
local atOwnPark = false
local parkId = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
local parkWp = parkId ~= nil and adEnv.ADGraphManager:getWayPointById(parkId) or nil
if parkWp ~= nil and vehicle.rootNode ~= nil then
local x, _, z = getWorldTranslation(vehicle.rootNode)
atOwnPark = (x - parkWp.x) ^ 2 + (z - parkWp.z) ^ 2 <= ADFlowWagons.OWN_PARK_M ^ 2
end
local action = ADFlowWagons.orphanAction({owned = owned, atOwnPark = atOwnPark, idleMs = idle + elapsed})
if action == "none" then
orphans[vehicle] = nil
elseif action == "home" then
orphans[vehicle] = nil
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' har et redskab på uden opgave (hentet før genindlæsning?) — sættes tilbage og kører hjem",
tostring(ADRuns.vehicleLabel(vehicle)))
goHome(adEnv, vehicle)
else
orphans[vehicle] = idle + elapsed
end
end
end
function ADFlowWagons.isEmptying(vehicle)
return emptying[vehicle] ~= nil
end
function ADFlowWagons.sendHome(adEnv, run)
for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
if vehicle ~= nil then
local ok, err = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name)
if not ok then
Logging.warning(ADFlowWagons.LOG_PREFIX .. "hjem fejlede: %s", tostring(err))
goHome(adEnv, vehicle)
end
end
end
end
-- Står traktoren ved tømningens markør, uden at læsset er faldet i EMPTY_STALL_MS?
local function stalledAt(adEnv, vehicle, state, cargo)
if state.lastCargo == nil or cargo < state.lastCargo - 1 then
state.lastCargo, state.changedAt = cargo, nowMs
end
local wayPoint = state.markerId ~= nil and adEnv.ADGraphManager:getWayPointById(state.markerId) or nil
local near = false
if wayPoint ~= nil and vehicle.rootNode ~= nil then
local x, _, z = getWorldTranslation(vehicle.rootNode)
near = math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2) <= ADFlowWagons.EMPTY_NEAR_M
end
return ADFlowWagons.isStalled({nearTarget = near, sinceChangeMs = nowMs - (state.changedAt or nowMs)})
end
local function tickEmptying(adEnv)
local list = {}
for vehicle, state in pairs(emptying) do table.insert(list, {vehicle = vehicle, state = state}) end
for _, item in ipairs(list) do
local vehicle, state = item.vehicle, item.state
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
local cargo, fillType = cargoOf(adEnv, vehicle)
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
if controlled then
emptying[vehicle] = nil -- spilleren har overtaget
else
local stalled = stalledAt(adEnv, vehicle, state, cargo)
local action = ADFlowWagons.emptyStep(state.stage, {cargo = cargo, adActive = module ~= nil and module:isActive(),
elapsedMs = nowMs - state.since, stalled = stalled, dumpAttempts = state.dumpAttempts})
if stalled and action ~= "wait" then
Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' kan ikke aflæsse ved %s (fuldt?) — videre", tostring(ADRuns.vehicleLabel(vehicle)),
tostring(state.markerName))
end
if action == "home" then
goHome(adEnv, vehicle)
elseif action == "dump" then
startDump(adEnv, vehicle, state)
elseif action == "giveUp" then
Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' kunne ikke tømme vognen (%d l %s tilbage) — parkeres med last",
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType))
goHome(adEnv, vehicle)
end
end
end
end
local function rememberAll(adEnv)
for _, run in ipairs(runs()) do
if not run.stopped then
for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
local wagonId = rear[1] ~= nil and 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
+168
View File
@@ -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
+582
View File
@@ -0,0 +1,582 @@
-- 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.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
-- 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)
if job.crept then state.arriveDistance = ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
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
+193
View File
@@ -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
+15 -1
View File
@@ -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
+34
View File
@@ -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
+146
View File
@@ -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
+168
View File
@@ -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
+89
View File
@@ -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
+39 -4
View File
@@ -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
+3 -1
View File
@@ -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
+485
View File
@@ -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
+121
View File
@@ -0,0 +1,121 @@
-- AD Profiler
-- Måler Smart Pickups indgange (AutoDrive-hooks, update-løkken, GUI) i spillet: kald, samlet tid og det
-- værste enkeltkald pr. minut, plus det værste frame i alt. Tager noget over REPORT_MIN_MS, skrives top-listen
-- i log.txt ("ADSmartPickup: profil …"), så et hak kan føres tilbage til præcis hvilken del der gjorde det.
-- Indpakningen sker på modul-tabellernes felter; hooksene slår funktionerne op ved hvert kald, så de måles.
ADProfiler = {}
ADProfiler.WINDOW_MS = 60000
ADProfiler.REPORT_MIN_MS = 2
ADProfiler.TOP = 6
ADProfiler.stats = {}
ADProfiler.frameMs = 0
ADProfiler.worstFrameMs = 0
ADProfiler.windowStart = nil
ADProfiler.depth = 0
ADProfiler.installed = false
function ADProfiler.clock()
local ok, ms = pcall(function() return netGetTime() end)
if ok and type(ms) == "number" then return ms end
return nil
end
local function record(name, started, ...)
local finished = ADProfiler.clock()
ADProfiler.depth = ADProfiler.depth - 1
if started ~= nil and finished ~= nil then
local spent = finished - started
local stat = ADProfiler.stats[name]
if stat == nil then
stat = {calls = 0, total = 0, max = 0}
ADProfiler.stats[name] = stat
end
stat.calls = stat.calls + 1
stat.total = stat.total + spent
if spent > stat.max then stat.max = spent end
-- kun yderste måling tæller i frame-summen (indlejrede måles også hver for sig)
if ADProfiler.depth == 0 then ADProfiler.frameMs = ADProfiler.frameMs + spent end
end
return ...
end
-- Pak tbl[key] ind under navnet name (gør intet hvis funktionen ikke findes).
function ADProfiler.wrap(tbl, key, name)
if type(tbl) ~= "table" or type(tbl[key]) ~= "function" then return end
local original = tbl[key]
tbl[key] = function(...)
ADProfiler.depth = ADProfiler.depth + 1
return record(name, ADProfiler.clock(), original(...))
end
end
-- Kaldes én gang pr. frame fra ADSmartPickup:update: afslutter frame-summen og rapporterer pr. minut.
function ADProfiler.frame(now)
if ADProfiler.frameMs > ADProfiler.worstFrameMs then ADProfiler.worstFrameMs = ADProfiler.frameMs end
ADProfiler.frameMs = 0
-- (en fejl i en indpakket funktion springer record over; dybden nulstilles derfor hvert frame)
ADProfiler.depth = 0
ADProfiler.windowStart = ADProfiler.windowStart or now
if now - ADProfiler.windowStart < ADProfiler.WINDOW_MS then return end
ADProfiler.report()
ADProfiler.windowStart = now
end
function ADProfiler.report()
local rows = {}
for name, stat in pairs(ADProfiler.stats) do
table.insert(rows, {name = name, stat = stat})
end
table.sort(rows, function(a, b) return a.stat.max > b.stat.max end)
local worst = rows[1] ~= nil and rows[1].stat.max or 0
if worst >= ADProfiler.REPORT_MIN_MS or ADProfiler.worstFrameMs >= ADProfiler.REPORT_MIN_MS then
local parts = {}
for index = 1, math.min(ADProfiler.TOP, #rows) do
local stat = rows[index].stat
table.insert(parts, string.format("%s max %.1f ms (%d kald, i alt %.0f ms)", rows[index].name, stat.max, stat.calls, stat.total))
end
Logging.info("ADSmartPickup: profil sidste minut — værste frame %.1f ms: %s", ADProfiler.worstFrameMs, table.concat(parts, "; "))
end
ADProfiler.stats = {}
ADProfiler.worstFrameMs = 0
end
-- Indpak alle kendte indgange. Kaldes én gang efter at hooksene er installeret.
function ADProfiler.install()
if ADProfiler.installed then return end
ADProfiler.installed = true
local targets = {
{ADUnloadWait, "beforeUpdate", "aflæsning/Wait (pr. vogn)"},
{ADUnloadWait, "adoptOrphans", "genoptag ventende"},
{ADUnloadWait, "releaseInactive", "slip standsede"},
{ADOutbound, "beforeLoadUpdate", "udkørsel pålæsning (pr. vogn)"},
{ADOutbound, "beforeUnloadUpdate", "udkørsel aflæsning (pr. vogn)"},
{ADOutbound, "beforeLoadFinished", "udkørsel runde"},
{ADOutbound, "choosePickup", "udkørsel turvalg"},
{ADOutbound, "holdSupplyVehicle", "forsyning hold"},
{ADOutbound, "releaseInactive", "udkørsel slip"},
{ADLoadSwap, "tryFromWait", "læs-bytte fra Wait"},
{ADSmartPickup, "shouldWaitForLoading", "vent ved pålæsning"},
{ADSmartPickup, "choosePickup", "forsyning kildevalg"},
{ADSmartPickup, "findSupplyPickup", "forsyning kildesøgning"},
{ADRunsController, "applyVehicleNames", "traktornavne"},
{ADFieldJobs, "tick", "markarbejde tick"},
{ADFields, "scanStep", "markscanning"},
{ADFieldWork, "follow", "markarbejde sæt"},
{ADFieldWork, "dispatch", "markarbejde udsendelse"},
{ADFieldWork, "getRigs", "markflåde"},
{ADBuildings, "list", "bygningsliste"},
{ADSources, "getFarmInventory", "gårdens lagre"},
{ADWaitPool, "hasRouteTo", "rutetjek"},
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
}
for _, target in ipairs(targets) do
ADProfiler.wrap(target[1], target[2], target[3])
end
if SmartPickupFrame ~= nil then
ADProfiler.wrap(SmartPickupFrame, "refreshLive", "menu opdatering")
ADProfiler.wrap(SmartPickupFrame, "rebuild", "menu genopbygning")
end
end
+69
View File
@@ -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,28 @@ 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).
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 +76,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 +86,8 @@ 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),
} }
end end
@@ -98,6 +126,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 = {},
@@ -164,6 +193,7 @@ 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
return new return new
end end
@@ -308,10 +338,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 +427,10 @@ 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),
} }
end end
return records return records
@@ -407,6 +473,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 +483,8 @@ 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),
}) })
store.nextId = math.max(store.nextId, id + 1) store.nextId = math.max(store.nextId, id + 1)
end end
+244 -30
View File
@@ -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,43 +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 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) table.insert(results, ADRunsController.startVehicle(adEnv, run, vehicle))
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
if not validation.ok then
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' sprunget over (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(validation.reason))
end
if validation.ok then
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
if ok then
started = started + 1
local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1])) or ("markør " .. tostring(run.targetWayPointId))
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), target)
else
result.ok, result.reason = false, "error"
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
end
end
table.insert(results, result)
end end
end end
if started > 0 then
ADRunsController.runningRuns[runId] = true
end
return results return results
end 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
if ADRunsController.getRunStatus(adEnv, run).isRunning then
summary.alreadyRunning = summary.alreadyRunning + 1
else
run.stopped = false
for _, vehicleId in ipairs(run.vehicleIds) do
local vehicle = vehiclesById[vehicleId]
if vehicle ~= nil then
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
-- solo-traktor med husket vogn: henter den ved start (valideringen ser den uden vogn)
if validation.ok or (ADFlowWagons ~= nil and (validation.reason == "noTrailer" or ADFlowWagons.hasWagonFor(run, vehicleId))) then
table.insert(queue, {runId = run.id, vehicleId = vehicleId})
else
table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding})
end
end
end
end
end
end
ADRunsController.startQueue = queue
ADRunsController.startQueueTotal = #queue
staggerTimerMs = ADRunsController.STAGGER_MS -- første startes ved næste tick
summary.queued = #queue
return summary
end
-- Stop alt og parkér (Oversigt): flows, markarbejde og marksilo-lån stoppes, køretøjerne sendes til deres
-- AD-parkeringspunkt, og moden sættes på pause (gemmes), så intet starter igen før "Start alle".
-- Høstere med manuel transport parkeres ikke (må ikke køre på vejen med skærebord). Returnerer
-- {stopped, parked, noPark = {navne}, manual = {navne}}.
function ADRunsController.stopAllAndPark(adEnv)
local summary = {stopped = 0, parked = 0, noPark = {}, manual = {}}
ADRunsController.clearStartQueue()
local toPark, seen = {}, {}
local vehiclesById = getFarmVehiclesById(adEnv)
for _, run in ipairs(ADRunsController.store.runs) do
for _, vehicleId in ipairs(run.vehicleIds) do
local vehicle = vehiclesById[vehicleId]
if vehicle ~= nil and not seen[vehicle] then
seen[vehicle] = true
if isVehicleActive(vehicle) then
stopVehicle(vehicle)
summary.stopped = summary.stopped + 1
end
table.insert(toPark, vehicle)
end
end
ADRunsController.runningRuns[run.id] = nil
run.stopped = true
end
if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then
for _, stopped in ipairs(ADFieldWork.stopAllAssignments(adEnv) or {}) do
summary.stopped = summary.stopped + 1
if stopped.pooled then
summary.parked = summary.parked + 1
elseif stopped.manual then
table.insert(summary.manual, tostring(ADRuns.vehicleLabel(stopped.vehicle)))
elseif not seen[stopped.vehicle] then
seen[stopped.vehicle] = true
table.insert(toPark, stopped.vehicle)
end
end
end
if ADFieldSilos ~= nil and ADFieldSilos.stopAll ~= nil then pcall(ADFieldSilos.stopAll, adEnv) end
if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end
for _, vehicle in ipairs(toPark) do
if ADHitch ~= nil and ADHitch.stow ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil then ADHitch.stow(vehicle, "stop alt") end
-- flow-traktor: tøm vognen, sæt den af på sin plads og kør hjem (ADFlowWagons klarer hele turen)
local okHome, pooled = false, false
if ADFlowWagons ~= nil and ADRunsController.getRunForVehicle(vehicle) ~= nil then
okHome = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle)
pooled = okHome
elseif ADHitch ~= nil then
okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
end
local park = not (okHome and pooled) and ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil
if okHome and pooled then
summary.parked = summary.parked + 1
elseif park ~= nil then
local ok = pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil)
if ok then summary.parked = summary.parked + 1 end
else
table.insert(summary.noPark, tostring(ADRuns.vehicleLabel(vehicle)))
end
end
Logging.info(ADRunsController.LOG_PREFIX .. "Stop alt og parkér: %d stoppet, %d til parkering, %d uden parkeringspunkt, %d høstere hentes manuelt — pause",
summary.stopped, summary.parked, #summary.noPark, #summary.manual)
return summary
end
-- Starter næste traktor i køen, når der er gået STAGGER_MS siden forrige.
function ADRunsController.processStartQueue(adEnv, dt)
if #ADRunsController.startQueue == 0 then return end
staggerTimerMs = staggerTimerMs + (dt or 0)
if staggerTimerMs < ADRunsController.STAGGER_MS then return end
staggerTimerMs = 0
local item = table.remove(ADRunsController.startQueue, 1)
local run = ADRuns.getRun(ADRunsController.store, item.runId)
local vehicle = run ~= nil and getFarmVehiclesById(adEnv)[item.vehicleId] or nil
if run ~= nil and vehicle ~= nil and not isVehicleActive(vehicle) then
ADRunsController.startVehicle(adEnv, run, vehicle)
end
end
-- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job — -- En kørende traktor der fjernes fra sit run (eller hvis run slettes) kører run'ets job —
-- den stoppes, så den ikke fortsætter uden run. Returnerer true hvis den blev stoppet. -- den stoppes, så den ikke fortsætter uden run. Returnerer true hvis den blev stoppet.
function ADRunsController.stopIfRunning(vehicle, reason) function ADRunsController.stopIfRunning(vehicle, reason)
@@ -1089,6 +1258,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 +1273,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 +1293,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
@@ -1287,6 +1471,12 @@ 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
-- 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 +1488,10 @@ 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 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 +1502,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
+7
View File
@@ -46,7 +46,11 @@ 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 "")
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 +78,10 @@ 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"),
vehicleIds = {}, vehicleIds = {},
} }
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey) xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
+120 -12
View File
@@ -10,14 +10,29 @@
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/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 +231,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)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) return cacheGet("loadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
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)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId) return cacheGet("unloadStation", tostring(wayPointId) .. ":" .. tostring(farmId), ADSmartPickup.STATION_CACHE_MS, function()
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
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 +265,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 +374,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 +422,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 +482,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 +669,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 +702,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 +729,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 +797,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 +1148,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 +1202,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 +1263,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 +1291,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,6 +1303,26 @@ 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 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))
+120 -25
View File
@@ -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
if ax ~= nil then table.insert(segments, {ax, az, bx or ax, bz or az}) end
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
total = total + level if own <= radius2 then
count = count + 1 if isNearestSpawner(rivals, placeable, own, x, z) then
total = total + level
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
@@ -999,6 +1090,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
+212
View File
@@ -108,3 +108,215 @@ 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
table.sort(list, function(a, b)
if a.score ~= b.score then return a.score > b.score end
return tostring(a.stat.title) < tostring(b.stat.title)
end)
local result = {}
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
return result
end
function ADStorageStats.kpis(stats)
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
for _, stat in ipairs(stats or {}) do
kpi.valueNow = kpi.valueNow + (stat.valueNow or 0)
kpi.gain = kpi.gain + math.max(0, (stat.valueTop or 0) - (stat.valueNow or 0))
if stat.emptyLevel ~= nil then kpi.emptyCount = kpi.emptyCount + 1 end
if stat.fullLevel ~= nil then kpi.fullCount = kpi.fullCount + 1 end
end
return kpi
end
-- procent -> bjælkens brøk i [0,1]
function ADStorageStats.fraction(percent)
if percent == nil then return 0 end
return math.max(0, math.min(1, percent / 100))
end
-- ---- "kræver handling" -> flow ----
-- Bygninger der BRUGER varen (fabrik-input, stald-foder) — silo/objektlager lagrer kun. -> {{name, placeable, level}}
function ADStorageStats.consumersOf(inventory, fillType)
local result = {}
for _, building in ipairs(inventory or {}) do
if building.kind == "factory" or building.kind == "husbandry" then
for _, entry in ipairs(building.entries or {}) do
if entry.fillType == fillType and not entry.isOutput then
table.insert(result, {name = building.name, placeable = building.placeable, level = entry.level or 0})
end
end
end
end
return result
end
-- Hjælper et flow? Et flow flytter kun varer: det nytter når varen ligger andre steder end hos forbrugerne.
-- s = {farmLevel, consumerLevel, perDay, consumerCount, existingRunId} -> "open" | "flow" | "produce" | "none"
ADStorageStats.MIN_FLOW_LITERS = 5000
ADStorageStats.MIN_FLOW_DAY_SHARE = 0.25 -- mindst en kvart dags forbrug skal kunne flyttes
function ADStorageStats.flowAdvice(s)
if (s.consumerCount or 0) == 0 then return "none" end
if s.existingRunId ~= nil then return "open" end
local elsewhere = (s.farmLevel or 0) - (s.consumerLevel or 0)
local needed = math.max(ADStorageStats.MIN_FLOW_LITERS, (s.perDay or 0) * ADStorageStats.MIN_FLOW_DAY_SHARE)
return elsewhere >= needed and "flow" or "produce"
end
-- "Sælg nu" -> udkørsels-flow: s = {sourceCount, existingRunId} -> "open" | "flow" | "none"
function ADStorageStats.sellAdvice(s)
if (s.sourceCount or 0) == 0 then return "none" end
if s.existingRunId ~= nil then return "open" end
return "flow"
end
+79
View File
@@ -0,0 +1,79 @@
-- AD Stuck Diag
-- Diagnose: et aktivt AutoDrive-køretøj der har holdt stille længe, logges med AutoDrives egen grund
-- (forhindring, AD-trafik på ruten + hvem, pause, mål nået) og om Smart Pickup holder det (Wait/udkørsel).
-- Kun læsning — ændrer intet. Logges én gang pr. ændret tilstand.
ADStuckDiag = {}
ADStuckDiag.CHECK_INTERVAL_MS = 10000
ADStuckDiag.STILL_MS = 20000
ADStuckDiag.STILL_SPEED = 0.00028 -- samme grænse som AD's egen stoppedTimer (m/ms)
local stillMs = setmetatable({}, {__mode = "k"})
local lastText = setmetatable({}, {__mode = "k"})
local timerMs = 0
local function name(vehicle)
if vehicle == nil then return "-" end
local ok, text = pcall(vehicle.getName, vehicle)
return ok and tostring(text) or "?"
end
local function taskName(vehicle)
local taskModule = vehicle.ad.taskModule
local task = taskModule ~= nil and taskModule.activeTask or nil
if task == nil then return "ingen" end
return tostring(task.taskType or "ukendt")
end
-- Tekst der beskriver hvorfor køretøjet står (nil hvis ukendt/ikke relevant).
function ADStuckDiag.describe(vehicle)
local ad = vehicle.ad
local cdm, dpm, sdm = ad.collisionDetectionModule, ad.drivePathModule, ad.specialDrivingModule
local wayPoints, current = nil, nil
if dpm ~= nil and dpm.getWayPoints ~= nil then wayPoints, current = dpm:getWayPoints() end
local parts = {
string.format("opgave=%s", taskName(vehicle)),
string.format("mål=%s", tostring(ad.stateModule.getFirstMarkerId ~= nil and ad.stateModule:getFirstMarkerId() or "?")),
string.format("wp=%s/%s", tostring(current), tostring(wayPoints ~= nil and #wayPoints or nil)),
string.format("pause=%s", tostring(dpm ~= nil and dpm.isPaused)),
string.format("målNået=%s", tostring(dpm ~= nil and dpm.isTargetReached ~= nil and dpm:isTargetReached())),
string.format("forhindring=%s", tostring(cdm ~= nil and cdm.detectedObstable)),
string.format("trafik=%s", cdm ~= nil and cdm.trafficVehicle ~= nil and name(cdm.trafficVehicle) or "-"),
string.format("stopper=%s blokeret=%s", tostring(sdm ~= nil and sdm.isStoppingVehicle ~= nil and sdm:isStoppingVehicle()),
tostring(sdm ~= nil and sdm.isBlocked)),
}
local wait = ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) or nil
local hold = ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) or nil
local pickup = ADSmartPickup ~= nil and ADSmartPickup.getActivePickup ~= nil and ADSmartPickup.getActivePickup(vehicle) or nil
table.insert(parts, string.format("SP: wait=%s hold=%s pickup=%s",
wait ~= nil and tostring(wait.markerId or wait.wayPointId or "ja") or "-",
hold ~= nil and tostring(hold.markerId or "ja") or "-",
pickup ~= nil and tostring(pickup.wayPointId or pickup.markerId or "ja") or "-"))
return table.concat(parts, " ")
end
function ADStuckDiag.tick(dt)
timerMs = timerMs + (dt or 0)
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
local elapsed = timerMs
timerMs = 0
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
for _, vehicle in pairs(vehicles) do
local ad = vehicle.ad
local active = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive()
if active and (vehicle.lastSpeedReal or 0) < ADStuckDiag.STILL_SPEED then
stillMs[vehicle] = (stillMs[vehicle] or 0) + elapsed
else
stillMs[vehicle], lastText[vehicle] = nil, nil
end
if (stillMs[vehicle] or 0) >= ADStuckDiag.STILL_MS then
local ok, text = pcall(ADStuckDiag.describe, vehicle)
text = ok and text or ("diagnose fejlede: " .. tostring(text))
if text ~= lastText[vehicle] then
lastText[vehicle] = text
Logging.info("ADSmartPickup: STÅR STILLE %d s '%s': %s", math.floor(stillMs[vehicle] / 1000), name(vehicle), text)
end
end
end
end
+30 -2
View File
@@ -29,15 +29,32 @@ 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
return level < bestLevel if ample == nil then
return level < bestLevel
end
local isAmple, bestIsAmple = level >= ample, bestLevel >= ample
if isAmple ~= bestIsAmple then return isAmple end
if isAmple then return level < bestLevel end
return level > bestLevel
end end
function ADSupplyPlanner.copyList(list) function ADSupplyPlanner.copyList(list)
@@ -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
+19 -3
View File
@@ -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)
+135 -12
View File
@@ -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,130 @@
-- Smart Pickup — "Kræver handling" -> flow (v1.60)
-- En "løber tør"-linje i lagerfanerne kan klikkes: findes der et flow der leverer varen til forbrugerne, åbnes
-- det; ligger varen andre steder på gården, åbnes "Nyt flow" udfyldt (ind til forbrugerne, varen valgt); ellers
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
-- Forsyningsflow der allerede leverer varen til en af forbrugerne -> run eller nil.
local function existingSupplyRun(consumerIds, name)
for _, run in ipairs(ADRunsController.store.runs) do
if run.kind ~= "outbound" then
local carries = run.ingredientMode ~= "manual"
for _, ingredient in ipairs(run.ingredients or {}) do
if ingredient == name then carries = true end
end
if carries then
for _, id in ipairs(run.targetBuildingIds or {}) do
if consumerIds[id] then return run end
end
end
end
end
return nil
end
-- Udkørselsflow der allerede sælger varen fra en af bygningerne -> run eller nil.
local function existingSellRun(sourceIds, name)
for _, run in ipairs(ADRunsController.store.runs) do
if run.kind == "outbound" then
local sells = false
for _, product in ipairs(run.products or {}) do
if product.fillType == name and (product.policy == "sell" or product.policy == "sellElseStore") then sells = true end
end
if sells then
for _, id in ipairs(run.sourceBuildingIds or {}) do
if sourceIds[id] then return run end
end
end
end
end
return nil
end
-- "Sælg nu": kilder = fanens bygninger med varen.
local function sellAdviceFor(stat)
local sourceIds, ids = {}, {}
for _, building in ipairs(stat.buildings or {}) do
local id = building.placeable ~= nil and ADBuildings.getId(building.placeable) or nil
if id ~= nil and not sourceIds[id] then
sourceIds[id] = true
table.insert(ids, id)
end
end
local name = fillTypeName(stat.fillType)
local run = existingSellRun(sourceIds, name)
local advice = ADStorageStats.sellAdvice({sourceCount = #ids, existingRunId = run ~= nil and run.id or nil})
return advice, {buildingIds = ids, run = run, name = name, stat = stat, sell = true}
end
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
function SmartPickupFrame:attentionAdvice(entry)
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
local stat = entry.stat
local consumers = ADStorageStats.consumersOf(self.lastFullInventory, stat.fillType)
local consumerIds, consumerLevel, ids = {}, 0, {}
for _, consumer in ipairs(consumers) do
consumerLevel = consumerLevel + consumer.level
local id = consumer.placeable ~= nil and ADBuildings.getId(consumer.placeable) or nil
if id ~= nil and not consumerIds[id] then
consumerIds[id] = true
table.insert(ids, id)
end
end
local name = fillTypeName(stat.fillType)
local run = existingSupplyRun(consumerIds, name)
local advice = ADStorageStats.flowAdvice({farmLevel = stat.farmLevel or stat.level, consumerLevel = consumerLevel,
perDay = stat.consumption, consumerCount = #ids, existingRunId = run ~= nil and run.id or nil})
return advice, {consumers = consumers, buildingIds = ids, run = run, name = name, stat = stat}
end
function SmartPickupFrame:attentionHint(advice, info)
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
return ""
end
function SmartPickupFrame:onAttentionClick(entry)
self:safe(function(frame)
local adEnv = frame.getAdEnv()
local advice, info = frame:attentionAdvice(entry)
if adEnv == nil or advice == "none" then return end
if advice == "open" then
frame.selectedRunId = info.run.id
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
frame:openEditPanel("flow", info.run.id)
return
end
if advice == "produce" then
local text = string.format(frame.i18n:getText("spu_attention_produce"), tostring(info.stat.title),
SmartPickupFrame.formatCompact(info.stat.farmLevel or 0))
frame.statusOverride, frame.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
return
end
local guide
if info.sell then
-- udfyldt "Nyt flow": ud af bygningerne med varen, varen til salg
local name = string.format(frame.i18n:getText("spu_attention_sellFlowName"), tostring(info.stat.title))
guide = ADFlowGuide.openNew(name, {})
guide.draft.kind = "outbound"
guide.draft.sourceBuildingIds = info.buildingIds
guide.draft.products = {{fillType = info.name, policy = "sell"}}
else
-- udfyldt "Nyt flow": ind til forbrugerne, kun denne vare; guiden åbnes på oversigten (Gem/Start eller [Ret])
local name = string.format(frame.i18n:getText("spu_attention_flowName"), tostring(info.stat.title), tostring(info.consumers[1].name))
guide = ADFlowGuide.openNew(name, {})
guide.draft.targetBuildingIds = info.buildingIds
guide.draft.ingredientMode = "manual"
guide.draft.ingredients = {info.name}
end
guide = ADGuide.new(ADFlowGuide.DEF, guide.draft, frame:flowGuideContext(adEnv, guide.draft), {step = "summary"})
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
frame:openGuide("flow", guide)
end)
end
@@ -0,0 +1,194 @@
-- Smart Pickup — redigeringsvindue (v1.58)
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS[kind] =
-- {title, exists, fill, status, startStop, startStopText, canDelete, delete} (se Flows-/Fields-fanen).
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.80, 0.90 -- andel af listens flade
SmartPickupFrame.EDIT_PANEL_HEADER_PX = 80
SmartPickupFrame.EDIT_PANEL_ROW_PX = 860 -- rækkerne er ~835 px brede
function SmartPickupFrame:initEditPanelButtons(i18n)
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
self.panelButtonsNoDelete = {self.panelCloseButtonInfo, self.panelStartButtonInfo}
self.panelGuideButtonCache = setmetatable({}, {__mode = "k"})
end
-- Vinduets knaplinje (uden Slet for arter der ikke kan slettes).
function SmartPickupFrame:getPanelButtons()
local hooks = self:editPanelHooks()
return (hooks ~= nil and hooks.canDelete) and self.panelButtons or self.panelButtonsNoDelete
end
function SmartPickupFrame:editPanelHooks()
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
end
-- Guidens knapper i vinduet: Tilbage (ESC) lukker trinnet/vinduet i stedet for hele menuen.
function SmartPickupFrame:panelGuideButtons()
local source = self:getGuideButtons()
local cached = self.panelGuideButtonCache[source]
if cached == nil then
cached = {}
for index, info in ipairs(source) do
cached[index] = info == self.backButtonInfo and self.panelCloseButtonInfo or info
end
self.panelGuideButtonCache[source] = cached
end
return cached
end
-- Sæt størrelse FØR position: setSize kan genberegne den absolutte position ud fra forælderen (review 29/9).
-- -> true hvis elementet flyttede sig.
local function place(element, x, y, w, h)
if w ~= nil then element:setSize(w, h) end
local pos = element.absPosition
local moved = pos == nil or math.abs(pos[1] - x) > 1e-6 or math.abs(pos[2] - y) > 1e-6
element:setAbsolutePosition(x, y)
return moved
end
-- Vinduet dækker listens flade; panelet er centreret i den (absolutte koordinater, y opad). Kaldes ved åbning,
-- ved hver opbygning og hvert sekund, så en layout-gennemgang i menuen ikke flytter det væk.
function SmartPickupFrame:layoutEditPanel()
local box = self.boxLayout
if box == nil or box.absPosition == nil or box.absSize == nil then return end
local x, y, w, h = box.absPosition[1], box.absPosition[2], box.absSize[1], box.absSize[2]
place(self.editBackdrop, x, y, w, h)
-- så bredt som rækkerne (~835 px) + margen, ikke bredere end listen
local pw = math.min(w * SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_ROW_PX * g_pixelSizeScaledX)
local ph = h * SmartPickupFrame.EDIT_PANEL_HEIGHT
local px, py = x + (w - pw) / 2, y + (h - ph) / 2
place(self.editPanel, px, py, pw, ph)
local header = SmartPickupFrame.EDIT_PANEL_HEADER_PX * g_pixelSizeScaledY
local margin = 20 * g_pixelSizeScaledX
place(self.editTitle, px + margin, py + ph - header * 0.45)
place(self.editStatus, px + margin, py + ph - header * 0.85)
if place(self.editLayout, px, py, pw, ph - header) then
self.editLayout:invalidateLayout()
end
end
function SmartPickupFrame:openEditPanel(kind, id)
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil or self.editOverlay == nil then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
self:closeGuide()
self.editOverlay:setVisible(true)
-- listen skjules bag vinduet: dens tekster skinnede igennem panelet (in-game 29/9)
self.boxLayout:setVisible(false)
self:layoutEditPanel()
self:requestRebuild()
end
-- Lukker uden genopbygning (fanebyt, lukket menu, slettet post).
function SmartPickupFrame:hideEditPanel()
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self:clearPanelRows()
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
if self.boxLayout ~= nil then self.boxLayout:setVisible(true) end
end
function SmartPickupFrame:closeEditPanel()
if not self.editPanelOpen then return end
self:closeGuide()
self:hideEditPanel()
self:requestRebuild()
end
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
function SmartPickupFrame:fillEditPanel(adEnv)
self:clearPanelRows()
local hooks = self:editPanelHooks()
if hooks == nil then return end
if not hooks.exists(self, self.editPanelId) then
self.statusOverride = self.i18n:getText("spu_panel_gone")
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self:closeGuide()
self:hideEditPanel()
return
end
self:layoutEditPanel()
self.editTitle:setText(hooks.title(self, self.editPanelId))
self:withRowTarget(self.editLayout, function(frame)
if frame.guide ~= nil then
frame:fillGuideRows(adEnv)
else
hooks.fill(frame, adEnv, frame.editPanelId)
end
end)
self:colorLayout(self.editLayout)
self.editLayout:invalidateLayout()
self:refreshEditPanel(adEnv)
end
-- Hvert sekund: status i niveaufarve + knaptekster.
function SmartPickupFrame:refreshEditPanel(adEnv)
local hooks = self:editPanelHooks()
if hooks == nil or adEnv == nil then return end
if not hooks.exists(self, self.editPanelId) then
self.statusOverride = self.i18n:getText("spu_panel_gone")
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self:closeEditPanel()
return
end
self:layoutEditPanel()
local status = hooks.status(self, adEnv, self.editPanelId)
self.editStatus:setText(tostring(status.text or ""))
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
if self.editStatus.setTextColor ~= nil then
self.editStatus:setTextColor(color[1], color[2], color[3], color[4])
end
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
self.panelStartButtonInfo.disabled = hooks.canStart ~= nil and not hooks.canStart(self, self.editPanelId)
end
-- ESC mens vinduet er åbent: menuen spørger requestClose; vinduet lukkes (trin først), menuen bliver.
-- Kalder spillet OGSÅ knappens callback for samme tryk, må det ikke lukke to trin (panelClosedAt).
SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS = 250
function SmartPickupFrame:requestClose(callback)
if self.editPanelOpen then
self:onClickPanelClose()
return false
end
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
return false
end
return SmartPickupFrame:superClass().requestClose(self, callback)
end
function SmartPickupFrame:onClickPanelClose()
if g_time ~= nil and self.panelClosedAt ~= nil and g_time - self.panelClosedAt < SmartPickupFrame.PANEL_ESC_DEBOUNCE_MS then
return
end
self.panelClosedAt = g_time
self:safe(function(frame)
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
if frame.guide ~= nil then
frame:closeGuide()
frame:requestRebuild()
return
end
frame:closeEditPanel()
end)
end
function SmartPickupFrame:onClickPanelStartStop()
self:safe(function(frame)
local hooks = frame:editPanelHooks()
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickPanelDelete()
self:safe(function(frame)
local hooks = frame:editPanelHooks()
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
end)
end
@@ -0,0 +1,242 @@
-- Smart Pickup — guiden "Opsæt mark" og fanen Markers knapper
-- Henter det spil-afhængige (afgrøder der kan sås, slåafgrøder, halm, maskiner, marksiloer, siloer i nærheden)
-- som `context` til ADFieldGuide og gemmer guidens resultat via ADFieldJobs. Selve trin-skærmen er fælles
-- (gui/SmartPickupGuide.lua). Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
-- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer)
SmartPickupFrame.FIELD_STORE_ROWS = 8
function SmartPickupFrame:initFieldButtons(i18n)
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
self.fieldSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_field_open"), callback = function() self:onClickFieldSetup() end}
self.fieldFleetButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_open"), callback = function() self:onClickFieldFleet(true) end}
self.fieldFleetCloseButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_close"), callback = function() self:onClickFieldFleet(false) end}
-- v1.59: listen har Start/Stop pr. række; Opsæt mark ligger i redigeringsvinduet
self.fieldButtons = {self.backButtonInfo, self.fieldFleetButtonInfo}
self.fieldFleetButtons = {self.backButtonInfo, self.fieldFleetCloseButtonInfo}
end
function SmartPickupFrame:getFieldButtons()
return self.fieldView == "fleet" and self.fieldFleetButtons or self.fieldButtons
end
-- ---- context til ADFieldGuide ----
-- {[fieldId] = rigIds} for alle marker
function SmartPickupFrame.rigIdsByField()
local byField = {}
for fieldId, field in pairs(ADFieldJobs.state.fields) do byField[fieldId] = field.plan ~= nil and field.plan.rigIds or {} end
return byField
end
local function fruitTypes()
return g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
end
-- Afgrøder der kan sås, slåafgrøder (navne) og afgrøder med halm (skår efter mejetærskeren).
function SmartPickupFrame:fruitContext()
local sowable, straw, mowable = {}, {}, {}
for _, fruit in ipairs(fruitTypes()) do
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
table.insert(sowable, {name = fruit.name, title = tostring(fruit.fillType.title or fruit.name)})
end
local ok, hasStraw = pcall(function() return fruit.hasWindrow == true or fruit.windrowName ~= nil end)
if ok and hasStraw and fruit.name ~= nil then straw[fruit.name] = true end
end
if ADFields ~= nil and g_fruitTypeManager ~= nil then
for index in pairs(ADFields.getMowable()) do
local fruit = g_fruitTypeManager:getFruitTypeByIndex(index)
if fruit ~= nil and fruit.name ~= nil then mowable[fruit.name] = true end
end
end
return sowable, mowable, straw
end
-- Markens vare ud fra kladdens plan (ensilage -> skår, hø -> tørskår, høst -> afgrøden); nil på bar mark.
local function fieldFillType(info, plan)
if info.kind == "mowable" then
return ADFieldModel.product(plan, "mowable") == "hay" and info.hay or info.windrow
end
if info.kind == "harvest" then return info.harvestFillType end
return nil
end
-- Siloer der tager markens vare: marksilo først, gæringssiloer, så de nærmeste (samme regel som før).
function SmartPickupFrame:fieldStoreContext(adEnv, info, plan, chosenIds)
local fillType = fieldFillType(info, plan)
local farmId = g_currentMission:getFarmId()
local fieldSilos, silosById = {}, {}
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
table.insert(fieldSilos, {id = building.id, name = tostring(building.name),
text = markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)})
silosById[building.id] = true
end
if fillType == nil then return nil, 0, fieldSilos end
local chosen = {}
for _, id in ipairs(chosenIds or {}) do chosen[id] = true end
local fermenterIds = {}
if info.kind == "mowable" and ADFieldModel.product(plan, "mowable") == "silage" then
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
end
local entries = {}
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
if chosen[building.id] or isSilo or fermenterIds[building.id] then
local distance = nil
local node = building.placeable ~= nil and building.placeable.rootNode or nil
if node ~= nil and info.x ~= nil then
local bx, _, bz = getWorldTranslation(node)
distance = math.sqrt((bx - info.x) ^ 2 + (bz - info.z) ^ 2)
end
table.insert(entries, {id = building.id, building = building, distance = distance, chosen = chosen[building.id] == true,
fieldSilo = silosById[building.id] == true, fermenter = fermenterIds[building.id] == true})
end
end
local shown, hidden = ADFieldView.storeRows(entries, SmartPickupFrame.FIELD_STORE_ROWS)
local stores = {}
for _, entry in ipairs(shown) do
local building = entry.building
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
-- det der skelner rækkerne først (rækken afkorter bagfra): markør, afstand, art — så bygningens navn
local label = (markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)) .. ", " .. ADFieldView.distanceText(entry.distance)
if entry.fieldSilo then label = label .. " (" .. self.i18n:getText("spu_field_fieldSilo") .. ")" end
if entry.fermenter then label = label .. " (" .. self.i18n:getText("spu_field_fermenter") .. ")" end
if markers[1] ~= nil then label = label .. " — " .. tostring(building.name) end
table.insert(stores, {id = building.id, text = label})
end
return stores, hidden, fieldSilos
end
-- Markerne til trin 1 ("F22 — Hvede — Venter").
function SmartPickupFrame:fieldPickList()
local fields = {}
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do
local crop = ADFieldView.cropText(row.info, function(key) return self.i18n:getText(key) end)
table.insert(fields, {id = row.info.id, text = string.format("F%d — %s — %s", row.info.id, crop, self:fieldStatusText(row.field, row.info, true))})
end
return fields
end
-- context for en mark (info) med kladdens plan; info nil = trin 1 uden valgt mark.
function SmartPickupFrame:fieldGuideContext(adEnv, info, plan, chosenStoreIds)
local sowable, mowable, straw = self:fruitContext()
local context = {info = info or {}, sowable = sowable, mowable = mowable, straw = straw,
rigIdsByField = SmartPickupFrame.rigIdsByField(), fieldSilos = {}, storeHidden = 0, fields = {}, rigs = {}}
-- marklisten bruges kun i trin 1 (guiden åbnet uden valgt mark)
if info == nil then context.fields = self:fieldPickList() end
for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do
table.insert(context.rigs, {id = rig.id, role = rig.role, text = string.format("%s — %s", tostring(rig.label), self:roleText(rig.role))})
end
if info ~= nil then
context.stores, context.storeHidden, context.fieldSilos = self:fieldStoreContext(adEnv, info, plan or {}, chosenStoreIds)
end
return context
end
-- ---- hooks til den fælles guide-skærm ----
local function chosenStoreIds(guide)
local ids = {}
for _, id in ipairs(guide.draft.storeIds or {}) do table.insert(ids, id) end
for _, id in ipairs(guide.draft.plan ~= nil and guide.draft.plan.storeBuildingIds or {}) do table.insert(ids, id) end
return ids
end
SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
SmartPickupFrame.GUIDE_HOOKS.field = {
model = ADFieldGuide,
tab = SmartPickupFrame.TAB_FIELDS,
cannotAdvanceKey = "spu_guide_pickField",
heading = function(frame, guide)
if guide.draft.fieldId == nil then return frame.i18n:getText("spu_guide_field_titleNone") end
return string.format(frame.i18n:getText("spu_guide_field_title"), guide.draft.fieldId)
end,
context = function(frame, adEnv, guide)
local fieldId = guide.draft.fieldId
if fieldId == nil then return frame:fieldGuideContext(adEnv, nil) end
local info = ADFields.getInfo(fieldId)
-- marken er solgt/bygget over mens guiden var åben
if info == nil or info.cultivable == false then return nil end
return frame:fieldGuideContext(adEnv, info, guide.draft.plan, chosenStoreIds(guide))
end,
choose = function(frame, id, value)
if id ~= "field" then return false end
local info = ADFields.getInfo(value)
if info == nil then return true end
local field = ADFieldJobs.getField(value)
frame.selectedFieldId = value
frame.statusOverride = nil
frame.guide = ADFieldGuide.pickField(frame.guide, field, frame:fieldGuideContext(frame.getAdEnv(), info, field.plan, field.plan.storeBuildingIds))
return true
end,
canAdvance = function(guide) return guide.draft.fieldId ~= nil end,
startKey = function() return "spu_guide_runAuto" end,
-- mål-afgrøde kører altid automatisk: ingen "Gem uden at starte"
showSaveOnly = function(guide) return (guide.draft.plan.targetFruit or "") == "" end,
save = function(frame, guide, start) frame:saveFieldGuide(guide, start) end,
afterRows = function(frame, adEnv, guide)
if guide.step ~= "summary" or guide.draft.fieldId == nil then return end
local info = guide.context.info
if info ~= nil and info.markerId == nil then
frame:addTextRow(" " .. string.format(frame.i18n:getText("spu_guide_noMarker"), guide.draft.fieldId))
end
end,
}
-- Gem: mål-afgrøde (hvis ændret), planen, og faste maskiner (fjernes fra andre marker).
function SmartPickupFrame:saveFieldGuide(guide, start)
local result = ADFieldGuide.result(guide, start)
if result.fieldId == nil then return end
if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
ADFieldJobs.setPlan(result.fieldId, result.plan)
-- markens form ændret siden kurserne blev lavet: gemte Courseplay-kurser til marken glemmes
local fieldInfo = ADFields.getInfo(result.fieldId)
if fieldInfo ~= nil and ADCourseCache ~= nil then ADCourseCache.checkField(result.fieldId, fieldInfo.shapeSig) end
local byField = SmartPickupFrame.rigIdsByField()
for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
ADFieldJobs.setPlan(fieldId, {rigIds = ids})
end
end
ADFieldJobs.log(result.fieldId, "guide gemt: mode=%s mål=%s faste=%s lager=%s", tostring(result.plan.mode), result.targetFruit ~= "" and result.targetFruit or "-",
table.concat(result.rigIds, " "), table.concat(result.plan.storeBuildingIds or {}, " "))
self.selectedFieldId = result.fieldId
self.statusOverride = string.format(self.i18n:getText("spu_guide_saved"), result.fieldId)
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
end
-- ---- knapper og [Ret] ----
-- [Opsæt mark]: den valgte mark med de nuværende valg, ellers trin 1 (vælg mark).
function SmartPickupFrame:onClickFieldSetup()
self:safe(function(frame)
local adEnv = frame.getAdEnv()
if adEnv == nil then return end
local info = frame.selectedFieldId ~= nil and ADFields.getInfo(frame.selectedFieldId) or nil
frame.fieldView = nil
if info == nil then
frame:openGuide("field", ADFieldGuide.openPicker(frame:fieldGuideContext(adEnv, nil)))
return
end
local field = ADFieldJobs.getField(info.id)
frame:openGuide("field", ADFieldGuide.open(field, frame:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)))
end)
end
-- [Ret] på en resumé-linje i fanens oversigt: kun det ene trin; [Gem] gemmer og går tilbage.
function SmartPickupFrame:openFieldStep(fieldId, step)
local adEnv = self.getAdEnv()
local info = ADFields.getInfo(fieldId)
if adEnv == nil or info == nil then return end
local field = ADFieldJobs.getField(fieldId)
local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
self:openGuide("field", ADFieldGuide.open(field, context, {step = step, single = true, returnTo = "close"}))
end
function SmartPickupFrame:onClickFieldFleet(show)
self:safe(function(frame)
frame.fieldView = show and "fleet" or nil
frame:requestRebuild()
end)
end
@@ -0,0 +1,117 @@
-- Smart Pickup — fanen Marksiloer
-- Opsætning (til/fra, maks. vogne, mindste mængde, kør til produktion + foderreserve, gårdlagre) og status for marksilo-flowet.
-- Klik går til onClickFieldSiloRow; reglerne ligger i ADFieldSilos/ADFieldSiloPlanner.
SmartPickupFrame.FIELDSILO_ROW_KINDS = {fsEnabled = true, fsMaxVehicles = true, fsMinLiters = true, fsStore = true,
fsToProduction = true, fsReserveDays = true}
local function numberTexts(from, to)
local texts = {}
for n = from, to do table.insert(texts, tostring(n)) end
return texts
end
local function literTexts()
local texts = {}
for _, liters in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do table.insert(texts, string.format("%d l", liters)) end
return texts
end
local function dayTexts(i18n)
local texts = {}
for _, days in ipairs(ADFieldSiloConfig.RESERVE_DAYS_CHOICES) do table.insert(texts, string.format(i18n:getText("spu_fs_days"), days)) end
return texts
end
local function indexOf(list, value)
for index, entry in ipairs(list) do if entry == value then return index end end
return 1
end
-- Valg med flere værdier: productPrefab (MultiTextOption). ingredientPrefab er en BinaryOption med kun to sider.
function SmartPickupFrame:addFieldSiloChoiceRow(kind, title, texts, state)
local element = self:addRow(self.productPrefab, {kind = kind})
local option = element:getDescendantByName("option")
option:setTexts(texts)
option:setState(state, false)
element:getDescendantByName("title"):setText(title)
local tooltip = option:getDescendantByName("ignore")
if tooltip ~= nil then tooltip:setText("") end
return option
end
function SmartPickupFrame:fillFieldSiloRows(adEnv)
local i18n, cfg = self.i18n, ADFieldSilos.config
local onOff = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
self:addToggleRow("fsEnabled", i18n:getText("spu_fs_enabled"), onOff, cfg.enabled)
self:addFieldSiloChoiceRow("fsMaxVehicles", i18n:getText("spu_fs_maxVehicles"), numberTexts(1, ADFieldSiloConfig.MAX_VEHICLES), cfg.maxVehicles)
self:addFieldSiloChoiceRow("fsMinLiters", i18n:getText("spu_fs_minLiters"), literTexts(), indexOf(ADFieldSiloConfig.MIN_LITERS_CHOICES, cfg.minLiters))
self:addToggleRow("fsToProduction", i18n:getText("spu_fs_toProduction"), onOff, cfg.toProduction)
if cfg.toProduction then
self:addFieldSiloChoiceRow("fsReserveDays", i18n:getText("spu_fs_reserveDays"), dayTexts(i18n), indexOf(ADFieldSiloConfig.RESERVE_DAYS_CHOICES, cfg.reserveDays))
end
local plan = ADFieldSilos.lastPlan
local waiting = 0
for _, entry in ipairs(plan ~= nil and plan.status or {}) do if entry.state == "ok" then waiting = waiting + 1 end end
self:addTextRow(string.format(i18n:getText("spu_fs_status"), ADFieldSilos.loanCount(), cfg.maxVehicles, waiting))
if #cfg.storeIds == 0 then self:addTextRow(i18n:getText("spu_fs_noStores")) end
local why = ADFieldSilos.lastVehicleReasons
if waiting > 0 and ADFieldSilos.loanCount() < cfg.maxVehicles and why ~= nil and why.eligible == 0 then
self:addTextRow(string.format(i18n:getText("spu_fs_noVehicle"), why.noPark, why.busy, why.controlled, why.notEnabled))
end
if why ~= nil and (why.harvest or 0) > 0 then
self:addTextRow(string.format(i18n:getText("spu_fs_harvestHeld"), why.harvest))
end
self:addTextRow(i18n:getText("spu_fs_silosTitle"))
if plan == nil or #plan.status == 0 then
self:addTextRow(" " .. i18n:getText("spu_fs_nothing"))
else
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_fs_col_silo"), i18n:getText("spu_col_crop"), i18n:getText("spu_fs_col_liters"),
i18n:getText("spu_fs_col_onWay"), i18n:getText("spu_fs_col_state"), ""})
for _, entry in ipairs(plan.status) do
local desc = g_fillTypeManager:getFillTypeByIndex(entry.fillType)
local title = desc ~= nil and (desc.title or desc.name) or tostring(entry.fillType)
local state = i18n:getText("spu_fs_state_" .. entry.state)
if entry.state == "noFarmStore" then state = string.format(state, title) end
if entry.toProduction then state = state .. " " .. i18n:getText("spu_fs_toFactory") end
self:addCellRow(self.fieldPrefab, {tostring(entry.siloName), title, SmartPickupFrame.formatCompact(entry.liters or 0),
SmartPickupFrame.formatCompact(entry.onWay or 0), state, ""})
end
end
self:addTextRow(i18n:getText("spu_fs_storesTitle"))
local farmId = g_currentMission:getFarmId()
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
if building.kind == "silo" then
local option = self:addToggleRow("fsStore", tostring(building.name), onOff, ADFieldSiloConfig.hasStore(cfg, building.id))
self.rowData[option] = {kind = "fsStore", buildingId = building.id}
end
end
end
function SmartPickupFrame:onClickFieldSiloRow(state, data)
self:safe(function(frame)
local cfg = ADFieldSilos.config
if data.kind == "fsEnabled" then
cfg = ADFieldSiloConfig.with(cfg, {enabled = state == SmartPickupFrame.STATE_ON})
elseif data.kind == "fsMaxVehicles" then
cfg = ADFieldSiloConfig.with(cfg, {maxVehicles = state})
elseif data.kind == "fsMinLiters" then
cfg = ADFieldSiloConfig.with(cfg, {minLiters = ADFieldSiloConfig.MIN_LITERS_CHOICES[state]})
elseif data.kind == "fsToProduction" then
cfg = ADFieldSiloConfig.with(cfg, {toProduction = state == SmartPickupFrame.STATE_ON})
elseif data.kind == "fsReserveDays" then
cfg = ADFieldSiloConfig.with(cfg, {reserveDays = ADFieldSiloConfig.RESERVE_DAYS_CHOICES[state]})
elseif data.kind == "fsStore" then
cfg = ADFieldSiloConfig.toggleStore(cfg, data.buildingId)
end
ADFieldSilos.setConfig(cfg)
pcall(ADFieldSilos.save)
local adEnv = frame.getAdEnv()
if adEnv ~= nil then pcall(ADFieldSilos.planNow, adEnv) end
frame:requestRebuild()
end)
end
+224 -229
View File
@@ -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)
frame.selectedFieldId = fieldId -- klik på [Start]/[Stop]-feltet = handling (kun denne mark); ellers åbn redigeringsvinduet
frame:requestRebuild() if frame.lastClickX ~= nil and actionCell ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
frame:toggleField(fieldId)
else
frame.selectedFieldId = fieldId
frame:openEditPanel("field", fieldId)
end
end) end)
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
+9 -114
View File
@@ -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 ----
@@ -80,8 +80,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 +141,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
+191 -205
View File
@@ -11,17 +11,21 @@ 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
-- 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"}
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 +36,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 +54,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 +86,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 +212,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 +261,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 +286,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 +336,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 +353,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 +366,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 title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title") local first = nil
title:setText(text) for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
return title local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
title:setText(line)
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 +450,33 @@ 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_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 +490,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 +509,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 +603,19 @@ 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 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 +636,17 @@ 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 then
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
self:refreshButtons()
return
end
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW 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 +654,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 +670,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 +691,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 +727,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 +760,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 +792,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 +832,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 +880,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">
+231
View File
@@ -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,329 @@
-- Smart Pickup — fanerne Lagre, vareoversigten (v1.52)
-- Én række pr. vare med samlet beholdning, fyldningsbjælke, værdi og gårdens netto; klik folder varen ud
-- til bygningerne den ligger i. Øverst nøgletal og "kræver handling". Tallene regnes i ADStorageStats;
-- her tegnes kun. Indlæses efter SmartPickupStorageTab.lua (funktionerne lægges på SmartPickupFrame).
SmartPickupFrame.STORAGE_VIEW_ITEMS, SmartPickupFrame.STORAGE_VIEW_BUILDINGS = 1, 2
SmartPickupFrame.MAX_ATTENTION = 4
-- nøgletalsrækken er højere end de andre rækker (tal over label)
SmartPickupFrame.KPI_ROW_SCALE = 1.6
-- ændret struktur (ny vare, flere alarmer) genopbygges højst hvert N. sekund, så fokus ikke hopper
SmartPickupFrame.ITEM_REBUILD_TICKS = 10
SmartPickupFrame.LEVEL_COLORS = {
red = {0.90, 0.25, 0.20, 1},
yellow = {0.95, 0.72, 0.15, 1},
green = {0.30, 0.75, 0.35, 1},
}
SmartPickupFrame.NEUTRAL_BAR_COLOR = {0.35, 0.60, 0.85, 1}
SmartPickupFrame.TEXT_COLOR = {1, 1, 1, 1}
local unpackFn = table.unpack or unpack
-- ---- små tegne-hjælpere ----
local function setTextColor(element, color)
if element ~= nil and element.setTextColor ~= nil then element:setTextColor(unpackFn(color)) end
end
-- Fyld bjælken til brøken af baggrundens bredde; tom bjælke skjules (bredde 0 tegnes som en prik).
function SmartPickupFrame.setBar(bar, background, fraction, color)
if bar == nil or background == nil or background.size == nil then return end
if fraction <= 0 then
bar:setVisible(false)
return
end
bar:setVisible(true)
bar:setSize(background.size[1] * fraction, background.size[2])
if bar.setImageColor ~= nil then bar:setImageColor(nil, unpackFn(color)) end
end
-- Bjælkens farve: rød/gul ved problem, ellers grøn for varer i omløb og blå for rent lager.
function SmartPickupFrame.barColor(level, isActive)
if level ~= nil then return SmartPickupFrame.LEVEL_COLORS[level] end
return isActive and SmartPickupFrame.LEVEL_COLORS.green or SmartPickupFrame.NEUTRAL_BAR_COLOR
end
function SmartPickupFrame:formatDays(days)
return string.format(self.i18n:getText("spu_stat_days"), days)
end
-- "−14.4k/døgn · 3.2 døgn", "+3.6k/døgn · fuld om 2.0 døgn", "+1.2k/døgn · stiger" eller "-"
function SmartPickupFrame:itemRateText(stat)
local fc = SmartPickupFrame.formatCompact
if (stat.net or 0) == 0 then
return (stat.consumption or 0) > 0 and self.i18n:getText("spu_item_balanced") or "-"
end
local rate = string.format(self.i18n:getText("spu_item_perDay"), (stat.net < 0 and "-" or "+") .. fc(math.abs(stat.net)))
if stat.days ~= nil then
return rate .. " · " .. self:formatDays(stat.days)
elseif stat.daysToFull ~= nil then
return rate .. " · " .. string.format(self.i18n:getText("spu_item_fullIn"), self:formatDays(stat.daysToFull))
end
return rate .. " · " .. self.i18n:getText("spu_stat_rising")
end
-- "1.2M/1.5M · 80 %" eller "24 paller · 1 %"
function SmartPickupFrame:itemAmountText(entry)
local fc = SmartPickupFrame.formatCompact
local amount = entry.count ~= nil and string.format(self.i18n:getText("spu_storage_pallets"), entry.count)
or string.format("%s/%s", fc(entry.level or 0), fc(entry.capacity or 0))
if entry.percent ~= nil then amount = amount .. string.format(" · %d %%", entry.percent) end
return amount
end
-- ---- data ----
-- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården.
function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory)
self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen)
local farmId = g_currentMission:getFarmId()
local totals = ADStorageStats.farmTotals(fullInventory)
local capacities = ADStorageStats.farmCapacities(fullInventory)
local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {}
local stats = {}
local titleOf = function(fillType) return self:fillTypeTitle(fillType) end
for _, group in ipairs(ADStorageStats.groupByFillType(inventory, titleOf)) do
local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(group.fillType) or nil
local farm = {level = totals[group.fillType], capacity = capacities[group.fillType]}
table.insert(stats, ADStorageStats.groupStats(group, farm, rates[group.fillType], quote))
end
return stats
end
-- Nøgle der ændrer sig når rækkerne skal være anderledes (varer, udfoldede bygninger, alarmer).
function SmartPickupFrame:itemStructureKey(stats, attention)
local parts = {}
for _, stat in ipairs(stats) do
table.insert(parts, tostring(stat.fillType))
if self:isItemExpanded(stat.fillType) then
-- sorteret: at to bygninger bytter plads i mængde må ikke udløse en genopbygning
local placeables = {}
for _, building in ipairs(stat.buildings) do table.insert(placeables, tostring(building.placeable)) end
table.sort(placeables)
table.insert(parts, table.concat(placeables, ";"))
end
end
table.insert(parts, "a" .. #attention)
return table.concat(parts, ",")
end
function SmartPickupFrame:isItemExpanded(fillType)
local expanded = self.expandedItems ~= nil and self.expandedItems[self.currentTab] or nil
return expanded ~= nil and expanded[fillType] == true
end
function SmartPickupFrame:toggleItem(fillType)
self.expandedItems = self.expandedItems or {}
self.expandedItems[self.currentTab] = self.expandedItems[self.currentTab] or {}
local expanded = self.expandedItems[self.currentTab]
expanded[fillType] = not expanded[fillType] or nil
-- listen hopper ellers til toppen ved genopbygningen
local layout = self.boxLayout
local scrollY = layout ~= nil and layout.firstVisibleY or nil
self:rebuild()
if scrollY ~= nil and layout.scrollTo ~= nil then pcall(layout.scrollTo, layout, scrollY, true) end
end
-- ---- visningsvælger (Varer | Bygninger) ----
function SmartPickupFrame:getStorageView()
local views = self.storageViewByTab or {}
return views[self.currentTab] or SmartPickupFrame.STORAGE_VIEW_ITEMS
end
function SmartPickupFrame:addStorageViewRow()
local row = self:addRow(self.productPrefab, {kind = "storageView"})
local option = row:getDescendantByName("option")
option:setTexts({self.i18n:getText("spu_view_items"), self.i18n:getText("spu_view_buildings")})
option:setState(self:getStorageView(), false)
row:getDescendantByName("title"):setText(self.i18n:getText("spu_view_title"))
local tooltip = option:getDescendantByName("ignore")
if tooltip ~= nil then tooltip:setText(self.i18n:getText("spu_view_tooltip")) end
option.toolTipText = self.i18n:getText("spu_view_tooltip")
end
function SmartPickupFrame:onClickStorageView(state)
self.storageViewByTab = self.storageViewByTab or {}
self.storageViewByTab[self.currentTab] = state
self:requestRebuild()
end
-- ---- opbygning ----
function SmartPickupFrame:kpiTexts(stats)
local kpi = ADStorageStats.kpis(stats)
local i18n = self.i18n
return {
{SmartPickupFrame.formatMoney(kpi.valueNow), i18n:getText("spu_kpi_value")},
{"+" .. SmartPickupFrame.formatMoney(kpi.gain), i18n:getText("spu_kpi_gain")},
{tostring(kpi.emptyCount), i18n:getText("spu_kpi_empty")},
{tostring(kpi.fullCount), i18n:getText("spu_kpi_full")},
}, kpi
end
function SmartPickupFrame:setKpiRow(row, stats)
local texts, kpi = self:kpiTexts(stats)
for index, pair in ipairs(texts) do
local value = row:getDescendantByName("v" .. index)
if value ~= nil then value:setText(pair[1]) end
local label = row:getDescendantByName("l" .. index)
if label ~= nil then label:setText(pair[2]) end
end
local colors = SmartPickupFrame.LEVEL_COLORS
setTextColor(row:getDescendantByName("v2"), kpi.gain > 0 and colors.green or SmartPickupFrame.TEXT_COLOR)
setTextColor(row:getDescendantByName("v3"), kpi.emptyCount > 0 and colors.red or SmartPickupFrame.TEXT_COLOR)
setTextColor(row:getDescendantByName("v4"), kpi.fullCount > 0 and colors.yellow or SmartPickupFrame.TEXT_COLOR)
end
function SmartPickupFrame:addKpiRow(stats)
local row = self:addRow(self.kpiPrefab, {kind = "text"})
if row.size ~= nil then row:setSize(row.size[1], row.size[2] * SmartPickupFrame.KPI_ROW_SCALE) end
self:setKpiRow(row, stats)
return row
end
function SmartPickupFrame:alertText(entry)
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
if entry.kind == "empty" then
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
elseif entry.kind == "full" then
if stat.daysToFull ~= nil then
return string.format(i18n:getText("spu_alert_fullIn"), stat.title, stat.percent or 0, self:formatDays(stat.daysToFull))
end
return string.format(i18n:getText("spu_alert_full"), stat.title, stat.percent or 0)
end
return string.format(i18n:getText("spu_alert_sell"), stat.title, math.floor((stat.priceRatio or 0) * 100 + 0.5), SmartPickupFrame.formatMoney(stat.valueNow))
end
function SmartPickupFrame:setAlertRow(row, entry)
row.entry = entry
local hint = ""
if SmartPickupFrame.attentionAdvice ~= nil then
local ok, advice, info = pcall(self.attentionAdvice, self, entry)
if ok then hint = self:attentionHint(advice, info) end
end
row.c1:setText(self:alertText(entry) .. hint)
if row.dot ~= nil and row.dot.setImageColor ~= nil then
row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green))
end
end
function SmartPickupFrame:itemCells(stat)
local marker = self:isItemExpanded(stat.fillType) and "– " or "+ "
local value = stat.valueNow ~= nil and SmartPickupFrame.formatMoney(stat.valueNow) or self.i18n:getText("spu_stat_noSell")
return {marker .. stat.title, self:itemAmountText(stat), value, self:itemRateText(stat)}
end
function SmartPickupFrame:setItemRow(row, stat)
SmartPickupFrame.setCells(row.cells, self:itemCells(stat))
local isActive = (stat.consumption or 0) > 0 or (stat.production or 0) > 0
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(stat.percent), SmartPickupFrame.barColor(stat.severity, isActive))
local rateColor = SmartPickupFrame.TEXT_COLOR
if stat.emptyLevel ~= nil then
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.emptyLevel]
elseif stat.fullLevel ~= nil then
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.fullLevel]
end
setTextColor(row.cells[4], rateColor)
end
-- Bygningens andel af varen: værdi efter samme pris som varen, bjælke efter bygningens egen fyldning.
function SmartPickupFrame:setSubRow(row, stat, building)
local kind = SmartPickupFrame.getTabKind(self.currentTab)
local isStableOutput = kind == "husbandry" and (stat.production or 0) > 0 and (stat.consumption or 0) == 0
local value = "-"
if stat.valueNow ~= nil and stat.level > 0 then
value = SmartPickupFrame.formatMoney(stat.valueNow * building.level / stat.level)
end
local share = stat.level > 0 and math.floor(100 * building.level / stat.level + 0.5) or 0
SmartPickupFrame.setCells(row.cells, {tostring(building.name), self:itemAmountText(building), value,
string.format(self.i18n:getText("spu_item_share"), share)})
local level = ADStorageStats.buildingLevel(building, kind, isStableOutput)
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(building.percent), SmartPickupFrame.barColor(level, false))
end
-- Klon af item-/sub-prefab med referencer til cellerne og bjælken.
function SmartPickupFrame:addBarRow(prefab)
local row = self:addRow(prefab, {kind = "text"})
local cells = {}
for index = 1, 4 do cells[index] = row:getDescendantByName("c" .. index) end
return {element = row, cells = cells, bar = row:getDescendantByName("bar"), barBg = row:getDescendantByName("barBg")}
end
function SmartPickupFrame:addItemHeader()
local i18n = self.i18n
local header = self:addBarRow(self.itemPrefab)
SmartPickupFrame.setCells(header.cells, {i18n:getText("spu_col_item"), i18n:getText("spu_icol_amount"),
i18n:getText("spu_icol_value"), i18n:getText("spu_icol_rate")})
if header.bar ~= nil then header.bar:setVisible(false) end
if header.barBg ~= nil then header.barBg:setVisible(false) end
end
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
local i18n = self.i18n
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
self:addTextRow(i18n:getText("spu_attention"))
if #attention == 0 then
self:addTextRow(i18n:getText("spu_attention_none"))
end
for _, entry in ipairs(attention) do
local element = self:addRow(self.alertPrefab, {kind = "text"})
local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")}
self:setAlertRow(row, entry)
-- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua)
self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end)
table.insert(self.itemView.alertRows, row)
end
self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint"))
if #stats == 0 then
self:addTextRow(i18n:getText("spu_storage_empty"))
return
end
self:addItemHeader()
for _, stat in ipairs(stats) do
local row = self:addBarRow(self.itemPrefab)
local fillType = stat.fillType
self:makeRowClickable(row.element, function(frame) frame:toggleItem(fillType) end)
self:setItemRow(row, stat)
row.subs = {}
if self:isItemExpanded(fillType) then
for _, building in ipairs(stat.buildings) do
local sub = self:addBarRow(self.subPrefab)
sub.placeable = building.placeable
self:setSubRow(sub, stat, building)
table.insert(row.subs, sub)
end
end
table.insert(self.itemView.itemRows, row)
end
end
-- Hvert sekund: tal og bjælker opdateres på stedet; ny struktur -> genopbygning (højst hvert 10. s).
function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
local view = self.itemView
if view == nil then return end
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
if self:itemStructureKey(stats, attention) ~= view.key then
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
return
end
self:setKpiRow(view.kpiRow, stats)
for index, entry in ipairs(attention) do self:setAlertRow(view.alertRows[index], entry) end
for index, stat in ipairs(stats) do
local row = view.itemRows[index]
self:setItemRow(row, stat)
-- underrækkerne beholder rækkefølgen fra opbygningen; tallene findes via bygningen
local byPlaceable = {}
for _, building in ipairs(stat.buildings) do byPlaceable[building.placeable] = building end
for _, sub in ipairs(row.subs) do
local building = byPlaceable[sub.placeable]
if building ~= nil then self:setSubRow(sub, stat, building) end
end
end
end
@@ -2,42 +2,8 @@
-- Mål, varer (Auto/Vælg), behovsrække, ture og "kør hjem"-tærskel for et forsynings-run. -- 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)
+50
View File
@@ -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>
+286 -14
View File
@@ -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 &apos;Nyt flow&apos;"/>
<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 &quot;X&quot; + markør &quot;Connect X&quot;)"/>
<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 &quot;Connect %s&quot;"/>
<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 &gt; %.0f m (Courseplay)"/> <text name="spu_field_reason_tooWide" text="%s for bred: %.1f m &gt; %.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 &lt; slåbredde %.1f m — sæt Riv til eller brug en bredere"/> <text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; 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å &quot;kun med flueben&quot;, men ingen maskiner har flueben — intet kan sendes ud. Åbn Markflåde og vælg &quot;Brug alle ledige maskiner&quot;."/>
<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,6 +163,7 @@
<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"/>
@@ -116,7 +186,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 +222,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 +240,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. &quot;Efter behov&quot; = 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 &quot;Stop alt&quot; (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 +312,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 +323,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)"/>
@@ -255,12 +351,12 @@
<text name="spu_flow_noVehicle" text="(ingen traktor)"/> <text name="spu_flow_noVehicle" text="(ingen traktor)"/>
<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 +396,181 @@
<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="-&gt; %s"/>
<text name="spu_guide_sum_sowAgain" text="så igen"/>
<text name="spu_guide_sum_rake" text="river"/>
<text name="spu_guide_sum_noRake" text="uden river"/>
<text name="spu_guide_sum_straw_chop" text="halm hakkes"/>
<text name="spu_guide_sum_straw_collect" text="halm samles"/>
<text name="spu_guide_sum_straw_bale" text="halm presses"/>
<text name="spu_guide_sum_auto" text="Automatisk"/>
<text name="spu_guide_sum_lime" text="kalk"/>
<text name="spu_guide_sum_mulch" text="mulch"/>
<text name="spu_guide_sum_roll" text="tromle"/>
<text name="spu_guide_sum_fert" text="gødning"/>
<text name="spu_guide_sum_weed" text="ukrudt"/>
<text name="spu_guide_sum_noCare" text="ingen pleje"/>
<text name="spu_guide_sum_dedicated" text="%d faste"/>
<text name="spu_guide_sum_freeRigs" text="ledige maskiner"/>
<text name="spu_guide_sum_rigs" text="%d ad gangen"/>
<text name="spu_guide_sum_unloaders" text="%d aflæsningsvogne"/>
<text name="spu_guide_sum_fieldSilo" text="Marksilo"/>
<text name="spu_guide_sum_stores" text="%d valgte siloer"/>
<text name="spu_guide_flow_open" text="Opsæt flow"/>
<text name="spu_guide_flow_title" text="— Opsæt flow: %s —"/>
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
<text name="spu_guide_flow_saved" text="Flow '%s' gemt"/>
<text name="spu_guide_flow_start" text="Start flow"/>
<text name="spu_guide_flow_rename" text="Skift navn …"/>
<text name="spu_guide_flow_step_name" text="Navn"/>
<text name="spu_guide_flow_step_direction" text="Retning"/>
<text name="spu_guide_flow_step_from" text="Fra"/>
<text name="spu_guide_flow_step_to" text="Til"/>
<text name="spu_guide_flow_step_goods" text="Varer"/>
<text name="spu_guide_flow_step_store" text="Lager"/>
<text name="spu_guide_flow_step_rules" text="Regler"/>
<text name="spu_guide_flow_step_wait" text="Vent ved"/>
<text name="spu_guide_flow_step_tractors" text="Traktorer"/>
<text name="spu_guide_flow_step_summary" text="Oversigt"/>
<text name="spu_guide_flow_hint_name" text="Flowets navn (traktorerne får navn efter det)."/>
<text name="spu_guide_flow_hint_direction" text="Ind til bygning: fyld stalde/fabrikker. Ud fra bygning: tøm, lagr og sælg."/>
<text name="spu_guide_flow_hint_buildings_in" text="Hvilke stalde/fabrikker skal fyldes? Markøren findes automatisk."/>
<text name="spu_guide_flow_hint_buildings_out" text="Hvor skal der hentes fra? Markøren findes automatisk."/>
<text name="spu_guide_flow_hint_goods_in" text="Automatisk henter hver tur den vare målet mangler mest."/>
<text name="spu_guide_flow_hint_goods_out" text="For hver vare: Lager, Salg, eller Salg ellers lager."/>
<text name="spu_guide_flow_hint_store" text="Hvor skal lagervarerne hen?"/>
<text name="spu_guide_flow_hint_rules_in" text="Antal ture, og hvornår traktoren kører hjem fra Wait."/>
<text name="spu_guide_flow_hint_rules_out" text="Hvornår der sælges, og hvornår der køres til lager."/>
<text name="spu_guide_flow_hint_wait" text="Hvor venter traktoren med et rest-læs?"/>
<text name="spu_guide_flow_hint_tractors" text="Hvilke traktorer kører flowet? Traktorer i andre flows vises ikke."/>
<text name="spu_guide_flow_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
<text name="spu_guide_flow_goods" text="Varer"/>
<text name="spu_guide_flow_goodsAutoChoice" text="Automatisk (det bygningen mangler)"/>
<text name="spu_guide_flow_goodsAutoText" text=" Hver tur hentes den vare målet mangler mest."/>
<text name="spu_guide_flow_noGoodsTo" text=" Vælg først hvor der skal køres til (trinnet Til)"/>
<text name="spu_guide_flow_noProducts" text=" Vælg først hvor der hentes fra (trinnet Fra)"/>
<text name="spu_guide_flow_storeAutoChoice" text="Automatisk (nærmeste med plads)"/>
<text name="spu_guide_flow_storeAutoText" text=" Lagerturene kører til nærmeste lager med plads."/>
<text name="spu_guide_flow_noTractors" text=" Ingen ledige traktorer"/>
<text name="spu_guide_flow_otherFlows" text=" %d traktorer kører i andre flows og vises ikke"/>
<text name="spu_guide_flow_blockedShow" text=" %d traktorer kan ikke bruges her (vognen passer ikke) — klik for at se hvorfor"/>
<text name="spu_guide_flow_blockedHide" text=" %d traktorer kan ikke bruges her — klik for at skjule"/>
<text name="spu_guide_flow_blocked" text=" %d traktorer kan ikke bruges her (vognen passer ikke)"/>
<text name="spu_guide_flow_none" text="ingen valgt"/>
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
<text name="spu_guide_flow_sumRound" text="samlerunde"/>
<text name="spu_guide_flow_sumLoops" text="%d ture"/>
<text name="spu_guide_flow_sumLoopsInf" text="uendelige ture"/>
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
<text name="spu_tab_fieldsilos" text="Marksiloer"/>
<text name="spu_fs_enabled" text="Hent marksiloer hjem automatisk"/>
<text name="spu_fs_maxVehicles" text="Maks. vogne ude"/>
<text name="spu_fs_minLiters" text="Mindste mængde pr. vare"/>
<text name="spu_fs_status" text="%d/%d vogne ude · %d siloer venter"/>
<text name="spu_fs_noStores" text="Vælg mindst ét gårdlager nedenfor — ellers køres intet hjem"/>
<text name="spu_fs_silosTitle" text="Marksiloer med varer"/>
<text name="spu_fs_noVehicle" text="Ingen ledig tømmevogn: %d uden AD-parkeringspunkt, %d optaget, %d med spiller i, %d ikke i flåden (vogne i flows lånes ikke)"/>
<text name="spu_fs_nothing" text="Ingen marksiloer har varer over mindste mængde"/>
<text name="spu_fs_col_silo" text="Silo"/>
<text name="spu_fs_col_liters" text="Liter"/>
<text name="spu_fs_col_onWay" text="På vej"/>
<text name="spu_fs_col_state" text="Status"/>
<text name="spu_fs_state_ok" text="ok"/>
<text name="spu_fs_state_noMarker" text="mangler markør"/>
<text name="spu_fs_state_noFarmStore" text="ingen gård tager %s"/>
<text name="spu_fs_state_paused" text="pause (kom ikke i gang)"/>
<text name="spu_fs_storesTitle" text="Gårdlagre (hvor varerne køres hjem til)"/>
<text name="spu_fs_toProduction" text="Kør overskud til produktion (foder til staldene først)"/>
<text name="spu_fs_reserveDays" text="Foderreserve: staldenes forbrug i"/>
<text name="spu_fs_days" text="%d døgn"/>
<text name="spu_fs_toFactory" text="→ fabrik"/>
<text name="spu_fs_harvestHeld" text="%d tømmevogn(e) holdes tilbage til høstere der arbejder"/>
<text name="spu_view_title" text="Vis"/>
<text name="spu_view_items" text="Varer"/>
<text name="spu_view_buildings" text="Bygninger"/>
<text name="spu_view_tooltip" text="Varer: samlet beholdning pr. vare (klik for at se hvor den ligger). Bygninger: tabel for én bygning."/>
<text name="spu_kpi_value" text="Lagerværdi nu"/>
<text name="spu_kpi_gain" text="Ekstra ved toppris"/>
<text name="spu_kpi_empty" text="Løber tør snart"/>
<text name="spu_kpi_full" text="Næsten fuldt"/>
<text name="spu_attention" text="— Kræver handling —"/>
<text name="spu_attention_none" text="Intet kræver handling lige nu"/>
<text name="spu_attention_hintFlow" text=" → klik: opret flow"/>
<text name="spu_attention_hintOpen" text=" → klik: åbn flow '%s'"/>
<text name="spu_attention_hintProduce" text=" → producér/køb"/>
<text name="spu_attention_produce" text="Et flow hjælper ikke — gården har kun %2$s %1$s i alt. Producér eller køb mere."/>
<text name="spu_attention_flowName" text="%s til %s"/>
<text name="spu_attention_hintSellFlow" text=" → klik: opret salgsflow"/>
<text name="spu_attention_sellFlowName" text="Sælg %s"/>
<text name="spu_alert_empty" text="%s løber tør om %s — gården har %s, bruger %s/døgn"/>
<text name="spu_alert_fullIn" text="%s er %d %% fuld — fuld om %s"/>
<text name="spu_alert_full" text="%s er %d %% fuld"/>
<text name="spu_alert_sell" text="Sælg %s nu: prisen er %d %% af årets top — %s"/>
<text name="spu_items_hint" text="(klik på en vare)"/>
<text name="spu_icol_amount" text="Beholdning"/>
<text name="spu_icol_value" text="Værdi nu"/>
<text name="spu_icol_rate" text="Gården netto · rækker"/>
<text name="spu_item_perDay" text="%s/døgn"/>
<text name="spu_item_fullIn" text="fuld om %s"/>
<text name="spu_item_balanced" text="i balance"/>
<text name="spu_item_share" text="%d %% af varen"/>
</texts> </texts>
</l10n> </l10n>
+286 -14
View File
@@ -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 &apos;New flow&apos;"/>
<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 &quot;X&quot; + marker &quot;Connect X&quot;)"/>
<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 &quot;Connect %s&quot;"/>
<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 &gt; %.0f m (Courseplay)"/> <text name="spu_field_reason_tooWide" text="%s too wide: %.1f m &gt; %.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 &lt; mowing width %.1f m — turn on Rake or use a wider one"/> <text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; 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 &quot;only ticked&quot;, but no machine is ticked — nothing can be sent out. Open Field fleet and choose &quot;Use all free machines&quot;."/>
<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,6 +163,7 @@
<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"/>
@@ -116,7 +186,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 +222,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 +240,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. &quot;By need&quot; = 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 &quot;Stop all&quot; (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 +312,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 +323,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)"/>
@@ -255,12 +351,12 @@
<text name="spu_flow_noVehicle" text="(no tractor)"/> <text name="spu_flow_noVehicle" text="(no tractor)"/>
<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 +396,181 @@
<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="-&gt; %s"/>
<text name="spu_guide_sum_sowAgain" text="sow again"/>
<text name="spu_guide_sum_rake" text="rake"/>
<text name="spu_guide_sum_noRake" text="no rake"/>
<text name="spu_guide_sum_straw_chop" text="straw chopped"/>
<text name="spu_guide_sum_straw_collect" text="straw collected"/>
<text name="spu_guide_sum_straw_bale" text="straw baled"/>
<text name="spu_guide_sum_auto" text="Automatic"/>
<text name="spu_guide_sum_lime" text="lime"/>
<text name="spu_guide_sum_mulch" text="mulch"/>
<text name="spu_guide_sum_roll" text="roll"/>
<text name="spu_guide_sum_fert" text="fertilizer"/>
<text name="spu_guide_sum_weed" text="weeds"/>
<text name="spu_guide_sum_noCare" text="no care"/>
<text name="spu_guide_sum_dedicated" text="%d dedicated"/>
<text name="spu_guide_sum_freeRigs" text="free machines"/>
<text name="spu_guide_sum_rigs" text="%d at a time"/>
<text name="spu_guide_sum_unloaders" text="%d unloaders"/>
<text name="spu_guide_sum_fieldSilo" text="Field silo"/>
<text name="spu_guide_sum_stores" text="%d chosen silos"/>
<text name="spu_guide_flow_open" text="Set up flow"/>
<text name="spu_guide_flow_title" text="— Set up flow: %s —"/>
<text name="spu_guide_flow_section" text="— Flow: %s —"/>
<text name="spu_guide_flow_saved" text="Flow '%s' saved"/>
<text name="spu_guide_flow_start" text="Start flow"/>
<text name="spu_guide_flow_rename" text="Change name …"/>
<text name="spu_guide_flow_step_name" text="Name"/>
<text name="spu_guide_flow_step_direction" text="Direction"/>
<text name="spu_guide_flow_step_from" text="From"/>
<text name="spu_guide_flow_step_to" text="To"/>
<text name="spu_guide_flow_step_goods" text="Goods"/>
<text name="spu_guide_flow_step_store" text="Storage"/>
<text name="spu_guide_flow_step_rules" text="Rules"/>
<text name="spu_guide_flow_step_wait" text="Wait at"/>
<text name="spu_guide_flow_step_tractors" text="Tractors"/>
<text name="spu_guide_flow_step_summary" text="Summary"/>
<text name="spu_guide_flow_hint_name" text="The flow's name (tractors are named after it)."/>
<text name="spu_guide_flow_hint_direction" text="Into building: keep barns/factories filled. Out of building: empty, store and sell."/>
<text name="spu_guide_flow_hint_buildings_in" text="Which barns/factories should be filled? The marker is found automatically."/>
<text name="spu_guide_flow_hint_buildings_out" text="Where should goods be picked up? The marker is found automatically."/>
<text name="spu_guide_flow_hint_goods_in" text="Automatic fetches whatever the target lacks most on each trip."/>
<text name="spu_guide_flow_hint_goods_out" text="For each good: Store, Sell, or Sell else store."/>
<text name="spu_guide_flow_hint_store" text="Where should stored goods go?"/>
<text name="spu_guide_flow_hint_rules_in" text="Number of trips, and when the tractor leaves Wait."/>
<text name="spu_guide_flow_hint_rules_out" text="When to sell, and when to drive to storage."/>
<text name="spu_guide_flow_hint_wait" text="Where does the tractor wait with a leftover load?"/>
<text name="spu_guide_flow_hint_tractors" text="Which tractors run this flow? Tractors in other flows are hidden."/>
<text name="spu_guide_flow_hint_summary" text="Check your choices. Click a line to change it."/>
<text name="spu_guide_flow_goods" text="Goods"/>
<text name="spu_guide_flow_goodsAutoChoice" text="Automatic (what the building needs)"/>
<text name="spu_guide_flow_goodsAutoText" text=" Each trip fetches the good the target lacks most."/>
<text name="spu_guide_flow_noGoodsTo" text=" First choose where to deliver (step To)"/>
<text name="spu_guide_flow_noProducts" text=" First choose where to pick up (step From)"/>
<text name="spu_guide_flow_storeAutoChoice" text="Automatic (nearest with room)"/>
<text name="spu_guide_flow_storeAutoText" text=" Storage trips go to the nearest store with room."/>
<text name="spu_guide_flow_noTractors" text=" No free tractors"/>
<text name="spu_guide_flow_otherFlows" text=" %d tractors run in other flows and are hidden"/>
<text name="spu_guide_flow_blockedShow" text=" %d tractors cannot be used here (trailer does not fit) — click to see why"/>
<text name="spu_guide_flow_blockedHide" text=" %d tractors cannot be used here — click to hide"/>
<text name="spu_guide_flow_blocked" text=" %d tractors cannot be used here (trailer does not fit)"/>
<text name="spu_guide_flow_none" text="none chosen"/>
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
<text name="spu_guide_flow_sumRound" text="collection round"/>
<text name="spu_guide_flow_sumLoops" text="%d trips"/>
<text name="spu_guide_flow_sumLoopsInf" text="endless trips"/>
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
<text name="spu_guide_flow_sumNoTractors" text="none"/>
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
<text name="spu_tab_fieldsilos" text="Field silos"/>
<text name="spu_fs_enabled" text="Bring field silos home automatically"/>
<text name="spu_fs_maxVehicles" text="Max. vehicles out"/>
<text name="spu_fs_minLiters" text="Minimum amount per product"/>
<text name="spu_fs_status" text="%d/%d vehicles out · %d silos waiting"/>
<text name="spu_fs_noStores" text="Select at least one farm storage below — otherwise nothing is brought home"/>
<text name="spu_fs_silosTitle" text="Field silos with products"/>
<text name="spu_fs_noVehicle" text="No free unloading trailer: %d without AD park destination, %d busy, %d with a player in it, %d not in the fleet (flow vehicles are never borrowed)"/>
<text name="spu_fs_nothing" text="No field silo has products above the minimum amount"/>
<text name="spu_fs_col_silo" text="Silo"/>
<text name="spu_fs_col_liters" text="Liters"/>
<text name="spu_fs_col_onWay" text="En route"/>
<text name="spu_fs_col_state" text="Status"/>
<text name="spu_fs_state_ok" text="ok"/>
<text name="spu_fs_state_noMarker" text="missing marker"/>
<text name="spu_fs_state_noFarmStore" text="no farm storage takes %s"/>
<text name="spu_fs_state_paused" text="paused (did not get going)"/>
<text name="spu_fs_storesTitle" text="Farm storages (where products are brought home to)"/>
<text name="spu_fs_toProduction" text="Send surplus to production (barn feed first)"/>
<text name="spu_fs_reserveDays" text="Feed reserve: barn consumption for"/>
<text name="spu_fs_days" text="%d days"/>
<text name="spu_fs_toFactory" text="→ factory"/>
<text name="spu_fs_harvestHeld" text="%d unloading trailer(s) held back for working harvesters"/>
<text name="spu_view_title" text="Show"/>
<text name="spu_view_items" text="Goods"/>
<text name="spu_view_buildings" text="Buildings"/>
<text name="spu_view_tooltip" text="Goods: total stock per good (click to see where it is stored). Buildings: table for one building."/>
<text name="spu_kpi_value" text="Stock value now"/>
<text name="spu_kpi_gain" text="Extra at top price"/>
<text name="spu_kpi_empty" text="Running out soon"/>
<text name="spu_kpi_full" text="Almost full"/>
<text name="spu_attention" text="— Needs attention —"/>
<text name="spu_attention_none" text="Nothing needs attention right now"/>
<text name="spu_attention_hintFlow" text=" → click: create flow"/>
<text name="spu_attention_hintOpen" text=" → click: open flow '%s'"/>
<text name="spu_attention_hintProduce" text=" → produce/buy"/>
<text name="spu_attention_produce" text="A flow won't help — the farm only has %2$s %1$s in total. Produce or buy more."/>
<text name="spu_attention_flowName" text="%s to %s"/>
<text name="spu_attention_hintSellFlow" text=" → click: create sell flow"/>
<text name="spu_attention_sellFlowName" text="Sell %s"/>
<text name="spu_alert_empty" text="%s runs out in %s — farm has %s, uses %s/day"/>
<text name="spu_alert_fullIn" text="%s is %d %% full — full in %s"/>
<text name="spu_alert_full" text="%s is %d %% full"/>
<text name="spu_alert_sell" text="Sell %s now: price is %d %% of the yearly top — %s"/>
<text name="spu_items_hint" text="(click a good)"/>
<text name="spu_icol_amount" text="Stock"/>
<text name="spu_icol_value" text="Value now"/>
<text name="spu_icol_rate" text="Farm net · lasts"/>
<text name="spu_item_perDay" text="%s/day"/>
<text name="spu_item_fullIn" text="full in %s"/>
<text name="spu_item_balanced" text="balanced"/>
<text name="spu_item_share" text="%d %% of the good"/>
</texts> </texts>
</l10n> </l10n>
+21 -1
View File
@@ -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.60.10.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,6 +26,8 @@
<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"/>
@@ -29,11 +35,25 @@
<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="adSmartPickup.lua"/> <sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles> </extraSourceFiles>
</modDesc> </modDesc>
+2
View File
@@ -2,6 +2,8 @@
**Brugermanual for spillere:** [dansk](docs/BRUGERMANUAL.md) · [English](docs/USER_MANUAL.md). Resten af denne fil er udviklernoter pr. version. **Brugermanual for spillere:** [dansk](docs/BRUGERMANUAL.md) · [English](docs/USER_MANUAL.md). Resten af denne fil er udviklernoter pr. version.
**Download / installation:** hent `FS25_ADSmartPickup.zip` under [Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases) og læg zip-filen (ikke udpakket) i `Documents/My Games/FarmingSimulator2025/mods/`. Kræver **AutoDrive** (og gerne Courseplay til markarbejde). Fanen "Smart Pickup" ligger i ESC-menuen. Fejl og idéer: opret et issue her i repoet.
AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Deliver: AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Deliver:
## 1. Vare-bevidst mappe-rotation (én vare valgt) ## 1. Vare-bevidst mappe-rotation (én vare valgt)
+10
View File
@@ -0,0 +1,10 @@
# Simuleringer
`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner
(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode.
luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode
git worktree add /tmp/old <commit>; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign
Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning.
luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms.
+103
View File
@@ -0,0 +1,103 @@
-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua <mappe med modulerne>)
-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3
-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode
-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame.
-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller.
local dir = arg[1] or "FS25_ADSmartPickup"
math.randomseed(42)
local function noop() end
Logging = {info = noop, warning = noop}
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
function entityExists() return true end
local nodes = {}
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
local simTime = 0
g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}}
function netGetTime() return os.clock() * 1000 end
addModEventListener = noop
Utils = {prependedFunction = function(a) return a end}
-- vejnet og markører
local wayPoints, markers = {}, {}
for id = 1, 13550 do
local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600
wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}}
end
for index = 1, 600 do
local id = index * 22
table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"})
end
-- stationer ved hver 5. markør, 3 triggere hver
local stations = {}
for index = 1, 120 do
local wp = wayPoints[markers[150 + index].id]
local triggers = {}
for t = 1, 3 do
local node = #nodes + 1
nodes[node] = {wp.x + t, wp.z + t}
table.insert(triggers, {exactFillRootNode = node})
end
table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index,
isa = function() return false end, getIsFillTypeAllowed = function() return true end})
end
g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end}
g_currentMission.placeableSystem = {placeables = {}}
-- køretøjer
local vehicles = {}
for index = 1, 40 do
local node = #nodes + 1
nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)}
table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end})
end
g_currentMission.vehicleSystem = {vehicles = vehicles}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return markers end,
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getPathTo = function(_, _, id) return {{id = id}} end,
}}
for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do
local chunk = loadfile(dir .. "/" .. file)
if chunk ~= nil then pcall(chunk) end
end
if ADCache ~= nil then ADCache.enabled = true end
ADPrices = ADPrices or {}
ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end
-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager)
local function replan(vehicle)
ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end)
for _, marker in ipairs(markers) do
ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1)
end
end
local t0 = os.clock()
replan(vehicles[1])
local single = (os.clock() - t0) * 1000
-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det.
local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS)
local timers = {}
for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end
local worst, steady, frames = 0, 0, 0
for frame = 1, 60 * 60 do
simTime = simTime + 16.7
g_currentMission.time = simTime
local started = os.clock()
for index = 1, 15 do
timers[index] = timers[index] + 16.7
if timers[index] >= 5000 then
timers[index] = 0
replan(vehicles[index])
end
end
local spent = (os.clock() - started) * 1000
if spent > worst then worst = spent end
if simTime > 12000 and spent > steady then steady = spent end
if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end
frames = frames + 1
end
print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " "))
print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady))
+76 -29
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — brugermanual # AD Smart Pickup — brugermanual
Version 1.27.0 · Farming Simulator 25 · tillæg til AutoDrive Version 1.28.0 · Farming Simulator 25 · tillæg til AutoDrive
*English version: [USER_MANUAL.md](USER_MANUAL.md)* *English version: [USER_MANUAL.md](USER_MANUAL.md)*
@@ -159,12 +159,17 @@ samme rækkefølge.
### Knapper ### Knapper
Fanen **Flows** viser tavlen (én linje pr. flow) og under den det valgte flows **resumé**: Navn, Retning,
Fra/Til, Varer, (Lager), Regler, Vent ved, Traktorer og Status. **Klik på et flow** i tavlen for at vælge det,
og **klik på en resumé-linje** for at rette netop det trin (**Gem** gemmer og går tilbage). Klik på
**Navn** for at omdøbe flowet.
| Knap | Funktion | | Knap | Funktion |
|---|---| |---|---|
| **Nyt run** | Opretter et nyt flow. Det starter som *Ind til bygning* | | **Start flow / Stop flow** | Starter eller stopper alle traktorer i flowet |
| **Omdøb** | Giver det valgte flow et nyt navn | | **Opsæt flow** | Guiden for det valgte flow med de nuværende valg |
| **Slet run** | Sletter flowet og stopper dets kørende traktorer. Kan ikke fortrydes | | **Nyt flow** | Guiden for et nyt flow. Intet oprettes før du trykker **Start flow** eller **Gem uden at starte** |
| **Start run / Stop run** | Starter eller stopper alle traktorer i flowet | | **Slet flow** | Sletter flowet og stopper dets kørende traktorer. Kan ikke fortrydes |
På fanen **Overblik** er knapperne i stedet: På fanen **Overblik** er knapperne i stedet:
@@ -172,12 +177,30 @@ På fanen **Overblik** er knapperne i stedet:
|---|---| |---|---|
| **Start alle flows** | Starter alle flows med traktorer. Flows, der allerede kører, røres ikke, så ingen tur afbrydes | | **Start alle flows** | Starter alle flows med traktorer. Flows, der allerede kører, røres ikke, så ingen tur afbrydes |
| **Stop alle flows** | Stopper alle aktive traktorer i alle flows | | **Stop alle flows** | Stopper alle aktive traktorer i alle flows |
| **Nyt run** | Opretter et nyt flow og hopper til fanen **Flows** | | **Nyt flow** | Hopper til fanen **Flows** og åbner guiden for et nyt flow |
Statuslinjen viser bagefter, hvor mange flows og traktorer der blev startet, og op til tre grunde til Statuslinjen viser bagefter, hvor mange flows og traktorer der blev startet, og op til tre grunde til
traktorer, der blev sprunget over. Alle grunde står i `log.txt`. traktorer, der blev sprunget over. Alle grunde står i `log.txt`.
Vælg flow i feltet **Run** øverst. Under tavlen står detaljerne for det valgte flow. ### Guiden "Opsæt flow"
Samme trin-skærm og knapper som markguiden (**Næste**, **Tilbage**, **Annullér**; ét spørgsmål ad gangen,
trin der ikke passer springes over):
| Trin | Vises når | Valg |
|---|---|---|
| **Navn** | nyt flow | *Skift navn …* åbner tekstfeltet |
| **Retning** | altid | *Ind til bygning* (forsyning) / *Ud fra bygning* (udkørsel) |
| **Til** / **Fra** | altid | Bygningerne (se *Bygningslisten* nedenfor) |
| **Varer** | altid | Forsyning: *Automatisk (det bygningen mangler)* eller *Vælg selv*. Udkørsel: pr. vare Fra / Lager / Salg / Salg, ellers lager |
| **Lager** | udkørsel med en vare på Lager | *Automatisk (nærmeste med plads)* eller *Vælg selv* blandt lagrene |
| **Regler** | altid | Udkørsel: sælg fra %, lager over %, saml op på én tur. Forsyning: ture, kør hjem fra Wait under % |
| **Vent ved** | altid | Wait-puljen |
| **Traktorer** | altid | Ledige traktorer og flowets egne. Traktorer i andre flows (og dem hvis vogn ikke passer) er skjult med en linje |
| **Oversigt** | altid | Resuméet; klik på en linje for at rette. **Start flow** eller **Gem uden at starte** |
Moden spørger aldrig om markøren ved bygningen, rutetjek, bedste foder først, læs-bytte, kildevalg,
fordeling på flere mål, hvile af tomme kilder eller traktornavne.
### Retning ### Retning
@@ -207,7 +230,7 @@ Et forsyningsflow holder én eller flere bygninger fyldt med alt det, de bruger.
### Opsætning ### Opsætning
1. **Nyt run**, og lad Retning stå på **Ind til bygning**. 1. **Nyt flow** (guiden), og lad Retning stå på **Ind til bygning**.
2. Under **Til**: sæt Ja ud for de stalde og fabrikker, der skal fyldes. Listen viser også siloer, 2. Under **Til**: sæt Ja ud for de stalde og fabrikker, der skal fyldes. Listen viser også siloer,
der kun kan fyldes og ikke tømmes, fx Pumps & Hoses' drive-on-bunkere (DOB 115), der fodrer en der kun kan fyldes og ikke tømmes, fx Pumps & Hoses' drive-on-bunkere (DOB 115), der fodrer en
biogas-fermentor. For dem er **Vælg** med de varer, fermentoren skal have, bedst. biogas-fermentor. For dem er **Vælg** med de varer, fermentoren skal have, bedst.
@@ -540,7 +563,7 @@ maskiner. Kræver **Courseplay** ud over AutoDrive.
### Vælg og glem: afgrøde på marken ### Vælg og glem: afgrøde på marken
Det nemmeste: vælg **Afgrøde på marken** (fx *Hvede*) øverst under marken. Så passer moden resten: Det nemmeste: vælg en afgrøde (fx *Hvede*) i guidens trin **Afgrøde** (Opsæt mark). Så passer moden resten:
- **Vokser der noget nu**, får det lov at vokse færdig, bliver passet (gødning, ukrudt, kalk) og høstet - **Vokser der noget nu**, får det lov at vokse færdig, bliver passet (gødning, ukrudt, kalk) og høstet
eller slået, når det er klart. eller slået, når det er klart.
@@ -550,9 +573,8 @@ Det nemmeste: vælg **Afgrøde på marken** (fx *Hvede*) øverst under marken. S
- **Bar mark:** klargøres og tilsås, så snart det er såtid. - **Bar mark:** klargøres og tilsås, så snart det er såtid.
- Derefter kører cyklussen af sig selv: vækst → pleje → høst → klargøring → såning → … - Derefter kører cyklussen af sig selv: vækst → pleje → høst → klargøring → såning → …
Med en afgrøde valgt kører marken automatisk. Linjen under valget fortæller, hvad moden vil gøre. Du kan Med en afgrøde valgt kører marken automatisk. Du kan stadig vælge ensilage/hø, halm, antal maskiner,
stadig vælge ensilage/hø, halm, antal maskiner, jordbehandling og pleje. Vælg *Ingen — planlæg selv* for jordbehandling og pleje i de næste trin. Vælg *Behold det der gror* for at styre marken som før.
at styre marken manuelt som før.
### Opsætning ### Opsætning
@@ -560,7 +582,7 @@ at styre marken manuelt som før.
`F109`. Maskinerne kører dertil ad vejen, før Courseplay tager over. Højst 400 m fra marken. Mangler `F109`. Maskinerne kører dertil ad vejen, før Courseplay tager over. Højst 400 m fra marken. Mangler
den, står marken med `!` og sendes ikke ud. den, står marken med `!` og sendes ikke ud.
2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig på. 2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig på.
3. **Markflåde.** Nederst på fanen står alle køretøjer med et markredskab, med rolle og bredde. Med 3. **Markflåde.** Knappen **Markflåde** viser alle køretøjer med et markredskab, med rolle og bredde. Med
**Brug alle ledige maskiner** = Ja (standard) finder moden selv maskinerne; med Nej bruges kun dem med **Brug alle ledige maskiner** = Ja (standard) finder moden selv maskinerne; med Nej bruges kun dem med
flueben. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører, flueben. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører,
og køretøjer du selv kører, bruges aldrig. En sprøjte/spreder har først en rolle, når den har fyld på og køretøjer du selv kører, bruges aldrig. En sprøjte/spreder har først en rolle, når den har fyld på
@@ -579,25 +601,50 @@ at styre marken manuelt som før.
### En mark ### En mark
Klik på en mark i tabellen. Under tabellen står: Klik på en mark i tabellen. Under tabellen står markens **resumé** — én linje pr. emne:
| Valg | Betydning | | Linje | Eksempel |
|---|---| |---|---|
| **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: den starter selv, når den er klar eller trænger til pleje | | **Afgrøde** | *Behold det der gror* / *-> Hvede* |
| **Maskiner pr. trin** | 1–5 sæt samtidig (Courseplays multi-tool). Et trin starter først når alle sæt er ledige | | **Afgrødens valg** | *Ensilage, river* / *halm hakkes* |
| **Plan** (slåafgrøde) | Ensilage eller Hø | | **Klargøring** | *Automatisk* / *Pløj, kalk* |
| **Riv** (slåafgrøde) | Riv skårene sammen før opsamling | | **Pleje** | *gødning, ukrudt* |
| **Aflæsningsvogne** (høst) | 1–3 vogne der følger høsteren og kører til lager | | **Maskiner** | *3 faste* / *ledige maskiner*, *1 ad gangen*, *1 aflæsningsvogne* |
| **Halm efter høst** (høst) | Hakkes (spredes), Samles op til silo, eller Presses til baller, som en ballesamler kører til et ballelager | | **Lager** | *Marksilo* / *Automatisk* / en valgt silo |
| **Stubbehakning** | Hak stubben efter høst |
| **Jordbehandling** | Auto (pløj når jorden trænger, ellers kultivér), Kultivér, Pløj eller Ingen |
| **Så bagefter** + **Afgrøde at så** | Så efter jordbehandlingen. Standard: samme afgrøde som blev høstet |
| **Tromle** | Tromle efter såning |
| **Gød / Kalk / Bekæmp ukrudt** | Pleje mens afgrøden vokser, når jorden trænger (højst to gange pr. pleje pr. vækst) |
| **Tving lager** | Vælg lagre. Uden valg: ensilage → nærmeste gæringssilo, ellers silo; hø og høst → nærmeste silo |
Under valgene står **Jorden trænger til** (kalk, pløjning, gødning, ukrudt), markøren og evt. slåbredde og **Klik på en linje** for at rette netop det (fx lageret): kun det ene trin åbnes, og **Gem** gemmer og går
skår-forskydning. tilbage. Under resuméet står hele statusteksten og markøren.
Bundknapperne: **Start nu / Stop mark**, **Opsæt mark** og **Markflåde**.
### Guiden "Opsæt mark"
**Opsæt mark** sætter marken op med ét spørgsmål ad gangen ("Trin 3 af 7 — …"). Trin, der ikke passer
til marken, springes over; en opsat mark åbner med de nuværende valg. Uden valgt mark vælger du først marken.
| Trin | Vises når | Valg |
|---|---|---|
| **Afgrøde** | altid | *Behold det der gror* eller en afgrøde at så (mål-afgrøde, se ovenfor). Høst-/bar mark: *Så samme afgrøde igen efter høst* |
| **Afgrødens valg** | slåafgrøde (nu eller som mål): Ensilage/Hø + Riv. Høstafgrøde med halm: Halm hakkes/samles/presses | |
| **Klargøring** | efter høst / bar mark / skift af afgrøde | *Automatisk (anbefalet)* eller *Vælg selv*: jordbehandling, kalk, stubhakning, tromle |
| **Pleje** | afgrøde der gror | Gødning, ukrudt (ikke på slåafgrøder); kalk her, når der ingen klargøring er |
| **Maskiner** | altid | Antal ad gangen (1–5), aflæsningsvogne ved høst (1–3), **Faste maskiner til marken?** + listen |
| **Lager** | høst/slåning | *Brug marksiloen?* (når der er en), ellers *Automatisk* eller *Vælg selv* blandt de 8 nærmeste |
| **Oversigt** | altid | Resuméet; klik på en linje for at rette. **Kør automatisk** (Auto) eller **Gem uden at starte** (Manuel) |
Knapperne: **Næste**, **Tilbage**, **Annullér**. Intet gemmes før **Kør automatisk** / **Gem uden at starte**
(eller **Gem** ved ret af én linje). Lukkes menuen, eller skifter du fane, kasseres guiden. Med en
mål-afgrøde kører marken altid automatisk. Mangler markøren, kan marken sættes op; arbejdet venter på markøren.
**Faste maskiner:** en fast maskine bruges kun på denne mark, og marken venter på sin egen hellere end at
låne en anden. Listen viser kun ledige maskiner og dem der allerede er faste her; maskiner der er faste på
andre marker står som "N maskiner er faste på andre marker (F22)".
**Markflåde** (knap): den fælles markflåde — *Brug alle ledige maskiner* og listen med flueben, "fast på
F22", "i gang på F108" og bredde-advarsler. **Til marker** går tilbage.
Moden spørger aldrig om: scanning, hvornår der høstes/slås/gødes/sprøjtes, påfyldning, tømning ved forkert
vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter og gæringssilo ved ensilage.
En mark er **klar**, når mindst 90 % står i høstvinduet. **Start nu** starter hele kæden (eller pleje, hvis En mark er **klar**, når mindst 90 % står i høstvinduet. **Start nu** starter hele kæden (eller pleje, hvis
marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne. marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne.
+82 -2
View File
@@ -1,4 +1,4 @@
# Testplan — Markarbejde (fanen Marker), version 1.27.0.0 # Testplan — Markarbejde (fanen Marker), version 1.28.0.0 (+ guider v1.31/v1.32)
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den. 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 Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
@@ -11,7 +11,7 @@ samme rækkefølge.
## 0. Forberedelse ## 0. Forberedelse
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.0.0) FS25_ADSmartPickup`. - [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.28.0.0) FS25_ADSmartPickup`.
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …" - [ ] **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). 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". Afgrøde-kolonnen viser afgrøden eller "bar jord".
@@ -119,6 +119,12 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke. (stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder. - [ ] **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. - [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
- [ ] **6.6 Tom sprøjte/spreder (v1.29.2).** Tom sættes i gang → log `er tom — fyldes med X ved <markør> og
kører så til F<nr>` → AutoDrive (LOAD) fylder op og kører til marken → Courseplay starter med fyld.
Ingen påfyldning med AD-markør → sendes IKKE ud; marken viser "er tom for X … fyld den op".
Sikkerhedsnet: Courseplay startes aldrig med tom tank ("er stadig tom … ved marken").
- [ ] **6.7 Forkert vare (v1.28.8).** Spreder med kalk sendes til gødskning kun hvis ingen tom/gødet spreder
findes → log `tømt for N l LIME (skal bruges til fertilizer)` → fyldes med gødning.
## 7. Multi-maskine (Courseplay multi-tool) ## 7. Multi-maskine (Courseplay multi-tool)
@@ -158,6 +164,73 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
- [ ] **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.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). - [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
## 10. Guiden "Opsæt mark" (v1.31)
- [ ] **10.1 Oversigt.** Klik F22 i tabellen. Forvent: resumé-linjer (Afgrøde, …, Lager) med "[Ret]", hele
status, markør. Ingen lang detaljeliste og ingen markflåde nederst. Knapper: Start nu, Opsæt mark, Markflåde.
- [ ] **10.2 Guiden fra bunden.** Opsæt mark → "Trin 1 af N — Afgrøde". Klik Næste gennem alle trin uden at
scrolle. Tællingen passer med de viste trin (græs: ingen Klargøring; bar mark: ingen Pleje/Lager).
- [ ] **10.3 Valg gemmes.** Vælg fx Hø + Riv, 2 ad gangen, Lager: Vælg selv + en silo → Oversigt viser valgene
→ **Kør automatisk**. Forvent: tabellens Plan "Hø + riv (x2)", Kørsel "Auto". Log: `F22 guide gemt: mode=auto …`.
- [ ] **10.4 Annullér.** Åbn guiden, skift afgrøde, Annullér → intet ændret. Samme med ESC (menuen lukkes).
- [ ] **10.5 Ret én linje.** Klik "Lager: …" i oversigten → "Ret: Lager"; skift → **Gem** → tilbage i
oversigten med ændringen; Kørsel (Auto/Manuel) uændret. Tilbage i et Ret-trin fortryder.
- [ ] **10.6 Ret fra Oversigt-trinnet.** I guidens sidste trin: klik en linje → ret → **OK** → tilbage på Oversigt.
- [ ] **10.7 Mål-afgrøde skifter trin.** Kornmark: vælg Græs som afgrøde → trinnet "Afgrødens valg" viser
Ensilage/Hø; tællingen ændres. På Oversigt kun **Kør automatisk** (ingen "Gem uden at starte").
- [ ] **10.8 Faste maskiner.** Maskiner → Faste = Ja → sæt flueben ved 2 maskiner → gem. Åbn en anden mark:
de 2 vises ikke; linje "2 maskiner er faste på andre marker (F22)". Log: `guide gemt … faste=…`.
- [ ] **10.9 Marksilo.** Mark med silo inden for 150 m: Lager-trinnet spørger "Brug marksiloen?". Ja → resumé "Marksilo".
- [ ] **10.10 Uden valgt mark.** Ingen mark valgt → Opsæt mark → trin "Mark" med listen; klik en mark → Afgrøde.
Næste uden valgt mark giver "Vælg en mark først".
- [ ] **10.11 Markflåde.** Knappen Markflåde viser flåden (flueben, "fast på F22"); klik et flueben gemmes;
**Til marker** går tilbage.
- [ ] **10.12 Klik routes.** Alle Ja/Nej, valg og flueben i guiden reagerer (ingen "GUI-fejl" i statuslinjen/log).
## 11. Guiden "Opsæt flow" (v1.32)
- [ ] **11.1 Flows-fanen.** Tavlen øverst; valgt flow (">") har resumé-linjer med "[Ret]" og Status. Ingen
lang detaljeliste og ingen faste felter (Run, Wait-pulje …). Knapper: Start flow, Opsæt flow, Nyt flow, Slet flow.
- [ ] **11.2 Nyt flow.** Nyt flow → "Trin 1 af 8 — Navn" → *Skift navn …* → skriv navn → Næste gennem Retning
(Ud fra bygning), Fra (en fabrik), Varer (sæt en vare på Lager → trinnet Lager dukker op, tællingen bliver 9),
Regler, Vent ved, Traktorer (flueben) → Oversigt → **Start flow**. Forvent: flowet findes, kører, log
`run '…': …` med kind/products/storeBuildingIds.
- [ ] **11.3 Annullér nyt flow.** Nyt flow → Annullér (eller ESC) → intet nyt flow i tavlen.
- [ ] **11.4 Ret én linje.** Klik "Traktorer: …" → "Ret: Traktorer" → sæt flueben → **Gem** → tilbage; traktoren er i flowet.
- [ ] **11.5 Omdøb.** Klik "Navn: …" → tekstfeltet åbner → nyt navn står i tavlen.
- [ ] **11.6 Andre flows skjult.** Traktor i et andet flow vises ikke i trinnet Traktorer; linje "N traktorer kører i andre flows".
- [ ] **11.7 Forsyning.** Opsæt et forsyningsflow: Til = stald, Varer = Vælg selv → varerne stalden tager vises; Regler viser Ture og "Kør hjem fra Wait under".
- [ ] **11.8 Nyt flow fra Oversigt-fanen** hopper til Flows med guiden åben.
- [ ] **11.9 Klik routes.** Alle valg (også pr. vare-politik) reagerer; ingen "GUI-fejl".
---
## 12. Genbrug af Courseplay-kurser (v1.34)
Log-linjer at kigge efter (`ADSmartPickup: F<nr> …`): `nyt kursus beregnes til '<maskine>' (<grund>)`,
`kursus genbrugt ('<maskine>', <bredde> m) — ingen ny beregning`, `kursus kopieret fra '<A>' til '<B>'`,
`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under `<courses>`.
- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)`
→ CP starter; `<courses><course vehicle=… field=… width=… sig=… settings=… waypoints=…/>` står i filen efter gem.
- [ ] **12.2 Genbrug.** Samme maskine til samme mark igen (næste pleje/klargøring, eller efter "Stop" og start) → log
`kursus genbrugt` → CP kører UDEN beregnings-hak og kører hele marken fra start (ikke kun sidste stykke).
⚠ Usikkert: accepterer CP 8.1 at jobbet startes på det eksisterende kursus uden `onClickGenerateFieldWorkCourse`?
(validate kræver kun `hasCpCourse`). Tegn på fejl: `Courseplay: …no course…`, eller maskinen kører ingenting.
- [ ] **12.3 Efter genstart.** Gem, genindlæs, vent de 2 min → samme maskine til samme mark → `kursus genbrugt`
(CP har selv gemt kurset i CpAssignedCourses.xml; antal punkter matcher registret).
- [ ] **12.4 Kopi fra anden maskine.** En anden maskine med samme CP-bredde til samme mark → `kursus kopieret fra 'A'`
→ den kører kurset (ikke A's rest). ⚠ Usikkert: kopieret kursus + lane/vin som i dag med `cpCopyCourse`.
- [ ] **12.5 Ny bredde.** Skift CP-bredden i HUD'en (eller en anden maskine med anden bredde) → `nyt kursus beregnes … (ny bredde)`.
- [ ] **12.6 Spilleren laver selv kursus i CP's HUD** på maskinen → næste udsendelse genbruger det IKKE
(antal punkter passer ikke) → kopi fra en anden maskine eller `… (kurset på maskinen er ændret)`.
- [ ] **12.7 Ændret mark.** Lav marken om (kort-redigering/mark-tilkøb der ændrer omridset) → efter scanning, eller når
marken gemmes i "Opsæt mark", log `markens form er ændret — N … glemt` → næste start beregner nyt og gemmer det.
- [ ] **12.8 Multi-tool.** 2 sæt på samme mark: lederen genbruger kun et kursus lavet til 2 sæt/samme bane; følgeren kopierer
stadig lederens kursus (log `…, multi-kursus kopieret, bane N`), og begge kører hver sin bane.
- [ ] **12.9 Vinmark.** Sprøjte (skip 1) og harve (skip 0) på samme vinmark må ikke genbruge hinandens kursus (vin-indstillinger
indgår i nøglen); høsteren (offset 0) heller ikke.
--- ---
## Kendte usikkerheder (første gang i spillet) ## Kendte usikkerheder (første gang i spillet)
@@ -172,4 +245,11 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
| 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 | | 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 | | 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) | | 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) |
| Halm-valg i guiden | `fruitType.hasWindrow/windrowName` = afgrøder med halm | Halm-valget mangler ved hvede eller vises ved majs |
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk | | Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
| Kursus-genbrug (v1.34) | CP-job startes på maskinens eksisterende kursus uden ny beregning | `Courseplay: no course`, eller maskinen starter midt/sidst i kurset |
- [ ] **Høsterens rest (v1.32.2).** Modet høster en mark med aflæsningsvogne → ved høstens slutning stoppes
vognene; høsteren med korn i tanken → log `'Mejetærsker' kører rest-læs (N l) til lager` → AutoDrive
tømmer den ved lageret → parkering. Kunne AD ikke tømme den: besked "har stadig korn i tanken … tøm den".
⚠ Usikkert: kan AutoDrive aflæsse en mejetærskers tank (røret) ved en silo?
+78 -29
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — user manual # AD Smart Pickup — user manual
Version 1.27.0 · Farming Simulator 25 · add-on for AutoDrive Version 1.28.0 · Farming Simulator 25 · add-on for AutoDrive
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
@@ -161,12 +161,17 @@ the same order.
### Buttons ### Buttons
The **Flows** tab shows the board (one line per flow) and below it the selected flow's **summary**: Name,
Direction, From/To, Goods, (Storage), Rules, Wait at, Tractors and Status. **Click a flow** on the board to
select it, and **click a summary line** to change just that step (**Save** saves and returns). Click
**Name** to rename the flow.
| Button | What it does | | Button | What it does |
|---|---| |---|---|
| **New run** | Creates a new flow. It starts as *Into building* | | **Start flow / Stop flow** | Starts or stops all tractors in the flow |
| **Rename** | Renames the selected flow | | **Set up flow** | The guide for the selected flow with its current choices |
| **Delete run** | Deletes the flow and stops its running tractors. Cannot be undone | | **New flow** | The guide for a new flow. Nothing is created before you press **Start flow** or **Save without starting** |
| **Start run / Stop run** | Starts or stops all tractors in the flow | | **Delete flow** | Deletes the flow and stops its running tractors. Cannot be undone |
On the **Overview** tab the buttons are instead: On the **Overview** tab the buttons are instead:
@@ -174,12 +179,30 @@ On the **Overview** tab the buttons are instead:
|---|---| |---|---|
| **Start all flows** | Starts every flow that has tractors. Flows already running are left alone, so no trip is interrupted | | **Start all flows** | Starts every flow that has tractors. Flows already running are left alone, so no trip is interrupted |
| **Stop all flows** | Stops all active tractors in all flows | | **Stop all flows** | Stops all active tractors in all flows |
| **New run** | Creates a new flow and jumps to the **Flows** tab | | **New flow** | Jumps to the **Flows** tab and opens the guide for a new flow |
Afterwards the status line shows how many flows and tractors were started, plus up to three reasons for Afterwards the status line shows how many flows and tractors were started, plus up to three reasons for
tractors that were skipped. All reasons are in `log.txt`. tractors that were skipped. All reasons are in `log.txt`.
Pick a flow in the **Run** field at the top. The details of the selected flow are shown below the board. ### The "Set up flow" guide
Same step screen and buttons as the field guide (**Next**, **Back**, **Cancel**; one question at a time,
steps that do not apply are skipped):
| Step | Shown when | Choices |
|---|---|---|
| **Name** | new flow | *Change name …* opens the text field |
| **Direction** | always | *Into building* (supply) / *Out of building* (delivery) |
| **To** / **From** | always | The buildings (see *The building list* below) |
| **Goods** | always | Supply: *Automatic (what the building needs)* or *Choose myself*. Delivery: per good Off / Store / Sell / Sell, else store |
| **Storage** | delivery with a good on Store | *Automatic (nearest with room)* or *Choose myself* among the stores |
| **Rules** | always | Delivery: sell from %, store above %, collect in one trip. Supply: trips, leave Wait below % |
| **Wait at** | always | The Wait pool |
| **Tractors** | always | Free tractors and the flow's own. Tractors in other flows (and those whose trailer does not fit) are hidden with a line |
| **Summary** | always | The summary; click a line to change it. **Start flow** or **Save without starting** |
The mod never asks about the marker at the building, route checks, best feed first, load swap, source
choice, spreading over several targets, resting empty sources or tractor names.
### Direction ### Direction
@@ -209,7 +232,7 @@ A supply flow keeps one or more buildings filled with everything they use.
### Setup ### Setup
1. **New run**, and leave Direction on **Into building**. 1. **New flow** (the guide), and leave Direction on **Into building**.
2. Under **To**: set Yes on the barns and factories to fill. The list also shows silos that can only 2. Under **To**: set Yes on the barns and factories to fill. The list also shows silos that can only
be filled and not emptied, such as the Pumps & Hoses drive-on bunkers (DOB 115) that feed a biogas be filled and not emptied, such as the Pumps & Hoses drive-on bunkers (DOB 115) that feed a biogas
fermenter. For those, **Select** with the goods the fermenter needs works best. fermenter. For those, **Select** with the goods the fermenter needs works best.
@@ -546,7 +569,7 @@ and which machines. Requires **Courseplay** in addition to AutoDrive.
### Select and forget: crop on the field ### Select and forget: crop on the field
The easiest way: pick **Crop on the field** (such as *Wheat*) at the top of the field. The mod does the rest: The easiest way: pick a crop (such as *Wheat*) in the guide step **Crop** (Set up field). The mod does the rest:
- **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when - **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when
ready. ready.
@@ -556,9 +579,8 @@ The easiest way: pick **Crop on the field** (such as *Wheat*) at the top of the
- **Bare field:** prepared and sown as soon as it is sowing time. - **Bare field:** prepared and sown as soon as it is sowing time.
- After that the cycle runs by itself: growth → care → harvest → preparation → sowing → … - After that the cycle runs by itself: growth → care → harvest → preparation → sowing → …
With a crop chosen, the field runs automatically. The line below the choice tells you what the mod will do. With a crop chosen, the field runs automatically. You can still choose silage/hay, straw, number of
You can still choose silage/hay, straw, number of machines, soil work and care. Choose *None — plan it machines, soil work and care in the following steps. Choose *Keep what grows* to run the field as before.
yourself* to run the field manually as before.
### Setup ### Setup
@@ -566,7 +588,7 @@ yourself* to run the field manually as before.
such as `F109`. Machines drive there on the road before Courseplay takes over. At most 400 m from the such as `F109`. Machines drive there on the road before Courseplay takes over. At most 400 m from the
field. Without it, the field shows `!` and nothing is sent. field. Without it, the field shows `!` and nothing is sent.
2. **Fixed rigs.** The implement must be attached (or self-propelled). The mod never attaches. 2. **Fixed rigs.** The implement must be attached (or self-propelled). The mod never attaches.
3. **Field fleet.** At the bottom are all vehicles with a field implement, with role and width. With 3. **Field fleet.** The **Field fleet** button shows all vehicles with a field implement, with role and width. With
**Use all free machines** = Yes (default) the mod finds the machines itself; with No only ticked ones **Use all free machines** = Yes (default) the mod finds the machines itself; with No only ticked ones
are used. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and are used. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and
vehicles you drive are never used. A sprayer/spreader only has a role once it holds product vehicles you drive are never used. A sprayer/spreader only has a role once it holds product
@@ -585,25 +607,52 @@ yourself* to run the field manually as before.
### A field ### A field
Click a field in the table. Below the table: Click a field in the table. Below the table is the field's **summary** — one line per topic:
| Option | Meaning | | Line | Example |
|---|---| |---|---|
| **Mode** | *Manual*: the field waits for **Start now**. *Auto*: it starts by itself when ready or when it needs care | | **Crop** | *Keep what grows* / *-> Wheat* |
| **Machines per step** | 1–5 rigs at once (Courseplay multi-tool). A step starts only when all rigs are free | | **Crop options** | *Silage, rake* / *straw chopped* |
| **Plan** (mowable) | Silage or Hay | | **Preparation** | *Automatic* / *Plow, lime* |
| **Rake** (mowable) | Rake the swaths before collecting | | **Crop care** | *fertilizer, weeds* |
| **Unloading trailers** (harvest) | 1–3 trailers that follow the harvester and drive to storage | | **Machines** | *3 dedicated* / *free machines*, *1 at a time*, *1 unloaders* |
| **Straw after harvest** (harvest) | Chopped (spread), Collected to a silo, or Baled, and a bale collector takes the bales to a bale store | | **Storage** | *Field silo* / *Automatic* / a chosen silo |
| **Mulch stubble** | Mulch the stubble after harvest |
| **Soil work** | Auto (plow when the soil needs it, else cultivate), Cultivate, Plow or None |
| **Sow afterwards** + **Crop to sow** | Sow after soil work. Default: the crop that was harvested |
| **Roll** | Roll after sowing |
| **Fertilize / Lime / Control weeds** | Care while the crop grows, when the soil needs it (at most twice per care per season) |
| **Force storage** | Pick stores. Without a choice: silage → nearest fermenting silo, else silo; hay and harvest → nearest silo |
Below the options: **The soil needs** (lime, plowing, fertilizer, weeds), the marker, and mowing width and **Click a line** to change just that (e.g. storage): only that step opens, and **Save** saves and returns.
swath offset when relevant. Below the summary are the full status and the marker.
Buttons: **Start now / Stop field**, **Set up field** and **Field fleet**.
### The "Set up field" guide
**Set up field** sets the field up one question at a time ("Step 3 of 7 — …"). Steps that do not fit the
field are skipped; a field already set up opens with its current choices. Without a selected field you pick
the field first.
| Step | Shown when | Choices |
|---|---|---|
| **Crop** | always | *Keep what grows* or a crop to sow (target crop, see above). Harvest/bare field: *Sow the same crop again after harvest* |
| **Crop options** | mowable (now or as target): Silage/Hay + Rake. Harvest crop with straw: straw chopped/collected/baled | |
| **Preparation** | after harvest / bare field / crop change | *Automatic (recommended)* or *Choose myself*: soil work, lime, mulch, roll |
| **Crop care** | a growing crop | Fertilizer, weeds (not on mowables); lime here when there is no preparation |
| **Machines** | always | At a time (1–5), unloaders when harvesting (1–3), **Dedicated machines for this field?** + the list |
| **Storage** | harvest/mowing | *Use the field silo?* (when there is one), else *Automatic* or *Choose myself* among the 8 nearest |
| **Summary** | always | The summary; click a line to change it. **Run automatically** (Auto) or **Save without starting** (Manual) |
Buttons: **Next**, **Back**, **Cancel**. Nothing is saved before **Run automatically** / **Save without
starting** (or **Save** when editing one line). Closing the menu or switching tab discards the guide. With a
target crop the field always runs automatically. Without a marker the field can be set up; work waits for
the marker.
**Dedicated machines:** a dedicated machine only works this field, and the field waits for its own rather
than borrowing another. The list only shows free machines and those already dedicated here; machines
dedicated to other fields show as "N machines are dedicated to other fields (F22)".
**Field fleet** (button): the shared fleet — *Use all free machines* and the ticked list, "dedicated to
F22", "working on F108" and width warnings. **Back to fields** returns.
The mod never asks about: scanning, when to harvest/mow/fertilize/spray, refilling, emptying wrong cargo,
waiting for rain, width checks, choosing the nearest machine, routes, and fermenting silo for silage.
A field is **ready** when at least 90 % is in the harvest window. **Start now** starts the whole chain (or A field is **ready** when at least 90 % is in the harvest window. **Start now** starts the whole chain (or
care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops
+174
View File
@@ -0,0 +1,174 @@
# Guider til opsætning (marker + flows) — implementeringsplan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
## Global Constraints
- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
## Review Focus
1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
---
## Beslutninger (uklarheder i spec'en)
- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
---
# Etape A — fælles ramme + Marker (v1.31.0.0)
### Task A1: Guide-motoren `adGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
**Interfaces (Produces):**
```lua
ADGuide.new(def, draft, context, options) -> guide
-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
-- guide = {def, draft, context, step, single, returnTo, snapshot}
ADGuide.visibleSteps(guide) -> {stepId...}
ADGuide.position(guide) -> index|nil, total
ADGuide.isLast(guide) -> bool
ADGuide.next(guide) -> guide, event -- event nil | "done"
ADGuide.back(guide) -> guide, event -- event nil | "cancel"
ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
```
- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
- [ ] Implementér; kør → ALLE TESTS OK.
### Task A2: Markguidens trin `adFieldGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
**Interfaces:**
- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
- Produces:
```lua
ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
ADFieldGuide.openPicker(context) -> guide -- trin "field"
ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
ADFieldGuide.isVisible(stepId, guide) -> bool
ADFieldGuide.rows(guide) -> {row...}
-- row = {type="choice", id, title, options={{label, value}}, value}
-- | {type="toggle", id, title, off, on, value}
-- | {type="check", id, item, title, value}
-- | {type="text", label} | {type="link", id, value, label}
ADFieldGuide.choose(guide, id, value) -> guide
ADFieldGuide.summary(guide) -> {line...}
ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
```
- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
**Interfaces (Produces):**
```lua
SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
SmartPickupFrame:initGuideButtons(i18n)
SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
SmartPickupFrame:guideText(label) -> string
SmartPickupFrame:addLinkRow(text, action)
SmartPickupFrame:onClickGuideRow(state, data)
SmartPickupFrame:getGuideButtons() -> knapliste
SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
```
- [ ] Skriv filen; `luajit -bl` syntakstjek.
### Task A4: Marker-fanen: oversigt, markflåde, guide
**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
- [ ] Skriv, syntakstjek, suiten grøn.
### Task A5: Docs + ship v1.31.0.0
- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
# Etape B — Flows på samme ramme (v1.32.0.0)
### Task B1: Flowguidens trin `adFlowGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
**Interfaces (Produces):**
```lua
ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
ADFlowGuide.summary(guide) -> {line...}
ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
```
- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
- [ ] FAIL → implementér → OK.
### Task B2: Flows-fanen i GUI'et
**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
- [ ] Skriv, syntakstjek, suiten grøn.
### Task B3: Docs + ship v1.32.0.0; selv-review
- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,919 @@
# Maskinpulje etape 1 (v1.48.0.0) 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:** Markarbejdet kan sende en ledig solo-traktor, der henter et frakoblet puljeredskab (plov, kultivator,
såmaskine …), udfører arbejdet med Courseplay, sætter redskabet af på dets AD-parkering og kører hjem.
**Architecture:** Nyt modul `adPool.lua` (ren planner + tynd spil-adapter) finder puljeredskaber (frakoblet +
AD-parkering "X" + markør "Connect X") og puljetraktorer og parrer dem. `ADFieldWork.dispatch` bruger puljen som
fallback, når ingen færdig kombination er ledig; en ny fase `fetching` kører `ADHitch.fetch` og fortsætter så i
den eksisterende `driving`-fase. Når en opgave slutter, kører `ADHitch.returnHome` redskabet til dets parkering
(afkobling) og traktoren hjem. Marker, der venter, betjenes længst-ventende først.
**Tech Stack:** Lua 5.1 (FS25-scripting), luajit til tests, AutoDrive 3.0.1.2 og Courseplay 8.1.0.3 API'er.
**Spec:** `docs/superpowers/specs/2026-09-28-maskinpulje-design.md`
## Global Constraints
- Kun etape 1 (markarbejde). Tømmevogne ved høst (`decision.helper == true`) og marksilo-flowet bruger IKKE puljen i denne version.
- Puljeredskab = frakoblet + AD-parkering "X" (`advd.parkDestination >= 1`) + markør "Connect X" (store/små bogstaver ligegyldigt).
- Puljetraktor = motoriseret, intet koblet bag på, ikke i et flow (`ADRunsController.getRunForVehicle == nil`), ikke optaget, ikke styret af spilleren, egen AD-parkering, flueben i markflåden (`ADFieldFleet.isEnabled`).
- Færdige kombinationer foretrækkes altid; puljen kun når `pick`/`pickMany` intet giver.
- Redskab nærmest marken, derefter nærmeste traktor til redskabet med nok hk; manglende hk-data = tilladt.
- Karantæne for et redskab, der ikke kunne kobles på: 10 min (`ADPool.QUARANTINE_MS = 600000`).
- En traktor køres kun hjem, når et af mod'ens egne jobs slutter — aldrig af sig selv.
- Flows (runs) rører aldrig puljen.
- Kommentarer og logtekster på dansk, som resten af koden. Ingen Co-Authored-By i commits. Push kun til remote `gitea`.
- Tests køres fra repo-roden: `luajit tests/test_<navn>.lua` — output slutter med `ALLE TESTS OK`.
- Build: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy mens spillet kører: `cp` til `~/FS25-data/mods/.FS25_ADSmartPickup.new` og `mv` på plads.
## Review Focus
- **To marker vil have samme plov samtidig** → kun den første får den; den anden venter (reservation) — testet i Task 1 (`pairFor` springer reserverede over).
- **Redskab uden hk-data eller traktor uden motor-data** → tillades (intet går i stå) — testet i Task 1 (`powerOk` nil-tilfælde, `numberOf` af tabel-specs).
- **Traktor på hjemtur bliver valgt til et nyt job** → må ikke ske; den er optaget til den er hjemme — testet i Task 3 (`ADPool.isReturning`) og brugt i `getRigs`/`poolTractors`.
- **Spilleren kobler selv et puljeredskab på og kører** → traktoren køres ikke hjem af mod'en — dækket af, at `returnHome` kun kaldes fra `finishAssignment`/fejlveje (Task 5); ingen tick-regel.
- **Kobling fejler (trækøje står skævt)** → traktor hjem, redskab i karantæne, marken prøver igen — testet i Task 1 (karantæne i `pairFor`) og Task 4 (fejlvej).
---
### Task 1: `adPool.lua` — ren planlægning
**Files:**
- Create: `FS25_ADSmartPickup/adPool.lua`
- Test: `tests/test_adPool.lua`
**Interfaces:**
- Consumes: `ADFieldFleet.roleFromSpecs(specs)` (adFieldFleet.lua:27), `ADFieldModel.checkWidth(role, width, requiredWidth)` (bruges via `accept`), `ADHitch.parseMarker(name)` (adHitch.lua).
- Produces:
- `ADPool.roleOf(specs) -> role|nil` (specs = `ADCpBridge.getSpecs`-tabel; `unloader` når `specs.spec_trailer and specs.spec_dischargeable` og ingen anden rolle).
- `ADPool.numberOf(value) -> number|nil`
- `ADPool.powerOk(tractorHp, neededHp) -> bool`
- `ADPool.connectFor(parkName, markers) -> marker|nil` (markers = `{{name, id}}`)
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) -> pair | nil, why, args`
- implement = `{id, name, role, roles?, x, z, width, neededHp, connectId, parkId}`
- tractor = `{id, name, x, z, hp, parkId}`
- blocked = `{[id] = true}` (reserverede + karantæne, både redskab- og traktor-id)
- pair = `{implement = implement, tractor = tractor}`
- why ∈ `noPoolImplement` / `noPoolTractor` / `tooWeak` (args = `{neededHp}`)
- `ADPool.queueOrder(infos, waitingSince) -> infos (ny tabel)`
- `ADPool.reserve(owner, ids)`, `ADPool.release(owner)`, `ADPool.quarantine(id, nowMs)`, `ADPool.blocked(nowMs) -> {[id]=true}`
- [ ] **Step 1: Write the failing test**
Create `tests/test_adPool.lua`:
```lua
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
dofile("FS25_ADSmartPickup/adHitch.lua")
dofile("FS25_ADSmartPickup/adPool.lua")
local P = ADPool
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
-- rolle ud fra redskabets egne specs
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
check("rolle: intet", P.roleOf({}), nil)
-- hk fra butiksdata: tal eller tabel (konfigurationer)
check("tal", P.numberOf(180), 180)
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
check("tom tabel", P.numberOf({}), nil)
check("nil", P.numberOf(nil), nil)
check("hk: nok", P.powerOk(200, 180), true)
check("hk: for lidt", P.powerOk(150, 180), false)
check("hk: redskab uden data", P.powerOk(150, nil), true)
check("hk: traktor uden data", P.powerOk(nil, 180), true)
-- Connect-markør til en parkering
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
check("connect: ingen", P.connectFor("Start", markers), nil)
check("connect: uden parkering", P.connectFor(nil, markers), nil)
-- parring: redskab nærmest marken, så traktor nærmest redskabet
local implements = {
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
}
local tractors = {
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
}
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
check("par: redskab med flere roller", multi.implement.id, "spreder")
-- reservationer og karantæne
P.reserve("F12", {"plovNear", "mid"})
check("reserveret: redskab", P.blocked(0).plovNear, true)
check("reserveret: traktor", P.blocked(0).mid, true)
P.release("F12")
check("frigivet", P.blocked(0).plovNear, nil)
P.quarantine("plovNear", 1000)
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
check("kø: rører ikke input", infos[1].id, 5)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `luajit tests/test_adPool.lua`
Expected: fejl `cannot open FS25_ADSmartPickup/adPool.lua`
- [ ] **Step 3: Write minimal implementation**
Create `FS25_ADSmartPickup/adPool.lua`:
```lua
-- 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
-- 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(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
-- 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] 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 = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end)
if tractor ~= nil then return {implement = implement, tractor = tractor} end
if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end
end
return nil, "tooWeak", {weakest or 0}
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.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
```
- [ ] **Step 4: Run test to verify it passes**
Run: `luajit tests/test_adPool.lua`
Expected: sidste linje `ALLE TESTS OK`
- [ ] **Step 5: Commit**
```bash
git add FS25_ADSmartPickup/adPool.lua tests/test_adPool.lua
git commit -m "feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne"
```
---
### Task 2: `adPool.lua` — spil-adapter (indsamling af puljen)
**Files:**
- Modify: `FS25_ADSmartPickup/adPool.lua` (tilføj nederst)
- Modify: `FS25_ADSmartPickup/modDesc.xml` (registrér `adPool.lua` efter `adHitch.lua`)
**Interfaces:**
- Consumes: `ADCpBridge.getSpecs(vehicle)` (adCpBridge.lua:153), `ADCpBridge.getWorkWidth(vehicle)` (:363), `ADRunsController.getFarmVehicles(adEnv)`, `ADRunsController.getRunForVehicle(vehicle)`, `ADRunsController.getVehicleId(vehicle)`, `ADRuns.vehicleLabel(vehicle)`, `ADFieldFleet.isEnabled(mode, fleet, id)`, `ADFieldJobs.state`, `ADHitch.rearImplementsOf(vehicle)` (Task 3), `ADPool.isReturning(vehicle)` (Task 3), `adEnv.ADGraphManager:getMapMarkers()`, `:getMapMarkerById(index)`, `g_currentMission.vehicleSystem.vehicles`.
- Produces: `ADPool.collect(adEnv, isBusy) -> implements, tractors` (formater som Task 1; implement har også `.vehicle`, tractor har `.vehicle`); `isBusy(vehicle) -> bool` gives af kalderen (markarbejdets optaget-regel).
- [ ] **Step 1: Tilføj adapteren**
Tilføj nederst i `FS25_ADSmartPickup/adPool.lua`:
```lua
-- ---- 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 function neededHpOf(vehicle)
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
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
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata.
local function tractorHpOf(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
local function isAttached(vehicle)
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
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 ipairs(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
if role ~= nil then
local x, z = positionOf(vehicle)
table.insert(implements, {
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
role = role, roles = specs.sprayRoles, x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
})
end
end
end
local state = ADFieldJobs.state
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle)
local park = parkMarker(adEnv, vehicle)
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle)
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) then
local x, z = positionOf(vehicle)
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
hp = tractorHpOf(vehicle), parkId = park.id})
end
end
return implements, tractors
end
```
- [ ] **Step 2: Registrér modulet**
I `FS25_ADSmartPickup/modDesc.xml` efter linjen `<sourceFile filename="adHitch.lua"/>`:
```xml
<sourceFile filename="adPool.lua"/>
```
- [ ] **Step 3: Syntakstjek og tests**
Run: `luajit -bl FS25_ADSmartPickup/adPool.lua >/dev/null && echo syntaks-ok && luajit tests/test_adPool.lua | tail -1`
Expected: `syntaks-ok` og `ALLE TESTS OK`
- [ ] **Step 4: Commit**
```bash
git add FS25_ADSmartPickup/adPool.lua FS25_ADSmartPickup/modDesc.xml
git commit -m "feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk"
```
---
### Task 3: `adHitch.lua` — hjemtur og "på hjemtur"-status
**Files:**
- Modify: `FS25_ADSmartPickup/adHitch.lua`
- Modify: `FS25_ADSmartPickup/adPool.lua` (isReturning-register)
- Test: `tests/test_adHitch.lua`, `tests/test_adPool.lua`
**Interfaces:**
- Consumes: `ADHitch.route(adEnv, vehicle, markerIds, onDone)` (findes), `attachedImplements`/`rearImplements` (findes i adHitch.lua).
- Produces:
- `ADHitch.homeRoute(implementParkIds, tractorParkId) -> {id, ...}` (ren)
- `ADHitch.rearImplementsOf(vehicle) -> {{object, z, name}}` (offentlig udgave af den lokale)
- `ADHitch.returnHome(adEnv, tractor, onDone) -> bool` (false = intet puljeredskab bag / ingen parkering)
- `ADPool.setReturning(vehicle, isOn)`, `ADPool.isReturning(vehicle) -> bool`
- [ ] **Step 1: Write the failing tests**
Tilføj i `tests/test_adHitch.lua` før `print(failures == 0`:
```lua
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
local home = H.homeRoute({4}, 2)
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
```
Tilføj i `tests/test_adPool.lua` før `print(failures == 0`:
```lua
-- på hjemtur: optaget indtil hjemme
local tractorObject = {}
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
P.setReturning(tractorObject, true)
check("hjemtur: markeret", P.isReturning(tractorObject), true)
P.setReturning(tractorObject, false)
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `luajit tests/test_adHitch.lua | tail -3; luajit tests/test_adPool.lua | tail -3`
Expected: fejl `attempt to call field 'homeRoute' (a nil value)` og `attempt to call field 'isReturning'`
- [ ] **Step 3: Implement**
I `FS25_ADSmartPickup/adPool.lua`, efter `ADPool.blocked`:
```lua
-- 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
```
I `FS25_ADSmartPickup/adHitch.lua`, i den rene del (efter `ADHitch.waitAfter`):
```lua
-- 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
```
I spil-delen af `adHitch.lua`, lige efter `local function isImplementPark(...) ... end`:
```lua
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
```
Efter `function ADHitch.route(...) ... end`:
```lua
-- 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.
function ADHitch.returnHome(adEnv, tractor, onDone)
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
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
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `luajit tests/test_adHitch.lua | tail -1; luajit tests/test_adPool.lua | tail -1; luajit -bl FS25_ADSmartPickup/adHitch.lua >/dev/null && echo syntaks-ok`
Expected: `ALLE TESTS OK` to gange og `syntaks-ok`
- [ ] **Step 5: Commit**
```bash
git add FS25_ADSmartPickup/adHitch.lua FS25_ADSmartPickup/adPool.lua tests/test_adHitch.lua tests/test_adPool.lua
git commit -m "feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens"
```
---
### Task 4: Markarbejde — puljen som fallback og fasen `fetching`
**Files:**
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`getRigs` :132-170, `dispatch` :382-482, `follow` :1083-1143, `release` :278-283)
- Modify: `FS25_ADSmartPickup/l10n/l10n_da.xml`, `FS25_ADSmartPickup/l10n/l10n_en.xml`
**Interfaces:**
- Consumes: `ADCpBridge.getSeedFruits/hasSeeds/isMechanicalWeeder(vehicle)`, `ADPool.collect(adEnv, isBusy)`, `ADPool.pairFor(...)`, `ADPool.blocked(nowMs)`, `ADPool.reserve/release/quarantine`, `ADPool.isReturning`, `ADHitch.fetch(adEnv, vehicle, markerId, onDone)`, `ADHitch.returnHome`.
- Produces: assignment-felter `pool = {implementId, implementName, connectId}`, `phase = "fetching"`, `hitch = {done=bool, ok=bool, detail=any}`; field-årsager `spu_field_reason_noPoolTractor`, `spu_field_reason_fetching` (tooWeak som fritekst).
- [ ] **Step 1: Optaget-regel genbrugelig + hjemtur tæller som optaget**
I `adFieldWork.lua` før `function ADFieldWork.getRigs`:
```lua
-- 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))
end
```
I `getRigs` erstat `busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)\n or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),` med:
```lua
busy = isBusyVehicle(vehicle, id),
```
og tilføj som sidste alternativ i `busyWhy`-kæden (før `or nil`):
```lua
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
```
- [ ] **Step 2: Fallback i dispatch**
I `ADFieldWork.dispatch`, lige efter `if rigs == nil then` og FØR `if #offNetwork > 0 ...`, indsæt:
```lua
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
if pooled then return end
if poolWhy == "tooWeak" or (poolWhy == "noPoolTractor" and why ~= "allBusy") then
why, args = poolWhy, poolArgs
end
end
```
Tilføj før `function ADFieldWork.dispatch` en ny funktion:
```lua
-- 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)
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
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 = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
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
```
- [ ] **Step 3: Frigiv reservation ved release**
I `ADFieldWork.release`, før `ADFieldJobs.state.assignments[assignment.vehicleId] = nil`:
```lua
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
```
- [ ] **Step 4: Fasen fetching i follow**
I `ADFieldWork.follow`, efter linjen `wake(vehicle)` og før `if assignment.phase == "driving" then`, gør `if` til en kæde ved at indsætte:
```lua
if assignment.phase == "fetching" then
local hitch = assignment.hitch
if not hitch.done then return end
ADPool.release(assignment)
if not hitch.ok then
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
ADPool.quarantine(assignment.pool.implementId, now())
ADFieldWork.release(assignment)
note(assignment.fieldId, {retryAt = 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å: bredden med redskabet på (for bred -> hjem igen)
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
assignment.width, assignment.phase, assignment.since = width, "driving", now()
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
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
```
(og fjern det oprindelige `if assignment.phase == "driving" then`, som nu er `elseif`.)
Tilføj også `fetching` til spiller-reglen øverst i `follow`, så AD's kørsel under hentning ikke tolkes som spillerens: `steeredByAi` er allerede sand, mens AD kører; ingen ændring nødvendig.
- [ ] **Step 5: Tekster**
`l10n/l10n_da.xml` (ved de andre `spu_field_reason_`):
```xml
<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"/>
```
`l10n/l10n_en.xml`:
```xml
<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"/>
```
`tooWeak` bruger fritekst (hk står før rollen, og `fieldReasonText` sætter rollen først): i Step 2 erstattes linjen
`why, args = poolWhy, poolArgs` med:
```lua
if poolWhy == "tooWeak" then
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
return
end
why, args = poolWhy, poolArgs
```
- [ ] **Step 6: Syntakstjek og alle tests**
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
Expected: `syntaks-ok`, ingen `FEJL`-linjer, `færdig`
- [ ] **Step 7: Commit**
```bash
git add FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/l10n/l10n_da.xml FS25_ADSmartPickup/l10n/l10n_en.xml
git commit -m "feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)"
```
---
### Task 5: Markarbejde — hjemtur når opgaven slutter
**Files:**
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`finishAssignment` :834-862, fejlvejen i `follow` "nåede ikke" :1115-1120)
**Interfaces:**
- Consumes: `ADHitch.returnHome(adEnv, tractor, onDone) -> bool`.
- Produces: ingen nye navne.
- [ ] **Step 1: finishAssignment**
Erstat i `finishAssignment`:
```lua
local park = 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
```
med:
```lua
-- 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 = not assignment.adHandlesEnd and not assignment.manual 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 pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
```
- [ ] **Step 2: "nåede ikke frem" og "tom" — også hjem med redskabet**
I `follow`, i grenen der slutter med
```lua
local park = parkMarkerId(adEnv, vehicle)
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
```
erstat de to linjer med:
```lua
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
```
I `finishAssignment`, i "tom såmaskine/sprøjte"-grenen efter `ADFieldWork.release(assignment)`, indsæt:
```lua
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
```
- [ ] **Step 3: Syntakstjek og alle tests**
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
Expected: `syntaks-ok`, ingen `FEJL`, `færdig`
- [ ] **Step 4: Commit**
```bash
git add FS25_ADSmartPickup/adFieldWork.lua
git commit -m "feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem"
```
---
### Task 6: Kø — længst ventende mark først
**Files:**
- Modify: `FS25_ADSmartPickup/adFieldJobs.lua` (`stepField` dispatch-gren ~:421-426, `tick` løkke ~:486)
- Test: `tests/test_adPool.lua` (dækket af `queueOrder`-tests i Task 1)
**Interfaces:**
- Consumes: `ADPool.queueOrder(infos, waitingSince)`.
- Produces: `ADFieldJobs.waitingSince = {[fieldId] = nowMs}`.
- [ ] **Step 1: Registrér ventetid**
I `adFieldJobs.lua` ved siden af `ADFieldJobs.lastDecision = {}`:
```lua
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
ADFieldJobs.waitingSince = {}
```
I `stepField`, erstat dispatch-grenen:
```lua
local before = ADFieldJobs.assignmentCount()
ADFieldWork.dispatch(adEnv, field, info, decision)
if ADFieldJobs.assignmentCount() > before then ADFieldJobs.lastDispatchAt = ADFieldJobs.now end
```
med:
```lua
local before = ADFieldJobs.assignmentCount()
ADFieldWork.dispatch(adEnv, field, info, decision)
if ADFieldJobs.assignmentCount() > before then
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
ADFieldJobs.waitingSince[info.id] = nil
elseif ADFieldJobs.waitingSince[info.id] == nil then
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
end
```
I samme funktion, i grenen `decision.action == "advance"` efter `ADFieldJobs.setField(nextField)`:
```lua
ADFieldJobs.waitingSince[info.id] = nil
```
- [ ] **Step 2: Kørsel i kø-rækkefølge**
I `ADFieldJobs.tick` erstat `for index, info in ipairs(ADFields.list()) do` med:
```lua
local infos = ADFields.list()
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
for index, info in ipairs(infos) do
```
- [ ] **Step 3: Tests**
Run: `for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; luajit -bl FS25_ADSmartPickup/adFieldJobs.lua >/dev/null && echo syntaks-ok`
Expected: ingen `FEJL`, `syntaks-ok`
- [ ] **Step 4: Commit**
```bash
git add FS25_ADSmartPickup/adFieldJobs.lua
git commit -m "feat(pulje): marker der venter på en maskine betjenes længst-ventende først"
```
---
### Task 7: Version, build, deploy og in-game test
**Files:**
- Modify: `FS25_ADSmartPickup/modDesc.xml` (`<version>1.47.4.0</version>` → `1.48.0.0`)
- [ ] **Step 1: Bump version, alle tests, build, deploy**
```bash
cd ~/fs25-adsmartpickup
sed -i 's#<version>1\.47\.4\.0</version>#<version>1.48.0.0</version>#' FS25_ADSmartPickup/modDesc.xml
for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done
rm -f FS25_ADSmartPickup.zip; (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .)
cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.new && mv ~/FS25-data/mods/.FS25_ADSmartPickup.new ~/FS25-data/mods/FS25_ADSmartPickup.zip
unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -m1 '<version>'
```
Expected: ingen `FEJL`, `<version>1.48.0.0</version>`
- [ ] **Step 2: Commit og push**
```bash
git add FS25_ADSmartPickup/modDesc.xml
git commit -m "chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde)"
TOKEN=$(grep -A15 -i "^## Gitea" ~/credentials-vault.md | grep -oE "[a-f0-9]{40}" | head -1); git -c http.extraHeader="Authorization: Basic $(printf 'masterdraco:%s' "$TOKEN" | base64 -w0)" push -q gitea main
```
- [ ] **Step 3: In-game test (savegame4, Golden Lands) — spilleren udfører, vi læser loggen**
Forudsætning: et redskab til markarbejde (fx kultivator/plov) med AD-parkering "X" og markør "Connect X" (bakkespor), Challengeren solo med egen AD-parkering, og en mark med markør der skal kultiveres/pløjes (plan auto eller Start).
Forventet i `log.txt` (grep `ADSmartPickup`):
1. `F<n>: cultivator: 'Challenger MT635' henter '<redskab>' (pulje)`
2. `kobling: 'Challenger MT635' koblede '<redskab>' på ved 'Connect X'`
3. `F<n>: cultivator 'Challenger MT635' har '<redskab>' på — kører til <markør>`
4. CP arbejder; `F<n>: cultivator '…' færdig`
5. `F<n>: '…' sætter redskabet af og kører hjem`, `kobling: … koblede '<redskab>' af ved 'X'`, `… er færdig med turen (2 trin)`
Fejlscenarie: flyt redskabet 3 m væk fra sin plads → `kunne ikke koble … på` efter 8 s, traktoren kører hjem, fanen viser årsagen, redskabet springes over i 10 min.
@@ -0,0 +1,690 @@
# Flows/Fields: farvet liste + redigeringsvindue — 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:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
## Global Constraints
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
- Rækker er ≈835 px brede (som lagervisningen).
- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
## Review Focus
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
---
### Task 1: Rene statusfunktioner for flows (+ fælles farver)
**Files:**
- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
- Test: `tests/test_flowStatus.lua` (ny)
**Interfaces:**
- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
`ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
```lua
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
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
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
```lua
-- ---- 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"
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
```
- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
---
### Task 2: Rækker kan tegnes i et andet layout
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
**Interfaces:**
- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
- [ ] **Step 1: `addRow`** — replace body:
```lua
function SmartPickupFrame:addRow(prefab, data)
local layout = self.rowTarget or self.boxLayout
local row = prefab:clone(layout)
row.spuLayout = layout
FocusManager:loadElementFromCustomValues(row)
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
local option = row:getDescendantByName("option")
if option ~= nil then
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
self.rowData[option] = data
end
return row
end
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
function SmartPickupFrame:withRowTarget(layout, fn)
local previous = self.rowTarget
self.rowTarget = layout
local ok, err = pcall(fn, self)
self.rowTarget = previous
if not ok then error(err, 0) end
end
function SmartPickupFrame:clearPanelRows()
for _, row in ipairs(self.panelRows or {}) do
FocusManager:removeElement(row)
row:delete()
end
self.panelRows = {}
end
```
- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
```lua
function SmartPickupFrame:colorLayout(layout)
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
if colors == nil or layout == nil then return end
local unpackFn = table.unpack or unpack
local isEvenRow = false
for _, element in ipairs(layout.elements) do
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
end
isEvenRow = not isEvenRow
end
end
function SmartPickupFrame:colorRows()
self:colorLayout(self.boxLayout)
end
```
- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
```lua
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
```
(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
---
### Task 3: Redigeringsvinduet (overlay + mixin)
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
**Interfaces:**
- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before `</Profiles>`):
```xml
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
<Profile name="spuOverlayBackdrop" extends="spuBar">
<imageColor value="0 0 0 0.65"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuOverlayPanel" extends="spuBar">
<imageColor value="0.09 0.09 0.09 0.97"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuPanelTitle" extends="spuCellBold">
<textSize value="20px"/>
</Profile>
```
- [ ] **Step 2: XML overlay** — directly after `</ScrollingLayout>` of `boxLayout`:
```xml
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
<GuiElement profile="baseReference" id="editOverlay" visible="false">
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
<Bitmap profile="spuOverlayPanel" id="editPanel">
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
</Bitmap>
</GuiElement>
```
- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
```lua
-- 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.
-- 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.78, 0.86 -- andel af listens flade
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_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
end
function SmartPickupFrame:editPanelKind()
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
end
-- Vinduet dækker listens flade; panelet er centreret i den.
function SmartPickupFrame:layoutEditPanel()
local box = self.boxLayout
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
self.editBackdrop:setPosition(x, y)
self.editBackdrop:setSize(w, h)
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
self.editPanel:setSize(pw, ph)
local top = 80 * g_pixelSizeScaledY
self.editLayout:setSize(pw, ph - top)
end
function SmartPickupFrame:openEditPanel(kind, id)
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
self:closeGuide()
self.editOverlay:setVisible(true)
self:layoutEditPanel()
self:requestRebuild()
end
function SmartPickupFrame:closeEditPanel()
if not self.editPanelOpen then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self:closeGuide()
self:clearPanelRows()
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
self:requestRebuild()
end
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
function SmartPickupFrame:fillEditPanel(adEnv)
self:clearPanelRows()
local hooks = self:editPanelKind()
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.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self.editOverlay:setVisible(false)
return
end
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 + knaptekster.
function SmartPickupFrame:refreshEditPanel(adEnv)
local hooks = self:editPanelKind()
if hooks == nil or adEnv == nil then return end
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
local status = hooks.status(self, adEnv, self.editPanelId)
self.editStatus:setText(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.panelDeleteButtonInfo.disabled = not hooks.canDelete
end
function SmartPickupFrame:onClickPanelClose()
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:editPanelKind()
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickPanelDelete()
self:safe(function(frame)
local hooks = frame:editPanelKind()
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
end)
end
```
- [ ] **Step 4: Wire into frame** —
- `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
- `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
- `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
- `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
- `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
- `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
- `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
- Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
---
### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
- Modify: `l10n/*.xml`
**Interfaces:**
- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
```xml
<!-- 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>
```
- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
```lua
-- Start/Stop for ét flow (rækkens knap 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)
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)
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
```
And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
```lua
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; ellers åbn vinduet
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
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 level = self:flowLevel(adEnv, run)
local color = ADFlowView.LEVEL_COLORS[level]
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
```
In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
- [ ] **Step 4: `refreshFlowTab`** — replace body:
```lua
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
```
- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
```lua
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,
}
```
In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
- [ ] **Step 7: Run all tests + XML check.**
- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
---
### Task 5: Etape 1 build + in-game test
- [ ] **Step 1:** bump `modDesc.xml` `<version>` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'` shows 1.58.0.0.
- [ ] **Step 2:** commit + push gitea main.
- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
---
### Task 6: Ren statusfunktion for marker
**Files:**
- Modify: `FS25_ADSmartPickup/adFieldView.lua`
- Test: `tests/test_fieldStatus.lua` (ny)
**Interfaces:**
- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
```lua
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
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
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
(If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
```lua
-- ---- 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,
}
-- s = {hasMarker, working, 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.awaiting or s.startRequested then return "waiting" end
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
if s.reasonKey ~= nil then return "waiting" end
return "stopped"
end
```
- [ ] **Step 4: Run all tests** — green.
- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
---
### Task 7: Fields-fanen som farvet liste + "field" i vinduet
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
- Modify: `l10n/*.xml`
**Interfaces:**
- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
```xml
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
<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>
```
The header row (column titles) passes a 7th title `""`.
- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
```lua
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),
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
reasonKey = reason})
end
function SmartPickupFrame:fieldActionText(id)
local field = ADFieldJobs.getField(id)
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
end
```
(If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
```lua
self:makeRowClickable(cells[1].parent, function(frame)
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
frame:toggleField(fieldId)
else
frame.selectedFieldId = fieldId
frame:openEditPanel("field", fieldId)
end
end)
```
Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
- [ ] **Step 4: Panel kind** —
```lua
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
canDelete = false,
exists = function(frame, id) return ADFields.getInfo(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)) end,
status = function(frame, adEnv, id)
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
end,
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
startStop = function(frame, id) frame:toggleField(id) end,
delete = function() end,
}
```
`fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
`getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
- [ ] **Step 6: Run all tests + XML check.**
- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
---
### Task 8: Etape 2 build + in-game test
- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.
@@ -0,0 +1,145 @@
# Guider til opsætning: marker og flows — design
Dato: 2026-09-26 · Status: udkast til godkendelse · Mod: FS25_ADSmartPickup (v1.30.4.0 → v1.31/v1.32)
Leveres i to etaper, der hver kan testes for sig:
- **Etape A (v1.31):** fælles guide-ramme + Fields-fanen ("Opsæt mark").
- **Etape B (v1.32):** Flows-fanen på samme ramme ("Opsæt flow"), så de to faner er ens at bruge.
## Formål
Fields-fanen er blevet en lang liste af valg, der er svær at overskue (brugerens ord: "uoverskueligt").
Målet er, at man sætter en mark op med **ét spørgsmål ad gangen**, og at alt, der kan afgøres automatisk,
aldrig bliver spurgt om. En eksisterende opsætning skal kunne rettes uden at gå hele guiden igennem.
Succes: brugeren kan sætte F22 op fra bunden på under et minut uden at scrolle, og kan rette ét valg
(fx lager) med to tryk.
## Hvad brugeren ser
### Oversigt (fanens standardvisning)
1. Marktabellen som i dag (Mark · Afgrøde · Plan · Kørsel · Status (kort) · Maskiner).
2. Når en mark er valgt: et **resumé** på én linje pr. emne, hver med sin egen række man kan klikke for at
rette det ene trin ("Ret"):
- Afgrøde: `Behold det der gror` / `-> Hvede`
- Afgrødens valg: `Ensilage, river` / `Halm hakkes`
- Klargøring: `Automatisk` / `Plov, kalk`
- Pleje: `Gødning + ukrudt`
- Maskiner: `3 faste` / `Ledige maskiner` · `1 ad gangen, 1 aflæsningsvogn`
- Lager: `Marksilo` / `Automatisk` / `F22 Silo`
- Status (hele teksten, ombrudt)
3. Knapper i bundlinjen (når en mark er valgt): **[Opsæt mark]**, **[Start nu / Stop]**, **[Markflåde]**.
Uden valgt mark: **[Markflåde]**.
4. Den lange detaljeliste og den fælles markflåde nederst forsvinder fra oversigten.
### Guide (erstatter fanens indhold, mens den er åben)
Ét trin pr. skærm: overskrift ("Trin 3 af 7 — Græs: ensilage eller hø?"), en kort forklaring, valgene som
rækker, og bundknapperne **[Næste]** (eller **[Gem]** på sidste trin), **[Tilbage]**, **[Annullér]**.
Trin der ikke er relevante for marken springes over, og tællingen følger de viste trin.
| # | Trin | Vises når | Valg | Standard |
|---|------|-----------|------|----------|
| 1 | Mark | guiden er åbnet uden valgt mark | liste over marker (nr, afgrøde, status) | — |
| 2 | Afgrøde | altid | "Behold det der gror" + afgrøder der kan sås | nuværende plan |
| 3 | Afgrødens valg | slåafgrøde (nu eller mål): ensilage/hø + river ja/nej. Høstafgrøde med halm: hakkes/samles/presses | se "Vises når" | nuværende plan |
| 4 | Klargøring | altid (efter høst / bar mark) | "Automatisk (anbefalet)" / "Vælg selv" → plov, kultivering, kalk, tromle, mulch | Automatisk |
| 5 | Pleje | afgrøde der gror | gødning ja/nej, ukrudt ja/nej | ja/ja |
| 6 | Maskiner | altid | "Faste maskiner til marken?" ja/nej; ja → liste (kun ledige + dem der allerede er faste her); antal ad gangen (1–5); aflæsningsvogne ved høst (1–5) | nej / 1 / 1 |
| 7 | Lager | høst/slåning | marksilo findes: "Brug marksiloen?" ja/nej; ellers/nej: "Automatisk (anbefalet)" / "Vælg selv" → de 8 nærmeste siloer | Automatisk |
| 8 | Oversigt | altid | resumé med [Ret] pr. linje; [Kør automatisk] (Auto) / [Gem uden at starte] (Manual) | — |
### Redigering
- **[Opsæt mark]** på en opsat mark åbner guiden med de nuværende valg udfyldt.
- **[Ret]** på en resumé-linje (oversigt eller trin 8) åbner kun det ene trin; **[Gem]** der gemmer og går
tilbage dit man kom fra.
- **[Annullér]** kasserer ændringer i guiden (intet gemmes før [Gem]/[Kør automatisk]).
### Markflåde
Egen visning (knap **[Markflåde]**): "Brug alle ledige maskiner" + listen med flueben, "fast på F22",
"i gang på F108", bredde-advarsler. Samme indhold som i dag, flyttet ud af markens detaljer.
## Hvad der ALDRIG spørges om (automatisk)
Scanning af marken og markøren, hvornår der høstes/slås/gødes/sprøjtes, påfyldning (eget lager før køb),
tømning ved forkert vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter, gæringssilo ved
ensilage, marksilo-tømning (advarsel hvis intet flow tømmer den).
## Etape B: Flows-fanen på samme måde
Brugerens ønske: "det samme for flow opsætningen så det bliver ensrettet".
**Oversigt:** flow-tavlen som i dag (én linje pr. flow med status). Valgt flow → resumé med [Ret]-linjer
(Retning · Fra · Til/Lager · Varer · Regler · Vent ved · Traktorer · Status) og bundknapperne
**[Opsæt flow]**, **[Start/Stop]**, **[Nyt flow]**, **[Omdøb]**, **[Slet]**, **[Start alle / Stop alle]**.
Den lange detaljeliste forsvinder.
**Guiden "Opsæt flow"** — samme trin-skærm, knapper og [Ret]/[Annullér] som markguiden:
| # | Trin | Vises når | Valg | Standard |
|---|------|-----------|------|----------|
| 1 | Navn | nyt flow | tekst (FS25-tekstdialog) | "Flow N" |
| 2 | Retning | altid | Ind til bygning (forsyning) / Ud fra bygning (udkørsel) | nuværende |
| 3 | Fra / Til | altid | bygninger med markør, fast rækkefølge (navn, markør), "Vis alle" | nuværende |
| 4 | Varer | altid | forsyning: "Automatisk (det bygningen mangler)" / "Vælg selv" → varer; udkørsel: pr. vare Sælg / Lager / Begge / Fra | Automatisk / nuværende |
| 5 | Lager | udkørsel med Lager | "Automatisk (nærmeste med plads)" / "Vælg selv" → lagerbygninger | Automatisk |
| 6 | Regler | altid | udkørsel: sælg ved pris-%, lager over %, samlerunde ja/nej; forsyning: antal ture, retur fra Wait under % | nuværende / standard |
| 7 | Vent ved | altid | Wait-puljer | nuværende |
| 8 | Traktorer | altid | ledige traktorer (+ dem i flowet); traktorer i andre flows skjult med linje "N i andre flows" | nuværende |
| 9 | Oversigt | altid | resumé med [Ret]; [Start flow] / [Gem uden at starte] | — |
Automatisk (spørges aldrig): markør ved bygningen, rutetjek, bedste foder først/parallelt foder, læs-bytte,
kildevalg (mindst/fyldeste først), fordeling på flere mål, hvile af tomme kilder, traktornavne efter flowet.
## Arkitektur
- **`adGuide.lua` (ny, ren Lua, testet):** den fælles guide-motor, uafhængig af marker/flows:
trin-liste, aktuelt trin, kladde, næste/tilbage/spring (til [Ret]), "returnér til oversigt", tælling
"Trin 3 af 7" over de synlige trin. Både markguiden og flowguiden er en trin-definition oven på den.
- **`gui/SmartPickupGuide.lua` (ny):** den fælles trin-skærm i menuen (overskrift, forklaring, valgrækker,
bundknapper Næste/Tilbage/Annullér/Gem) — ét sted, så de to guider ser og opfører sig ens.
- **`adFlowGuide.lua` (ny, etape B, ren Lua, testet):** flowguidens trin, valg, standarder og resumé; gemmer
via `ADRunsController.updateRun`/`assignVehicle` som i dag.
- **`adFieldGuide.lua` (ny, ren Lua, testet):** markguidens trin (oven på `ADGuide`).
- `ADFieldGuide.open(field, info, context)` → guide-tilstand `{fieldId, draft, steps, index, returnTo}`;
`draft` er en kopi af markens plan (+ faste maskiner), så intet ændres før gem.
- `ADFieldGuide.steps(draft, info, context)` → listen af synlige trin (tabellen ovenfor).
- `ADFieldGuide.choices(step, draft, info, context)` → rækkerne for trinnet (tekstnøgler + værdier).
- `ADFieldGuide.choose(guide, key, value)` → ny guide med ændret draft (immutabelt).
- `ADFieldGuide.next/back/jump(guide, step)`; `ADFieldGuide.result(guide)` → `{plan, rigIds}` til gem.
- `ADFieldGuide.summary(field, info, context)` → resumé-linjerne `{step, key, args}`.
- `context` = det spil-afhængige: marksilo findes?, siloer i nærheden, maskiner (ledige/faste), afgrøder
der kan sås. Hentes af GUI'et, så modellen kan testes uden spillet.
- **`gui/SmartPickupFieldTab.lua`:** oversigt (tabel + resumé) og guide-visning; bygger rækker ud fra
`ADFieldGuide`, gemmer via `ADFieldJobs.setPlan` og `ADFieldFleet.dedicate`. Fil-størrelse: guide-visningen
lægges i `gui/SmartPickupFieldGuide.lua` (ny), så ingen fil passerer ~800 linjer.
- **`gui/SmartPickupFrame.lua`:** bundknapper for fanen (Opsæt mark, Start/Stop, Markflåde, Næste/Tilbage/
Annullér/Gem i guiden) og routing af klik.
- **Uændret:** `ADFieldModel` (plan-felter og lagring), `ADFieldJobs`, `ADFieldWork`, gemt XML. Guiden er et
nyt ansigt på de samme data — eksisterende opsætninger virker videre.
## Fejl og kanttilfælde
- Mark uden markør: guiden kan køres, men oversigten siger "sæt markøren F22" og [Kør automatisk] gemmer som
Manual indtil markøren findes (samme regel som i dag).
- Maskine der er fast på en anden mark vises ikke i trin 6 (som v1.30.3); linje "N maskiner er faste på
andre marker".
- Marken forsvinder (solgt/bygget over) mens guiden er åben: guiden lukkes, oversigten vises.
- Guiden er åben og menuen lukkes: kladden kasseres.
## Test
- `tests/test_adGuide.lua`: motoren — næste/tilbage over skjulte trin, spring+retur, tælling, annullér.
- `tests/test_adFlowGuide.lua` (etape B): trin pr. retning, varer auto/vælg, lager-trin kun ved Lager, resumé.
- `tests/test_adFieldGuide.lua`: synlige trin pr. marktype (slåafgrøde, høst med halm, bar mark, mål-afgrøde),
standardvalg, udfyldning fra eksisterende plan, `choose` ændrer kun kladden, `result` giver plan + rigIds,
[Ret]-spring og tilbage, "Automatisk" vs "Vælg selv" i klargøring og lager, marksilo-spørgsmålet kun når en
findes, resumé-linjerne.
- Hele suiten skal være grøn; GUI-routing verificeres in-game (testplan-punkt).
## Uden for scope
Vinmarker (egne roller), genoptagelse af Courseplay efter genstart, fanerne Factories/Silos/Barns/Pallet sheds.
@@ -0,0 +1,103 @@
# Marksilo-flow — design
Dato: 2026-09-27 · Status: godkendt i chat, afventer review af denne spec
## Formål
Høsten lægges ofte i siloer ude ved markerne (mellemlager). I dag skal spilleren selv huske at køre varerne
hjem. Marksilo-flowet kigger jævnligt alle marksiloer igennem og sender selv op til **x** ledige vogne ud for at
hente varerne hjem til gårdens lagre.
**Succes:** står der varer i en marksilo, som gården kan tage, bliver de kørt hjem uden at spilleren gør noget,
med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre flows eller spilleren.
## Brugerens valg (fra brainstorm)
| Spørgsmål | Valg |
|---|---|
| Hvor er "hjem"? | Nærmeste af de gårdlagre, spilleren vælger i flowet, der tager varen og har plads |
| Hvad er "x vogne"? | Lånte ledige vogne fra hele gården — ingen fast tilknytning — højst x ude samtidig |
| Hvilke vogne må lånes? | Samme regel og flueben som markarbejdet (`fleetMode` "all"/"ticked", `ADFieldJobs.state.fleet`) |
## Begreber
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
## Flowet
Marksilo-flowet er ÉN fast opsætning pr. savegame — ikke en ny run-type. Beslutning (2026-09-27, under
planlægning): run-editoren (`SmartPickupFrame`) er bygget til præcis to typer (forsyning/udkørsel) mange steder,
og runs har faste vogne, Start/Stop pr. vogn og Wait-puljer, som intet af det passer til lånte vogne. En egen
underfane + egen gemmefil giver langt færre indgreb i eksisterende kode.
Opsætning (`ADSmartPickup_fieldsilos.xml` i savegame-mappen, gemmes sammen med runs):
- `enabled` — til/fra, standard fra
- `storeIds` — gårdlagre (bygnings-id'er fra `ADBuildings`, kun `kind == "silo"`)
- `maxVehicles` — x, 1–10, standard 2
- `minLiters` — tærskel pr. silo og vare: 5000 / 10000 / 25000 / 50000, standard 10000
## Planlægning (ren logik, nyt modul `adFieldSiloPlanner.lua`, testes med luajit)
Input: marksiloer `{id, markerId|nil, stock = {[fillType] = liters}}`, gårdlagre `{id, markerId, accepts =
{[fillType] = freeLiters}, x, z}`, aktive lån `{siloId, fillType, liters}`, ledige vogne `{id, x, z, capacity,
carries = {[fillType] = true}}`, `maxVehicles`, `minLiters`.
1. **Kvalificerede opgaver:** pr. (silo, vare) hvor silo har markør, `liters >= minLiters`, og mindst ét
gårdlager har `freeLiters >= minLiters` for varen. Resten = liters minus det aktive lån på samme (silo, vare)
allerede er på vej med; kun opgaver med rest `>= minLiters` tæller.
Siloer uden markør → status `noMarker`; varer intet gårdlager tager → status `noFarmStore`.
2. **Rækkefølge:** fuldeste rest først; lige → lavest silo-id (stabilt).
3. **Vogne:** ledige pladser = `maxVehicles - #aktive lån`. For hver opgave i rækkefølge, så længe der er
pladser: nærmeste ledige vogn (til siloen), der kan bære varen; resten reduceres med vognens kapacitet;
samme opgave får flere vogne, så længe resten stadig er `>= minLiters`.
4. **Gårdlager pr. lån:** nærmeste gårdlager (til siloen) der tager varen med `freeLiters >= min(vognens
kapacitet, rest)`; ellers det med mest plads; ellers intet (opgaven springes over denne runde).
Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId, liters}}, status = {...}}`.
## Udførelse (`adFieldSilos.lua`)
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
(flows tager allerede kun deres egne tilknyttede vogne).
- **Afsend:** AutoDrive "Hent og aflever": første markør = silomarkør, anden = gårdlagerets markør, vare =
lånets vare, `loopCounter = 1`, parkering ved job slut slået til (`enableParkAtJobFinished`); start som
markarbejdet (`startAd` → mode:start). Log: `marksilo: '<vogn>' henter <liter> l <vare> i <silo> → <lager>`.
- **Frigivelse:** lånet slettes når AD ikke længere er aktivt for vognen (turen og parkeringen er færdig),
når spilleren sætter sig i den, eller når flowet stoppes (stop = AD stoppes på lånte vogne).
- **Vagt:** er vognen ikke kommet i gang (ingen stigning i fyldning og står stille) efter 120 s, stoppes AD,
lånet slettes, og der logges én advarsel pr. (silo, vare), så samme opgave ikke fejler i ring hvert minut
(opgaven får 10 min pause).
- **Genindlæsning:** lån gemmes ikke. Ved indlæsning er alle vogne frie for flowet; en vogn AutoDrive selv
genstarter, kører sin tur færdig som almindelig AD-tur.
## GUI
Ny underfane "Marksiloer" efter "Måned". Indstillinger: gårdlagre (eksisterende vælger),
"Maks. vogne ude" (1–10), "Mindste mængde" (5.000 / 10.000 / 25.000 / 50.000 l), Start/Stop.
Status: `x/y vogne ude · n siloer venter`, og én række pr. marksilo med indhold: silo, vare, liter, på vej,
status (`ok` / `mangler markør` / `ingen gård tager <vare>`). Tekster i `l10n_da` + `l10n_en` (`spu_fs_*`).
## Uden for scope
- Salg direkte fra marksilo (kan senere bruge udkørslens prisregel).
- Faste vogne i flowet (valg 1 i brainstorm) — fravalgt.
- Lån der overlever genindlæsning.
## Test
- `tests/test_adFieldSiloPlanner.lua`: kvalificering (tærskel, markør, gård tager varen), fuldeste først,
x-loft inkl. aktive lån, flere vogne til én stor silo, vogn der ikke kan bære varen springes over, nærmeste
gårdlager med plads, fallback til mest plads, intet lager → ingen afsendelse.
- `tests/test_adFieldSiloConfig.lua`: standarder, grænser (x 1–10, tærskel kun de fire værdier), til/fra af gårdlager, records frem og tilbage.
- In-game: silo ved en mark med 50.000 l hvede, gårdlager = Farma-silo, x = 2 → to vogne kører, afleverer,
parkerer; markarbejdet tager ikke de lånte vogne imens.
@@ -0,0 +1,75 @@
# Flow-vogne — design
Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har
det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`).
## Formål
Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke
længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har
noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen.
## Brugerens valg
| Spørgsmål | Valg |
|---|---|
| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning |
| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor |
| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet |
| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber |
## Begreber
- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af
flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml`
(`wagons="traktorId=vognId;…"`).
- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og
"bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde
stille uden at flowet er stoppet).
- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to
tjek i træk (60 s mellem tjek).
## Adfærd
1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor]
= redskab` (ændringer gemmes med næste gem).
2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn:
- vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect <vognens parkering>", derefter startes
traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn".
- vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s.
- vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør).
3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med
`ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets.
4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke).
5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere
— stoppet, traktorer kørt hjem".
## Puljen og flows
- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**.
- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab.
- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå.
## Komponenter
- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning
væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`,
`sendHome(run)`).
- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records.
- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`,
`startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`).
- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller.
- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken.
## Test
- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af
bygning væk, ejer-opslag (stoppet vs kørende).
- `tests/test_adRuns.lua`: records med `stopped` og `wagons`.
- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter
deres egne vogne og kører.
## Uden for scope
- Valg af vogn i GUI (automatisk er valgt).
- At et flow tager en anden vogn end sin egen.
@@ -0,0 +1,149 @@
# Maskinpulje — design
Dato: 2026-09-28 · Status: godkendt i chat (afsnit 1–4), afventer review af denne spec
## Formål
Plove, harver, kultivatorer, såmaskiner, tømmevogne m.m. bruges kun en gang imellem. I dag skal hvert af dem have
en traktor koblet fast på hele året, fordi markarbejdet kun kan sende en traktor, der allerede har det rigtige
redskab bag sig. Maskinpuljen gør traktorer og redskaber til fælles puljer: et job venter i kø, til en traktor er
ledig; traktoren henter det redskab, jobbet kræver, udfører arbejdet, parkerer redskabet på dets plads og kører
selv hjem til sin egen parkering.
**Succes:** med fx 3 ledige traktorer, 2 plove og 1 kultivator bliver markernes arbejde udført uden at nogen
traktor står låst til et redskab — færre køretøjer at købe og vedligeholde. Flows, der kører hele tiden (fodring,
slurry, mælk …), påvirkes ikke.
## Brugerens valg (fra brainstorm)
| Spørgsmål | Valg |
|---|---|
| Hvordan findes redskabets Connect-markør? | Navneregel: AD-parkering "X" ↔ markør "Connect X" |
| Hvem er i puljen? | Automatisk ud fra opsætningen — ingen afkrydsning |
| Hvilken traktor henter? | Nærmeste ledige med nok hk |
| Omfang | Markarbejde + tømmevogne ved høst + marksilo-flowet. Flows er undtaget |
| Tømmevogne ved høst | Også fra puljen, men dedikeret til marken, til høsten dér er færdig |
| Arkitektur | Udvid markarbejdets eget maskinvalg (ikke en separat puljemanager) |
## Begreber
- **Puljeredskab:** et redskab eller en vogn, der holder **frakoblet**, har en AD-parkering (`advd.parkDestination`
→ markør "X"), og hvor der findes en markør "Connect X" (`ADHitch.parseMarker` = attach, navn = X; store/små
bogstaver ligegyldigt). Uden Connect-markør er redskabet ikke i puljen — det er spillerens måde at holde det
udenfor.
- **Puljevogn:** puljeredskab med rollen `unloader` (vogn med tip/snegl, `spec_trailer` + `spec_dischargeable`).
- **Puljetraktor:** motoriseret køretøj uden noget koblet bag på (`ADHitch.rearImplements` tom), ikke i et flow
(`ADRunsController.getRunForVehicle == nil`), ikke optaget (samme `busy`-regel som `getRigs`), ikke styret af
spilleren, med egen AD-parkering (`vehicle.advd.parkDestination >= 1`) og flueben i markflåden
(`ADFieldFleet.isEnabled`).
- **Færdig kombination:** det markarbejdet bruger i dag — traktor med redskabet allerede koblet på (`getRigs`).
- **Puljekombination:** {traktor, redskab, Connect-markør-id, redskabets parkering-id, traktorens parkering-id}.
AD-parkering: hver maskine har sin egen `advd.parkDestination`; AD bruger det valgte redskabs, når et er koblet på
(`AutoDriveVehicleData.lua:88-95`). Traktorens egen parkering læses derfor altid på traktoren selv.
## Komponenter
### `adPool.lua` (nyt, ren logik + tynd spil-adapter)
Ren logik (testes med luajit):
- `ADPool.roleOf(specs)` — rolle for et frakoblet redskab ud fra dets egne specs via `ADFieldFleet.ROLE_SPECS`;
vogn med `spec_trailer` + `spec_dischargeable` og intet andet → `unloader`.
- `ADPool.isPoolImplement(item)` — `item = {attached, parkMarkerName, connectMarkerId}` → frakoblet, med parkering
og Connect-markør.
- `ADPool.powerOk(tractorPower, neededPower)` — true når et af tallene mangler (nil/0), ellers `tractorPower >=
neededPower`.
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, reserved, accept)` — redskab med rollen nærmest marken
(ikke reserveret, `accept(implement)` sand), derefter nærmeste ledige traktor til redskabet med `powerOk`.
Returnerer `pair` eller `nil, why, args` (`noPoolImplement` / `noPoolTractor` / `tooWeak` med hk-tal).
- `ADPool.queueOrder(fields)` — markers ventende rækkefølge: `waitingSince` stigende, lige → lavest mark-id.
- `ADPool.isReturnDue(rearImplements, poolParkIds)` — traktor har et puljeredskab bag sig (dets parkering har en
Connect-markør) → hjemtur skal køres.
Spil-adapter:
- `ADPool.collect(adEnv)` → `implements, tractors` fra `g_currentMission.vehicleSystem.vehicles` (gårdens), med
position, rolle, specs til `acceptFor`, hk og parkering/Connect-id.
- hk: `g_storeManager:getItemByXMLFilename(vehicle.configFileName).specs` → traktor `power`, redskab
`neededPower`; mangler de, bruges nil (→ tilladt).
- Reservationer: `ADPool.reserve(implementId, tractorId, owner)` / `release(owner)`; i hukommelsen, ryddes ved
genindlæsning (se Genindlæsning).
- Karantæne: redskab, der ikke kunne kobles på, springes over i 10 min (`ADPool.QUARANTINE_MS = 600000`).
### `adHitch.lua` (findes, v1.47.4 — udvides let)
Udfører kørslerne: `fetch` (kør til Connect + kobl på), `route` (trin-tur, afkobling ved redskabets parkering).
Ny: `ADHitch.returnHome(adEnv, tractor, onDone)` = `route({redskabets parkering, traktorens parkering})`; ved
parkeringen kobles puljeredskabet af (samme regel som i dag i ruter).
### Markarbejdet (`adFieldWork.lua`, `adFieldJobs.lua`)
- `dispatch`: som i dag først færdige kombinationer. Returnerer `pick`/`pickMany` intet (`noRig`/`allBusy`/
`cannotDo`), forsøges `ADPool.pairFor` for de manglende. Puljekombinationens assignment får `phase = "fetching"`,
`pool = {implementId, connectId, implementParkId, tractorParkId}`, nøgle = traktorens id.
- `follow`: fase `fetching` → når `ADHitch.fetch` melder ok: bredde tjekkes med redskabet på (`checkWidth`; for
bred → hjemtur + årsag `tooWide`), derefter samme vej som i dag (`startAd` til markens markør / påfyldning,
`phase = "driving"`). Fejl → hjemtur, karantæne, markens `retryAt` som ved `noRig`.
- `finishAssignment`: har traktoren et puljeredskab bag sig (`ADPool.isReturnDue`) → `ADHitch.returnHome` i
stedet for den almindelige parkeringstur; fase `returning`; assignment slettes, når hjemturen er færdig (eller
fejler — så med advarsel).
- Kø: `tick` gennemgår ventende marker i `ADPool.queueOrder`. `waitingSince` sættes, når en mark første gang ikke
fik en maskine, og nulstilles ved udsendelse. `DISPATCH_GAP_MS` (10 s) beholdes.
- Fanen viser faserne "henter X" og "sætter X af" og årsagerne `noPoolTractor`/`tooWeak`.
### Tømning ved høst
- Helper-dispatch (`role = "unloader"`, `helper = true`) bruger samme fallback: puljevogn + puljetraktor.
- Efter `fetch`: `startUnloader` som i dag. Assignmentet bliver hos marken, til høststadiet er slut (som i dag);
først da `returnHome`. Ingen frigivelse mellem læs.
### Marksilo-flowet (`adFieldSilos.lua`, `adFieldSiloPlanner.lua`)
- `collectVehicles` tilføjer puljekombinationer (puljevogn der kan bære varen + nærmeste puljetraktor) som
kandidater med `pool = {...}`; planneren behandler dem som vogne (position = vognens).
- `withoutHarvestReserve` tæller puljevogne med, så høsten går først.
- Lån med `pool`: `ADHitch.fetch` → AD "Hent og aflever" som i dag (`enableParkAtJobFinished`) → når AD er
inaktiv: `returnHome` → lånet slettes, når traktoren er hjemme. Vagten (120 s uden fremgang) starter efter
koblingen.
## Genindlæsning
Reservationer og assignments gemmes ikke (som i dag). Efter indlæsning ser en traktor med puljeredskab ud som en
færdig kombination; når markarbejdet eller marksilo-flowet afslutter et job med den, køres `returnHome`. Reglen
afhænger kun af, hvad der hænger bag traktoren, ikke af gemt tilstand. En traktor, som ingen af vores jobs bruger,
køres aldrig hjem af sig selv (spilleren kan have koblet et puljeredskab på for at køre selv).
## Fejl
| Situation | Handling |
|---|---|
| Kobling fejler (`noAttachable`, `stoppedAway`) | Traktor hjem, redskab i karantæne 10 min, marken prøver igen (andet redskab eller venter) |
| Redskab for bredt når koblet på | Hjemtur, årsag `tooWide` i fanen |
| Spilleren sætter sig i traktoren | Assignment afbrydes som i dag; reservationer frigives |
| Ingen traktor stærk nok | Årsag `tooWeak` med hk-tal: "ingen traktor med ≥180 hk ledig til 'Plov 1'" |
| Hjemtur fejler | Advarsel i loggen; traktor og redskab frigives (spilleren må ordne det) |
## Undtaget / uden for scope
- Flows (runs) — deres køretøjer er aldrig i puljen (uændret regel i `getRigs`).
- Vogne uden Connect-markør — dedikerede som i dag.
- Genbrug af et redskab direkte til næste mark uden at parkere (senere optimering).
- Flere redskaber pr. traktor (front + bag).
- Egen GUI-side for puljen.
## Etaper
1. **v1.48.0.0 — markarbejde med pulje:** `adPool.lua`, `returnHome`, dispatch-fallback, fase fetching/returning,
kø-rækkefølge, fane-tekster. In-game: savegame4, mark der skal harves/pløjes, redskab med parkering + Connect,
solo-traktor henter → arbejder → parkerer → hjem.
2. **v1.49.0.0 — tømmevogn ved høst fra puljen.**
3. **v1.50.0.0 — marksilo med puljevogne** (inkl. høst-reserve over puljevogne).
## Test
- `tests/test_adPool.lua`: puljeredskab ja/nej (koblet på, uden parkering, uden Connect), `roleOf` inkl.
`unloader`, `powerOk` (nok/for lidt/manglende data), `pairFor` (redskab nærmest marken, traktor nærmest redskabet,
reserveret og karantæne springes over, `tooWeak`/`noPoolTractor`/`noPoolImplement`), `queueOrder`,
`isReturnDue`.
- `tests/test_adHitch.lua`: `returnHome`-rutens trin.
- `tests/test_adFieldJobs.lua`: færdig kombination foretrækkes; fallback til pulje; kø længst ventende først.
- `tests/test_adFieldSiloPlanner.lua` (etape 3): puljekandidater og høst-reserve med puljevogne.
@@ -0,0 +1,131 @@
# Flows og Fields: farvet liste + redigeringsvindue
Dato: 2026-09-29 · Status: godkendt i chat ("men kør"), afventer review af denne spec
## Formål
Flows- og Fields-fanen er i dag én lang rullende side: listen over flows/marker efterfulgt af den valgte
posts redigeringslinjer. Det er svært at overskue (brugerens screenshot 29/9 17:15). Målet:
- Fanen viser **kun listen**, med farvet status og **Start/Stop pr. række**.
- **Klik på en række** åbner et **redigeringsvindue** (popup) oven på listen med de nuværende linjer.
Succes = man kan se med ét blik hvad der kører/venter/fejler, starte/stoppe en post uden at vælge den først,
og redigere i et vindue der lukkes med Luk/ESC — uden at noget af flow-/mark-logikken ændres.
## Ikke-mål
- Ingen ændring af flow-/mark-logik, guiderne ("Nyt flow", "Opsæt mark") eller deres trin.
- Oversigt-tavlen og Fields' flåde-visning er uændrede.
- Ingen ny FS25-GUI-skærm/dialog (fravalgt: al rækkekode skulle genbygges; fokus/ESC oven på ESC-menuen er
skrøbeligt). "Popup" er et panel inde i samme frame.
## Brugeroplevelse
### Flows-fanen (liste)
Én række pr. flow: `● navn · varer · kørende/i alt · statustekst · [Start]/[Stop]`.
| Niveau | Prik/statusfarve | Hvornår |
|---|---|---|
| running | grøn | mindst én traktor kører og ingen er blokeret |
| waiting | gul | startet, men venter: ingen varer at hente, venter på vogn, traktor optaget, holder ved Wait |
| error | rød | kan ikke køre: sidste start afvist (fx ingen vogn), advarsel (`!!`) |
| stopped | grå | stoppet / aldrig startet |
- `[Start]/[Stop]` bruger den eksisterende `ADRunsController.startRun/stopRun` (samme regler som knaplinjen, inkl.
sprunget-over-besked i højre side).
- Klik på rækken (ikke knappen) åbner redigeringsvinduet for flowet.
- Knaplinjen (i dag Tilbage, Start/Stop, Opsæt flow, Nyt flow, Slet) bliver: Tilbage, Nyt flow. Start/Stop sidder på
rækkerne; "Opsæt flow" og "Slet" flyttes ind i vinduet.
### Fields-fanen (liste)
Nuværende kolonner (Mark, Afgrøde, Plan, Kørsel, Status, Traktorer) + `●` forrest og `[Start]/[Stop]` bagerst.
| Niveau | Hvornår |
|---|---|
| running (grøn) | marken har aktive opgaver der arbejder |
| waiting (gul) | startet men venter: på maskine, vejr, påfyldning, "startes når en maskine er ledig" |
| error (rød) | mangler markør, arbejdsbredde for bred/ukendt, ingen egnet maskine |
| stopped (grå) | ingen plan / stoppet |
- `[Start]/[Stop]` genbruger `onClickFieldStartStop` (inkl. "Overtag høster").
- Klik på rækken åbner vinduet med markens resumé og [Ret]-linjer.
### Redigeringsvinduet (fælles)
- Mørk halvgennemsigtig baggrund over listen; centreret panel (~70 % bredde, ~80 % højde).
- Top: titel ("Rediger: Cow Food" / "Mark F12"), statuslinje i niveau-farve.
- Midte: egen rullende liste med præcis det indhold, der i dag vises under listen:
flow → `fillFlowSummary`-linjerne; mark → `fillFieldOverview`-linjerne. [Edit]/[Ret] åbner trinnet
(guiden i enkelt-trin-tilstand) **inde i vinduet** med Gem/Tilbage som i dag.
- Bund: `Luk` · `Start/Stop` · `Slet` (kun flow). ESC/Tilbage lukker vinduet (ikke menuen); står et trin åbent,
går ESC først tilbage til resuméet.
- Mens vinduet er åbent, opdateres statuslinjen og listen bagved hvert sekund som i dag.
## Arkitektur
### Rene funktioner (testbare)
- `ADFlowView.flowStatus(s)` → `{level, text}` hvor `s = {running, total, canStop, lastStartSkipped, warning,
waitReason, holdReason, stopped}`. Afgør niveau efter tabellen ovenfor.
- `ADFieldView.fieldStatus(s)` → `{level, text}` hvor `s = {working, awaiting, starting, missingMarker,
widthProblem, noMachine, hasPlan}`.
- `ADFlowView.LEVEL_COLORS` (grøn/gul/rød/grå) — flyttes/udleveres fra `SmartPickupStorageView`s lokale tabel,
som også eksporterer `setTextColor` til genbrug.
### GUI
- **XML** (`SmartPickupFrame.xml`): efter `boxLayout` et `GuiElement id="editOverlay"` (skjult) med
`Bitmap id="editBackdrop"` (fuld flade, sort ~60 %), `Bitmap id="editPanel"` (centreret, baggrund som
fs25-paneler) med `Text id="editTitle"`, `Text id="editStatus"`, `ScrollingLayout id="editLayout"` (+ slider) og
en knaplinje med tre `Button`s (`editClose`, `editStartStop`, `editDelete`).
- **Rækkeprefabs**: `flowPrefab` og `fieldPrefab` får en `Bitmap name="dot"` (profil `spuDot`) forrest og en
`Button name="rowAction"` bagerst. Kolonnebredder justeres så rækken stadig er ≈835 px.
- **Mål-layout for rækker**: `addRow`/`addTextRow`/`addCellRow`/`addLinkRow`/`addGuideRow` kloner i dag altid ind i
`self.boxLayout`. Der indføres `self.rowTarget` (standard `boxLayout`); vinduet sætter det til `editLayout`
mens det fyldes. `clearRows` holder klonerne pr. layout, så listen og vinduet kan genopbygges hver for sig.
- **Ny fil** `gui/SmartPickupEditPanel.lua` (mixin på `SmartPickupFrame`, `source()`s som de andre):
`openEditPanel(kind, id)`, `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`,
knap-callbacks. `kind ∈ {"flow","field"}`.
- **Input**: `backButtonInfo`-callback og ESC tjekker først `self.editPanelOpen` (luk trin → luk vindue →
normal Tilbage). Klik på backdrop gør intet (blokerer klik til listen bagved via `mouseEvent` der returnerer
"brugt").
- **Guide i vinduet**: `openFlowStep`/`openFieldStep` sætter `returnTo = "panel"`, så Gem/Tilbage vender tilbage til
vinduets resumé i stedet for at lukke guiden til fanen.
### Dataflow
Klik på række → `openEditPanel(kind, id)` → `selectedRunId`/`selectedFieldId` sættes (eksisterende felter) →
`requestRebuild` → `rebuild` tegner listen i `boxLayout` og, når vinduet er åbent, vinduets indhold i
`editLayout`. Start/Stop i række eller vindue kalder de eksisterende controller-funktioner og `requestRebuild`.
## Fejlhåndtering
- Alt nyt GUI-arbejde kører i `self:safe(...)` som resten af frame'n; fejl vises i statuslinjen og logges.
- Forsvinder flowet/marken mens vinduet er åbent (slettet), lukkes vinduet og statuslinjen siger hvorfor.
- `rowAction`-knappen er deaktiveret når handlingen ikke er mulig (fx mark uden markør → ingen Start).
## Test
- luajit: `tests/test_flowStatus.lua` og `tests/test_fieldStatus.lua` for alle niveauer i tabellerne +
grænsetilfælde (0 traktorer, stoppet med advarsel, venter + afvist).
- Eksisterende tests skal forblive grønne (`test_noStrayGlobals` dækker den nye fil).
- In-game (brugeren): liste med farver, Start/Stop pr. række, åbn/luk vindue med klik/Luk/ESC, [Edit] → Gem inde i
vinduet, Slet flow fra vinduet, klik på listen bag vinduet gør intet, mark "Overtag høster" virker fra rækken.
## Levering
To etaper, hver sin version og in-game test:
1. **Flows**: rene statusfunktioner + fælles vindue + Flows-liste.
2. **Fields**: mark-status + Fields-liste genbruger vinduet.
## Risici
- **Klik gennem backdrop** til listen: afbødes ved at backdrop'en fanger `mouseEvent`; fallback: skjul listens rækker
(`setVisible(false)`) mens vinduet er åbent.
- **Musehjul/rulning** i to ScrollingLayouts: hver får sin egen slider; FS25 ruller det layout musen er over.
- **Knapper i rækkeprefabs** (Button i en klonet Bitmap) er ikke brugt før i modden: onClick sættes efter
`clone` pr. række (som `makeRowClickable`), ikke via XML-callback.
+16
View File
@@ -234,5 +234,21 @@ do
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71) check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
end end
-- pickLoadMarker: en pallespawner viger for aflæsningen, når begge vil have samme markør og aflæsningen står
-- tættere (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food", foderindkast 5 m -> Goat Food fik "Sheep Milk")
do
local food, milk = {id = 1, name = "Sheep Food 1", distance = 9}, {id = 2, name = "Sheep Milk 1", distance = 13}
local unloadBest = {id = 1, name = "Sheep Food 1", distance = 5}
check("PL spawner viger for tættere aflæsning", ADBuildings.pickLoadMarker({food, milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger beholder markøren (Sugarbeet Crusher)", ADBuildings.pickLoadMarker({food, milk}, unloadBest, false).name, "Sheep Food 1")
-- in-game 27/9 fårestald 3 og 5: spawner 5 m fra "Sheep Food", foderindkast 9 m -> spawneren vandt stadig
check("PL spawner viger også når den selv står tættest", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger tættest beholder", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, false).name, "Sheep Food 1")
check("PL ingen konflikt", ADBuildings.pickLoadMarker({milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL eneste kandidat er aflæsningens -> ingen læssemarkør", ADBuildings.pickLoadMarker({food}, unloadBest, true), nil)
check("PL ingen kandidater", ADBuildings.pickLoadMarker({}, unloadBest, true), nil)
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+40
View File
@@ -0,0 +1,40 @@
-- Kør: luajit tests/test_adCache.lua (fra repo-roden)
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
g_currentMission = {time = 0}
dofile("FS25_ADSmartPickup/adCache.lua")
local calls = 0
local function lookup() calls = calls + 1; return "station" end
check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation")
check("K1 to opslag", calls, 2)
ADCache.enabled = true
calls = 0
ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup)
check("K2 genbrugt", calls, 1)
g_currentMission.time = 900
ADCacheGet("b", "k", 1000, lookup)
check("K2 inden for levetid", calls, 1)
g_currentMission.time = 1600
ADCacheGet("b", "k", 1000, lookup)
check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2)
local nothing = 0
ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end)
check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil)
check("K4 kun ét opslag", nothing, 1)
-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig
g_currentMission.time = 10000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end
local expired = 0
g_currentMission.time = 22000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end
check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true)
ADCache.clear()
check("K6 ryddet", next(ADCache.buckets), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+195
View File
@@ -0,0 +1,195 @@
-- Kør: luajit tests/test_adCourseCache.lua (fra repo-roden)
-- Kursus-registret: hvilken mark/bredde/markform/indstillinger en maskines nuværende Courseplay-kursus blev
-- lavet til. Genbrug kun når ALT matcher; egen maskine først, ellers en anden maskines kursus (kopi).
dofile("FS25_ADSmartPickup/adCourseCache.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 count(map)
local n = 0
for _ in pairs(map) do n = n + 1 end
return n
end
local C = ADCourseCache
local SIG = "n=4;a=960000;c=800,300"
-- ---- indstillings-nøglen ----
check("K1 standard", C.settingsKey({}), "off=0.0;lane=-;multi=1;vine=-;skip=-")
check("K2 nil = standard", C.settingsKey(nil), C.settingsKey({}))
check("K3 alt sat", C.settingsKey({toolOffset = 1.25, lane = -1, multiTools = 2, vineOffset = -1, vineSkip = 1}),
"off=1.3;lane=-1;multi=2;vine=-1.0;skip=1")
check("K4 -0.0 = 0.0", C.settingsKey({toolOffset = -0.0001}), C.settingsKey({toolOffset = 0}))
check("K5 float-støj i forskydning tæller ikke", C.settingsKey({toolOffset = 1.2000001}), C.settingsKey({toolOffset = 1.2}))
check("K6 anden bane = anden nøgle", C.settingsKey({multiTools = 2, lane = 1}) ~= C.settingsKey({multiTools = 2, lane = -1}), true)
check("K7 vin-offset indgår", C.settingsKey({vineOffset = 0}) ~= C.settingsKey({vineOffset = -1}), true)
check("K8 vin-skip indgår", C.settingsKey({vineOffset = -1, vineSkip = 0}) ~= C.settingsKey({vineOffset = -1, vineSkip = 1}), true)
local settings = C.settingsKey({})
local request = {field = 22, width = 30, sig = SIG, settings = settings}
local own = C.newRecord({vehicle = "v1", field = 22, width = 30.0, sig = SIG, settings = settings, waypoints = 812})
-- ---- key/matches ----
check("M1 samme alt", C.matches(own, request), true)
check("M2 bredde som float32 fra XML (30.0000001)", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30.0000001, sig = SIG, settings = settings}), request), true)
check("M3 ny bredde", C.matches(own, {field = 22, width = 12, sig = SIG, settings = settings}), false)
check("M4 anden mark", C.matches(own, {field = 23, width = 30, sig = SIG, settings = settings}), false)
check("M5 form ændret", C.matches(own, {field = 22, width = 30, sig = "n=5;a=960000;c=800,300", settings = settings}), false)
check("M6 andre indstillinger", C.matches(own, {field = 22, width = 30, sig = SIG, settings = C.settingsKey({toolOffset = 2})}), false)
check("M7 ukendt form (nil) matcher aldrig", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30, settings = settings}), {field = 22, width = 30, settings = settings}), false)
check("M8 intet register", C.matches(nil, request), false)
check("M9 key", C.key(request), "22|30.0|" .. SIG .. "|" .. settings)
check("M10 key uden form = nil", C.key({field = 22, width = 30, settings = settings}), nil)
-- ---- grund til ny beregning ----
check("R1 intet gemt", C.mismatchReason(nil, request), "intet kursus gemt")
check("R2 form ændret", C.mismatchReason(own, {field = 22, width = 30, sig = "x", settings = settings}), "markens form ændret")
check("R3 ny bredde", C.mismatchReason(own, {field = 22, width = 12, sig = SIG, settings = settings}), "ny bredde")
check("R4 anden mark", C.mismatchReason(own, {field = 9, width = 30, sig = SIG, settings = settings}), "kursus til en anden mark")
check("R5 indstillinger", C.mismatchReason(own, {field = 22, width = 30, sig = SIG, settings = "x"}), "andre indstillinger")
check("R6 ukendt form", C.mismatchReason(own, {field = 22, width = 30, settings = settings}), "markens form ukendt")
-- ---- register: immutabelt ----
local records = C.withRecord({}, own)
check("I1 tilføjet", records.v1 ~= nil, true)
local other = C.newRecord({vehicle = "v2", field = 22, width = 30, sig = SIG, settings = settings, waypoints = 812})
local both = C.withRecord(records, other)
check("I2 original urørt", count(records), 1)
check("I3 begge", count(both), 2)
check("I4 without", count(C.without(both, "v1")), 1)
check("I5 without urørt original", count(both), 2)
check("I6 newRecord uden vehicle = nil", C.newRecord({field = 22}), nil)
-- ---- findReusable ----
local rec, how = C.findReusable(both, request, "v1")
check("F1 egen maskine først", rec and rec.vehicle, "v1")
check("F2 egen = own", how, "own")
rec, how = C.findReusable(both, request, "v3")
check("F3 anden maskines kursus", rec and rec.vehicle, "v1")
check("F4 = copy", how, "copy")
-- egen maskines kursus er til en anden bredde -> kopi fra v2
local mixed = C.withRecord(both, C.newRecord({vehicle = "v1", field = 22, width = 12, sig = SIG, settings = settings}))
rec, how = C.findReusable(mixed, request, "v1")
check("F5 egen passer ikke -> kopi fra v2", rec and rec.vehicle, "v2")
check("F6 kopi", how, "copy")
-- intet passer -> grunden fra egen post
local changed = {field = 22, width = 30, sig = "n=6;a=1;c=0,0", settings = settings}
rec, how = C.findReusable(both, changed, "v1")
check("F7 intet passer", rec, nil)
check("F8 grund: form ændret", how, "markens form ændret")
rec, how = C.findReusable({}, request, "v1")
check("F9 tomt register", how, "intet kursus gemt")
-- gyldighedstjek: egen maskines kursus er væk/ændret -> kopi fra v2; alle ugyldige -> ny
rec, how = C.findReusable(both, request, "v1", function(r) return r.vehicle ~= "v1" end)
check("F10 egen ugyldig -> kopi", rec and rec.vehicle, "v2")
rec, how = C.findReusable(both, request, "v1", function() return false end)
check("F11 alle ugyldige", rec, nil)
check("F12 grund: kurset på maskinen er ændret", how, "kurset på maskinen er ændret")
-- multi-tool: lederens kursus (2 sæt, bane -1) genbruges ikke til et enkelt sæt
local multi = C.newRecord({vehicle = "v1", field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})})
rec = C.findReusable(C.withRecord({}, multi), request, "v1")
check("F13 multi-kursus != enkelt", rec, nil)
rec, how = C.findReusable(C.withRecord({}, multi), {field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})}, "v1")
check("F14 multi samme bane genbruges", how, "own")
-- deterministisk: flere kandidater -> laveste id
local many = C.withRecord(C.withRecord({}, C.newRecord({vehicle = "v9", field = 22, width = 30, sig = SIG, settings = settings})),
C.newRecord({vehicle = "v4", field = 22, width = 30, sig = SIG, settings = settings}))
rec = C.findReusable(many, request, "v1")
check("F15 deterministisk valg", rec and rec.vehicle, "v4")
-- ---- invalidateField ----
local fieldRecords = C.withRecord(both, C.newRecord({vehicle = "v5", field = 7, width = 30, sig = "n=3;a=50;c=3,3", settings = settings}))
local kept, removed = C.invalidateField(fieldRecords, 22, "n=5;a=1;c=0,0")
check("V1 to poster for marken slettet", removed, 2)
check("V2 anden mark bevaret", kept.v5 ~= nil, true)
check("V3 original urørt", count(fieldRecords), 3)
kept, removed = C.invalidateField(fieldRecords, 22, SIG)
check("V4 samme form: intet slettes", removed, 0)
check("V4b samme tabel tilbage", kept, fieldRecords)
kept, removed = C.invalidateField(fieldRecords, 22, nil)
check("V5 ukendt form: intet slettes", removed, 0)
kept, removed = C.invalidateField(nil, 22, SIG)
check("V6 nil-register", removed, 0)
-- ---- lim: prepare/afterStart/checkField med falske spil-objekter ----
dofile("FS25_ADSmartPickup/adCpBridge.lua")
local saves, logs = 0, {}
ADFieldJobs = {state = {courseRecords = {}}, save = function() saves = saves + 1 end,
log = function(_, fmt, ...) table.insert(logs, string.format(fmt, ...)) end}
ADRuns = {vehicleLabel = function(v) return v.label end}
Logging = {warning = function(...) print("WARN", ...) end}
local function fakeCourse(n)
return {getNumberOfWaypoints = function() return n end}
end
local function fakeVehicle(id, width, course)
return {label = id, course = course,
getFieldWorkCourse = function(self) return self.course end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return width end}} end}
end
local tractorA = fakeVehicle("A", 30, nil)
local tractorB = fakeVehicle("B", 30, nil)
ADRunsController = {getFarmVehicles = function() return {tractorA, tractorB} end,
getVehicleId = function(v) return v.label end}
local fieldObj, info = {id = 22}, {shapeSig = SIG, fruitName = "WHEAT"}
local assignA = {vehicle = tractorA, vehicleId = "A", role = "cultivator"}
-- G1: intet gemt -> ny beregning, opts urørt (ingen reuse/course)
local opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G1 ny beregning: ingen reuse", opts.reuse, nil)
check("G1b ingen kursus", opts.course, nil)
check("G1c log: nyt kursus (intet gemt)", logs[#logs]:find("nyt kursus beregnes", 1, true) ~= nil and logs[#logs]:find("intet kursus gemt", 1, true) ~= nil, true)
-- CP har lavet kurset (812 punkter) -> registreres
tractorA.course = fakeCourse(812)
C.afterStart(tractorA, "A", ctx)
check("G2 registreret", ADFieldJobs.state.courseRecords.A ~= nil and ADFieldJobs.state.courseRecords.A.waypoints, 812)
check("G2b gemt til fil", saves, 1)
-- G3: næste gang -> genbrug
opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G3 genbrug", opts.reuse, true)
check("G3b log genbrugt", logs[#logs]:find("kursus genbrugt", 1, true) ~= nil, true)
C.afterStart(tractorA, "A", ctx)
check("G3c uændret register gemmes ikke igen", saves, 1)
-- G4: B skal samme mark -> kopi fra A
local assignB = {vehicle = tractorB, vehicleId = "B", role = "cultivator"}
opts, ctx = C.prepare({}, fieldObj, info, assignB, {})
check("G4 kopi fra A", opts.course, tractorA.course)
check("G4b log kopieret", logs[#logs]:find("kopieret fra 'A'", 1, true) ~= nil, true)
tractorB.course = fakeCourse(812)
C.afterStart(tractorB, "B", ctx)
check("G4c B registreret", ADFieldJobs.state.courseRecords.B ~= nil, true)
-- G5: spilleren har lavet et nyt kursus på A i CP's HUD (andet antal punkter) -> kopi fra B
tractorA.course = fakeCourse(400)
opts = C.prepare({}, fieldObj, info, assignA, {})
check("G5 A's kursus ændret -> kopi fra B", opts.course, tractorB.course)
-- G6: kursus kopieret fra slåmaskinen (opts.course) -> registret bruges ikke, maskinens post glemmes
local mowerCourse = fakeCourse(99)
opts, ctx = C.prepare({}, fieldObj, info, assignB, {course = mowerCourse})
check("G6 opts urørt", opts.course, mowerCourse)
check("G6b ingen kontekst", ctx, nil)
tractorB.course = fakeCourse(99)
C.afterStart(tractorB, "B", ctx)
check("G6c B glemt", ADFieldJobs.state.courseRecords.B, nil)
-- G7: vinmark: vin-offset indgår i nøglen
local vineReq = C.requestFor(tractorA, 22, {shapeSig = SIG, fruitName = "GRAPE"}, "fertilizer", {})
check("G7 vin i nøglen", vineReq.settings, C.settingsKey({vineOffset = -1, vineSkip = 1}))
-- G8: markens form ændret -> A's post glemmes, log
local before = saves
check("G8 checkField sletter", C.checkField(22, "n=9;a=1;c=0,0"), 1)
check("G8b register tomt for marken", ADFieldJobs.state.courseRecords.A, nil)
check("G8c gemt", saves, before + 1)
check("G9 samme form: intet", C.checkField(22, "n=9;a=1;c=0,0"), 0)
-- G10: fejl i spil-API'et -> falder tilbage til ny beregning (opts urørt), ingen crash
local brokenVehicle = {label = "X", getFieldWorkCourse = function() error("boom") end}
local plainOpts = {lane = 1}
local okPrep, gotOpts, gotCtx = pcall(C.prepare, {}, fieldObj, nil, {vehicle = brokenVehicle, vehicleId = "X", role = "x"}, plainOpts)
check("G10 ingen crash", okPrep, true)
check("G10b opts urørt", gotOpts, plainOpts)
check("G10c ingen kontekst", gotCtx, nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+88
View File
@@ -70,6 +70,35 @@ check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer") check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder") check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil) check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
-- SR2: tom Bredal-spreder (tager gødning OG kalk) kan begge dele; fyldt med kalk er den primært kalkspreder
local bredal = {FERTILIZER = true, LIME = true}
local p1, r1, f1 = ADCpBridge.sprayRolesFor(nil, bredal)
check("SR2 tom spreder primært gødning", p1, "fertilizer")
check("SR2 tom spreder kan kalke", r1 and r1.limer, true)
check("SR2 tom = intet fyld", f1, nil)
local p2, r2, f2 = ADCpBridge.sprayRolesFor("LIME", bredal)
check("SR2 med kalk = kalkspreder", p2, "limer")
check("SR2 med kalk kan stadig gøde (efter tømning)", r2 and r2.fertilizer, true)
check("SR2 fyldets rolle", f2, "limer")
check("SR2 ukrudtssprøjte", (ADCpBridge.sprayRolesFor(nil, {HERBICIDE = true, WATER = true})), "weeder")
check("SR2 vandvogn er ingen sprøjte", (ADCpBridge.sprayRolesFor(nil, {WATER = true})), nil)
-- SR3: kun en lille rest må hældes ud (100.000 l gødning blev hældt ud in-game 26/9)
check("SR3 tom", ADCpBridge.isDumpable(0, 4000), true)
check("SR3 lille rest 300 af 4000", ADCpBridge.isDumpable(300, 4000), true)
check("SR3 halv tank 2000 af 4000", ADCpBridge.isDumpable(2000, 4000), false)
check("SR3 100000 l aldrig", ADCpBridge.isDumpable(100000, 1000000), false)
check("SR3 over 2000 l aldrig", ADCpBridge.isDumpable(2500, 100000), false)
-- VN: vinmark — i gangen for alt undtagen høsteren; almindelige marker røres ikke
check("VN sprøjte på druer = venstre", ADCpBridge.vineOffsetFor("GRAPEB", "fertilizer"), -1)
check("VN harve på druer = venstre", ADCpBridge.vineOffsetFor("grape", "cultivator"), -1)
check("VN høster på druer = på vinen", ADCpBridge.vineOffsetFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineOffsetFor("WHEAT", "fertilizer"), nil)
check("VN ukendt afgrøde", ADCpBridge.vineOffsetFor(nil, "weeder"), nil)
check("VN sprøjte springer 1 række over (begge sider)", ADCpBridge.vineSkipFor("GRAPEB", "weeder"), 1)
check("VN gødning springer 1 over", ADCpBridge.vineSkipFor("GRAPEB", "fertilizer"), 1)
check("VN harve springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "cultivator"), 0)
check("VN høster springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineSkipFor("WHEAT", "weeder"), nil)
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen -- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}}) local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
@@ -86,5 +115,64 @@ check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false) check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true) check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
-- CR: kursus-genbrug — start på maskinens eget kursus uden at CP beregner et nyt (opts.reuse)
do
local generated, started = 0, 0
AIJobType = {FIELDWORK_CP = 1}
function getWorldTranslation() return 0, 0, 0 end
local job = {cpJobParameters = {}}
function job.applyCurrentState() end
function job.setValues() end
function job.validate() return true end
function job.registerFieldBoundaryDetectionCallback() end
function job.detectFieldBoundary() return true, false, "" end
function job.onClickGenerateFieldWorkCourse(_, callback) generated = generated + 1; callback(true) end
g_currentMission = {aiJobTypeManager = {createJob = function() return job end},
aiSystem = {startJob = function() started = started + 1 end}}
local course = {}
local vehicle = {rootNode = 1, getOwnerFarmId = function() return 1 end, getFieldWorkCourse = function() return course end}
local reason = "?"
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR1 genbrug: ingen beregning", generated, 0)
check("CR2 genbrug: CP startet", started, 1)
check("CR3 genbrug: reason", reason, "reused")
-- ingen kursus på maskinen -> beregnes alligevel
vehicle.getFieldWorkCourse = function() return nil end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR4 intet kursus -> beregnes", generated, 1)
check("CR5 reason ny", reason, "generated")
-- uden reuse: beregnes som før
vehicle.getFieldWorkCourse = function() return course end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {})
check("CR6 uden reuse -> beregnes", generated, 2)
AIJobType, g_currentMission = nil, nil
end
-- forager: 2 runder og start på forageren (så der ikke køres ind i træer o.l. i hjørnerne)
local recorded = {}
local function setting(name) return {setValue = function(_, v) recorded[name] = v end} end
local headlandVehicle = {getCourseGeneratorSettings = function() return {numberOfHeadlands = setting("headlands"), startOnHeadland = setting("startOnHeadland")} end}
check("forager: sat", ADCpBridge.setHeadlands(headlandVehicle, 2), true)
check("forager: 2 runder", recorded.headlands, 2)
check("forager: start på forageren", recorded.startOnHeadland, true)
ADCpBridge.setHeadlands(headlandVehicle, 0)
check("forager: ingen -> ikke start på forageren", recorded.startOnHeadland, false)
check("forager: køretøj uden CP", ADCpBridge.setHeadlands({}, 2), false)
check("forager: standard er 2", ADCpBridge.HEADLANDS, 2)
-- genbrugt kursus uden forager beregnes på ny
check("forager i kursus: 2 runder", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 2 end}), true)
check("forager i kursus: ingen", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 0 end}), false)
check("forager i kursus: ukendt -> antag ja", ADCpBridge.courseHasHeadlands({}), true)
check("forager i kursus: intet kursus", ADCpBridge.courseHasHeadlands(nil), false)
-- isWorking: AutoDrive kører også som AI i FS25 — det er ikke Courseplay (in-game 29/9: Milk Store holdt ved
-- Cow Wait med AD aktiv og "ventede på sig selv" som 'Courseplay kører')
local function fakeVehicle(cp, ai, ad)
return {getIsCpFieldWorkActive = function() return cp end, getIsAIActive = function() return ai end,
ad = {stateModule = {isActive = function() return ad end}}}
end
check("CP arbejder: Courseplay markarbejde", ADCpBridge.isWorking(fakeVehicle(true, true, false)), true)
check("CP arbejder: AI uden AD (CP-kørsel)", ADCpBridge.isWorking(fakeVehicle(false, true, false)), true)
check("CP arbejder: AutoDrive kører -> ikke CP", ADCpBridge.isWorking(fakeVehicle(false, true, true)), false)
check("CP arbejder: intet", ADCpBridge.isWorking(fakeVehicle(false, false, false)), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+26
View File
@@ -0,0 +1,26 @@
-- Kør: luajit tests/test_adCpResume.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adCpResume.lua")
local R = ADCpResume
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
-- hvem skrives ved gem: CP kører, AD styrer ikke (AD genstarter selv sine)
local ids = R.activeIds({{id = "a", cpActive = true}, {id = "b", cpActive = true, adActive = true}, {id = "c"}, {id = "d", cpActive = true}})
check("gem: CP-aktive uden AD", table.concat(ids, " "), "a d")
check("gem: tom liste", #R.activeIds({}), 0)
-- records frem og tilbage
local set = R.fromRecords({"a", "", "d", "a"})
check("indlæs: a", set.a, true)
check("indlæs: tomt id springes over", set[""], nil)
-- hvem startes ved indlæsning
local pending = {a = true, b = true, c = true, d = true}
local vehicles = {{id = "a"}, {id = "b", aiActive = true}, {id = "c", controlled = true}, {id = "d"}, {id = "e"}}
local start = R.toStart(pending, vehicles)
check("start: kun gemte, ikke allerede kørende/spiller i", table.concat(start, " "), "a d")
check("start: ingen gemt liste -> nil (gammel regel bruges)", R.toStart(nil, vehicles), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+47
View File
@@ -94,5 +94,52 @@ check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollect
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = 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") check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
-- FL-spreder: tom spreder med rollerne {gødning, kalk} vælges til kalkning; spreder med forkert vare
-- på (kalk, skal gøde) kun hvis ingen anden kan — og den nærmeste med rigtig/ingen vare først
local spreaders = {
{id = "kalk", role = "limer", roles = {fertilizer = true, limer = true}, filledRole = "limer", enabled = true, x = 1, z = 0, width = 12},
{id = "tom", role = "fertilizer", roles = {fertilizer = true, limer = true}, enabled = true, x = 50, z = 0, width = 12},
}
check("FLS tom spreder tager kalkning", F.pick({spreaders[2]}, "limer", 0, 0) and F.pick({spreaders[2]}, "limer", 0, 0).id, "tom")
check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "fertilizer", 0, 0).id, "tom")
check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk")
check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk")
-- FLS2: fuld af en anden vare (ikke dumpable) bruges slet ikke til opgaven
local fullOther = {id = "fuld", role = "fertilizer", roles = {fertilizer = true, weeder = true}, filledRole = "fertilizer", dumpable = false, enabled = true, x = 0, z = 0, width = 30}
check("FLS2 fuld gødningssprøjte bruges ikke til ukrudt", F.pick({fullOther}, "weeder", 0, 0), nil)
check("FLS2 men gerne til gødning", F.pick({fullOther}, "fertilizer", 0, 0).id, "fuld")
-- FLD: faste maskiner pr. mark
local sprayA = {id = "sprøjte", role = "fertilizer"}
local sprayB = {id = "spreder", role = "fertilizer"}
local harve = {id = "harve", role = "cultivator"}
local all = {sprayA, sprayB, harve}
local byField = {[22] = {"sprøjte"}}
local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end
check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte")
check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve")
check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve")
check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve")
local moved = F.dedicate(byField, 109, "sprøjte", true)
check("FLD flyt: væk fra F22", #moved[22], 0)
check("FLD flyt: fast på F109", moved[109][1], "sprøjte")
check("FLD original urørt", byField[22][1], "sprøjte")
check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0)
-- FLV: menuen skjuler maskiner der er faste på en anden mark end den valgte
local vrigs = {{id = "vogn"}, {id = "harve"}, {id = "plov"}}
local vis, hid, flds = F.visibleForField(vrigs, 18, {[22] = {"vogn", "harve"}})
check("FLV F18 ser kun de frie", #vis .. "/" .. hid .. "/" .. tostring(flds[1]), "1/2/22")
check("FLV F22 ser alle", #(F.visibleForField(vrigs, 22, {[22] = {"vogn", "harve"}})), 3)
check("FLV ingen valgt = alle", #(F.visibleForField(vrigs, nil, {[22] = {"vogn"}})), 3)
-- FL-plov: FS25-plove har også spec_cultivator — de er plove, men må også tage kultivering
check("FLP plov med kultivator-spec er plov", F.roleFromSpecs({spec_plow = true, spec_cultivator = true}), "plow")
check("FLP plov kan kultivere", F.rolesFor("plow").cultivator, true)
check("FLP plov kan pløje", F.rolesFor("plow").plow, true)
check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil)
local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3}
check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p")
check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+255
View File
@@ -0,0 +1,255 @@
-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFieldGuide.lua")
local FG, G = ADFieldGuide, ADGuide
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 SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
local RIGS = {
{id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
{id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
{id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
{id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
}
local function ctx(info, extra)
local context = {
info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
local BARE = {id = 4, kind = "bare", markerId = 8}
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function field(id, plan)
local f = ADFieldModel.newField(id)
return ADFieldModel.with(f, {plan = plan or {}})
end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
-- ---- synlige trin pr. marktype ----
check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
local picker = FG.openPicker(ctx(GRASS))
check("FG1 vælger: kun mark", steps(picker), "field")
check("FG1 vælger: trin", picker.step, "field")
check("FG1 vælger: position uden total", (G.position(picker)), 1)
-- ---- standarder og udfyldning ----
local g = FG.open(field(2), ctx(WHEAT))
check("FG2 starter på afgrøde", g.step, "crop")
check("FG2 klargøring auto", g.draft.prepAuto, true)
check("FG2 ingen faste", g.draft.useDedicated, false)
check("FG2 lager auto", g.draft.storeMode, "auto")
check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
-- ---- choose er immutabel ----
local chosen = FG.choose(g, "targetFruit", "BARLEY")
check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
-- ---- rækker ----
local cropRows = FG.rows(g)
local target = findRow(cropRows, "targetFruit")
check("FG4 afgrøde: valg", target.type, "choice")
check("FG4 afgrøde: behold først", target.options[1].value, "")
check("FG4 afgrøde: såbare", #target.options, 5)
check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
local prepRows = FG.rows(G.jump(g, "prep", "close"))
check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
-- pleje: Automatisk/Manuelt som klargøring (brugerønske 29/9); knapperne vises under Manuelt
local careAutoRows = FG.rows(G.jump(g, "care", "close"))
check("FG7 pleje: valg auto/manuel", findRow(careAutoRows, "careAuto").type, "choice")
check("FG7 pleje auto: ingen knapper", findRow(careAutoRows, "fertilize"), nil)
check("FG7 standard: pleje auto", g.draft.careAuto, true)
check("FG7 uden gødning -> manuel", FG.open(field(2, {fertilize = false}), ctx(WHEAT)).draft.careAuto, false)
local autoCare = FG.result(FG.choose(FG.open(field(2, {fertilize = false, weed = false}), ctx(WHEAT)), "careAuto", true), true).plan
check("FG7 auto slår gødning til", autoCare.fertilize, true)
check("FG7 auto slår ukrudt til", autoCare.weed, true)
local careGrass = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "careAuto", false), "care", "close"))
check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
local careWheat = FG.rows(G.jump(FG.choose(g, "careAuto", false), "care", "close"))
check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
local machinesOff = FG.rows(G.jump(g, "machines", "close"))
check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
local hiddenText = nil
for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
local firstCheck = nil
for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FG8 egne først", firstCheck.item, "v2")
check("FG8 egne sat", firstCheck.value, true)
local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
local pickRows = FG.rows(picker)
check("FG10 vælger: links", countType(pickRows, "link"), 2)
check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
local picked = FG.pickField(picker, field(2), ctx(WHEAT))
check("FG10 valgt mark", picked.draft.fieldId, 2)
check("FG10 videre til afgrøde", picked.step, "crop")
check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
-- ---- resultat ----
local r = FG.result(g, true)
check("FG11 auto ved start", r.plan.mode, "auto")
check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
check("FG11 mål uændret", r.targetChanged, false)
check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
check("FG12 auto: jord", autoPrep.soil, "auto")
check("FG12 auto: mulch", autoPrep.mulch, false)
check("FG12 auto: kalk", autoPrep.lime, true)
check("FG12 auto: tromle", autoPrep.roll, false)
local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
-- ---- faste maskiner på tværs af marker ----
local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partKeys(line)
local keys = {}
for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
return table.concat(keys, ",")
end
local sum = FG.summary(g)
check("FG16 linjer for synlige trin", #sum, 6)
check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
check("FG16 pleje auto", partKeys(lineFor(sum, "care")), "spu_guide_sum_auto")
check("FG16 pleje manuel", partKeys(lineFor(FG.summary(FG.choose(g, "careAuto", false)), "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
local sumTarget = FG.summary(chosen)
check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
local sumRigs = FG.summary(withRigs)
check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
local sumNoCare = FG.summary(FG.choose(FG.choose(FG.choose(g, "careAuto", false), "fertilize", false), "weed", false))
check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
local sumSow = FG.summary(FG.choose(g, "sow", true))
check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
-- ---- oversigts-trinnet: links pr. linje ----
local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+122 -2
View File
@@ -52,6 +52,9 @@ local withHarvester = {{phase = "working", stage = "harvesting", helper = false}
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0) 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 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") check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn
local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}}
check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none")
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører -- 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} 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") check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
@@ -89,14 +92,131 @@ local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain") 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") 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 -- J12: bar mark med mål-afgrøde klargøres straks — kun selve såningen venter på såtiden
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}} local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}}) local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
local canSow = {plantable = function(name) return name == "WHEAT" end} 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 + 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 + ikke såtid -> start alligevel (klargøring)", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "start")
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait") 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") 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")
-- J13: afbrudt slåning efter genindlæsning (in-game 27/9 F90): genvækst i mellemhøjde -> vent; hjørne efter en tur -> slå straks
local regrowing = {id = 90, markerId = 9, mowable = true, readyRatio = 0.5, immatureRatio = 0.5}
local dRegrow = J.decide(mowing, regrowing, {}, 0)
check("J13 genvækst -> vent", dRegrow.action, "wait"); check("J13 grund", dRegrow.reason, "regrowing")
check("J13 genvækst men tur færdig -> slå hjørnet", J.decide(mowing, regrowing, {{phase = "finished"}}, 0).action, "dispatch")
check("J13 halvt slået uden genvækst -> slå resten", J.decide(mowing, {id = 90, markerId = 9, mowable = true, readyRatio = 0.6, immatureRatio = 0}, {}, 0).action, "dispatch")
-- J14: kultivering på en mark med uhøstede kartofler (in-game 27/9 F16) -> tilbage til klar; rest efter høst -> ikke
local cultivatingField = M.with(auto, {stage = "cultivating", kind = "harvest"})
check("J14 uhøstet -> spol tilbage", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 1}, {}, 0).action, "rewind")
check("J14 høstrest 30 % -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 0.3}, {}, 0).action ~= "rewind", true)
check("J14 stub -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "bare", phase = "cut", readyRatio = 0}, {}, 0).action ~= "rewind", true)
-- resumeField: efter genindlæsning stopper spillet alle arbejdere; en CP-maskine der står PÅ en mark hvis
-- aktuelle trin passer til maskinens rolle, meldes ind på den mark igen (in-game 27/9)
do
local square = {{x = 0, z = 0}, {x = 100, z = 0}, {x = 100, z = 100}, {x = 0, z = 100}}
local fields = {
{id = 90, stage = "collecting", verts = square},
{id = 111, stage = "harvesting", verts = {{x = 200, z = 0}, {x = 300, z = 0}, {x = 300, z = 100}, {x = 200, z = 100}}},
}
check("RF høster på F111", ADFieldJobs.resumeField(250, 50, "harvester", fields), 111)
check("RF læssevogn på F90", ADFieldJobs.resumeField(50, 50, "collector", fields), 90)
check("RF forkert rolle til trinnet -> nil", ADFieldJobs.resumeField(250, 50, "collector", fields), nil)
check("RF uden for alle marker -> nil", ADFieldJobs.resumeField(150, 50, "harvester", fields), nil)
check("RF mark uden omrids -> nil", ADFieldJobs.resumeField(50, 50, "collector", {{id = 1, stage = "collecting", verts = {}}}), nil)
check("RF på kanten tæller som indenfor", ADFieldJobs.resumeField(0.5, 50, "collector", fields), 90)
end
-- startAt: hvor Courseplay skal starte i kurset (CP's standard er "first" = forfra, også på en halvt høstet mark;
-- in-game 27/9: mejetærskeren startede F111 forfra)
check("SA eget kursus med husket waypoint -> last", ADFieldJobs.startAtFor({reuse = true, remembered = true, stage = "harvesting", standingRatio = 0.4}), "last")
check("SA halvt høstet -> nearest", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.4}), "nearest")
check("SA halvt slået -> nearest", ADFieldJobs.startAtFor({stage = "mowing", standingRatio = 0.5}), "nearest")
check("SA urørt mark -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 1}), "first")
check("SA næsten urørt (97 %) -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.97}), "first")
check("SA ukendt fremdrift -> first", ADFieldJobs.startAtFor({stage = "harvesting"}), "first")
check("SA klargøring (ingen afgrøde at måle) -> first", ADFieldJobs.startAtFor({stage = "cultivating", standingRatio = 0.3}), "first")
check("SA husket waypoint uden eget kursus -> ikke last", ADFieldJobs.startAtFor({reuse = false, remembered = true, stage = "cultivating"}), "first")
-- shouldResume: kun maskiner der stadig er midt i jobbet genstartes (in-game 27/9: to ROPA'er med et husket
-- waypoint fra et gammelt job kørte ud ad vejene mod det gamle kursus)
check("SR står på en mark i gang med sit trin -> ja", ADFieldJobs.shouldResume({fieldId = 111, distance = 400}), true)
check("SR lige ved sit huskede waypoint -> ja", ADFieldJobs.shouldResume({distance = 12}), true)
check("SR langt fra waypointet og ingen mark -> nej", ADFieldJobs.shouldResume({distance = 850}), false)
check("SR ukendt afstand og ingen mark -> nej", ADFieldJobs.shouldResume({}), false)
-- mayDispatch: markarbejdet sender højst én maskine 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)
check("MD første udsendelse", ADFieldJobs.mayDispatch(5000, nil), true)
check("MD for tæt på forrige", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS - 1, 5000), false)
check("MD efter mellemrummet", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS, 5000), true)
-- halvt høstet mark (in-game 27/9 F111): høsten er ikke færdig før næsten intet står; en mark i klargøring med
-- stående afgrøde spoles tilbage
do
local halfInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.45, standingRatio = 0.45, fruitIndex = 1}
check("HH halvt høstet uden høster -> send høster (ikke videre)", J.decide(harvesting, halfInfo, {}, 0).action, "dispatch")
local doneInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.02, standingRatio = 0.02, fruitIndex = 1}
check("HH næsten intet står -> videre", J.decide(harvesting, doneInfo, {}, 0).action, "advance")
local cultivating = M.with(harvesting, {stage = "cultivating"})
check("HH klargøring med halv mark stående -> spol tilbage", J.decide(cultivating, halfInfo, {}, 0).action, "rewind")
end
-- startTimedOut: CP må beregne kursus i op til 5 min (in-game 27/9 F90: 140 ha tog over 60 s, moden opgav midt i)
check("ST start uden beregning > 60 s -> opgiv", ADFieldJobs.startTimedOut(61000, false), true)
check("ST beregner stadig efter 90 s -> vent", ADFieldJobs.startTimedOut(90000, true), false)
check("ST beregner over 5 min -> opgiv", ADFieldJobs.startTimedOut(301000, true), true)
check("ST under 60 s -> vent", ADFieldJobs.startTimedOut(30000, false), false)
-- shouldEmptyHarvester: høsten er færdig, men høsteren har korn i tanken og står ved marken -> tøm først
-- (in-game 27/9 F15: marken gik til halm med fuld tank, ingen tømmevogn kom)
check("EH korn i tanken ved marken -> tøm", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true}), true)
check("EH tom tank -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 0, near = true}), false)
check("EH langt væk -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = false}), false)
check("EH optaget -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, busy = true}), false)
check("EH spiller i -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, controlled = true}), false)
check("EH allerede forsøgt -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, tried = true}), false)
-- nearestPoint: CP bruger markens punkt som kursets startsted — tag det nærmest traktoren (in-game 27/9 F17: første
-- punkt lå 280 m væk i den anden ende, og CP kørte gennem skoven)
do
local pts = {{x = 1600, z = -1839}, {x = 1350, z = -1740}, {x = 1500, z = -1800}}
local p = ADFieldJobs.nearestPoint(pts, 1340, -1733)
check("NP nærmest traktoren", p.x .. "," .. p.z, "1350,-1740")
check("NP uden punkter -> nil", ADFieldJobs.nearestPoint({}, 0, 0), nil)
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
end
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
-- JP: klargøring der allerede er gjort (fx efter genindlæsning) går videre uden at sende maskinen ud igen
-- (in-game 28/9: F17 blev kultiveret 3 gange — opgaver gemmes ikke, så "færdig" blev aldrig set)
local cultField = M.with(M.newField(17), {stage = "cultivating", kind = "bare", plan = {mode = "auto"}})
local cultInfo = {id = 17, markerId = 5, kind = "bare", readyRatio = 0, needs = {}}
cultInfo.prepared = 0.98
check("JP kultiveret -> videre", J.decide(cultField, cultInfo, {}, 0).action, "advance")
cultInfo.prepared = 0.4
check("JP halvt kultiveret -> send maskine", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = nil
check("JP ukendt jord -> som før (send)", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = 0.98
check("JP kultivator i gang -> vent på den", J.decide(cultField, cultInfo, {{phase = "working", stage = "cultivating"}}, 0).action, "none")
local plowField = M.with(cultField, {stage = "plowing"})
check("JP kultiveret er ikke pløjet -> send plov", J.decide(plowField, cultInfo, {}, 0).action, "dispatch")
cultInfo.plowed = 0.97
check("JP pløjet -> videre", J.decide(plowField, cultInfo, {}, 0).action, "advance")
local sowField = M.with(cultField, {stage = "sowing"})
check("JP såning berøres ikke af klargjort jord", J.decide(sowField, cultInfo, {}, 0).action ~= "advance", true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+34 -3
View File
@@ -67,6 +67,14 @@ check("M7 maks slå", M.maxRigs("mowing"), 1)
local okWide, whyWide = M.checkWidth("mower", 60, nil) local okWide, whyWide = M.checkWidth("mower", 60, nil)
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide") 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) check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
-- M7b: bredde under 1,5 m er ikke læst rigtigt (spreder gav 0,5 m -> CP-kursus hver halve meter på 72 ha
-- -> spillet frøs, in-game 26/9) — sendes aldrig ud
local okTiny, whyTiny = M.checkWidth("fertilizer", 0.5, nil)
check("M7b 0,5 m afvises", okTiny, false)
check("M7b grund", whyTiny, "widthUnknown")
check("M7b 3 m strigle ok", (M.checkWidth("weeder", 3, nil)), true)
check("M7b aflæsningsvogn uden bredde ok", (M.checkWidth("unloader", 0, nil)), true)
check("M7b ballesamler uden bredde ok", (M.checkWidth("baleCollector", 0, nil)), true)
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8) 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 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 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
@@ -82,7 +90,7 @@ check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
-- M8: gem/indlæs bevarer trin, plan og slåbredde -- 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" 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"}} saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}}
local back = M.fromRecord(M.toRecord(saved)) local back = M.fromRecord(M.toRecord(saved))
check("M8 id", back.id, 109) check("M8 id", back.id, 109)
check("M8 stage", back.stage, saved.stage) check("M8 stage", back.stage, saved.stage)
@@ -90,6 +98,7 @@ check("M8 mode", back.plan.mode, "auto")
check("M8 product", back.plan.product, "hay") check("M8 product", back.plan.product, "hay")
check("M8 rake", back.plan.rake, true) check("M8 rake", back.plan.rake, true)
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7") check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2")
check("M8 slåbredde", back.mowWidth, 40.8) check("M8 slåbredde", back.mowWidth, 40.8)
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM") check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil) check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
@@ -254,9 +263,9 @@ check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
check("M19 start: høst først", started.stage, "harvesting") 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 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) 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) -- M20: skal en bar mark med mål starte? (altid — kun såningen venter på såtiden)
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true) 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 bar + ikke såtid -> klargør alligevel (kun såningen venter)", M.shouldStartBare(target, "bare"), true)
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), 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 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false) check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
@@ -289,5 +298,27 @@ check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting"
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale") 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") check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
local calm = M.newField(95)
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
check("M24 manuel venter = passiv", M.isPassive(calm), true)
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
check("M24 i gang = ikke passiv", M.isPassive(started), false)
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper
local stoppedBare = M.stop(M.with(target, {stage = "cultivating"}))
check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false)
check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true)
check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true)
check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil)
-- gemt produkt (silage/hø) gælder kun slåafgrøder: en vinmark der engang fejlagtigt blev "slåbar" (F22, 29/9)
check("produkt: gemt silage på høstmark -> høst", ADFieldModel.product({product = "silage"}, "harvest"), "harvest")
check("produkt: gemt hø på slåmark -> hø", ADFieldModel.product({product = "hay"}, "mowable"), "hay")
check("produkt: auto på slåmark -> silage", ADFieldModel.product({product = "auto"}, "mowable"), "silage")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+46
View File
@@ -0,0 +1,46 @@
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
local C = ADFieldSiloConfig
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 cfg = C.new()
check("standard fra", cfg.enabled, false)
check("standard x=2", cfg.maxVehicles, 2)
check("standard tærskel 10000", cfg.minLiters, 10000)
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
check("with muterer ikke", cfg.maxVehicles, 2)
local on = C.toggleStore(cfg, "silo1")
check("toggle til", C.hasStore(on, "silo1"), true)
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
local back = C.fromRecord(C.toRecord(full))
check("record: enabled", back.enabled, true)
check("record: lagre", table.concat(back.storeIds, " "), "a b")
check("record: x", back.maxVehicles, 4)
check("record: tærskel", back.minLiters, 25000)
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
-- kør til produktion + foderreserve
check("standard: ikke til produktion", cfg.toProduction, false)
check("standard foderreserve 7 døgn", cfg.reserveDays, 7)
check("reserve snappes (10 -> 7)", C.with(cfg, {reserveDays = 10}).reserveDays, 7)
check("reserve 40 -> 30", C.with(cfg, {reserveDays = 40}).reserveDays, 30)
local prod = C.with(C.toggleStore(cfg, "a"), {toProduction = true, reserveDays = 14})
check("with bevarer lagre", C.hasStore(prod, "a"), true)
check("with muterer ikke (produktion)", cfg.toProduction, false)
local prodBack = C.fromRecord(C.toRecord(prod))
check("record: til produktion", prodBack.toProduction, true)
check("record: foderreserve", prodBack.reserveDays, 14)
check("gammel record uden felter -> standard", C.fromRecord({enabled = 1}).reserveDays, 7)
check("toggleStore bevarer produktion", C.toggleStore(prod, "b").toProduction, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+159
View File
@@ -0,0 +1,159 @@
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
local P = ADFieldSiloPlanner
local WHEAT, CANOLA = 1, 2
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 silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
local function base(over)
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
for k, v in pairs(over or {}) do input[k] = v end
return input
end
-- kvalificering
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
check("under tærskel -> ingen status", #r.status, 0)
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
check("uden markør -> noMarker", r.status[1].state, "noMarker")
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
-- afsendelse, fuldeste først, x-loft
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
check("x=2 -> to afsendt", #r.dispatch, 2)
check("fuldeste silo først", r.dispatch[1].siloId, "B")
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
check("anden tur = resten", r.dispatch[2].liters, 30000)
-- aktive lån tæller mod x og mod siloens rest
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
check("status viser på vej", r.status[1].onWay, 50000)
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
check("frie pladser 0", r.freeSlots, 0)
-- flere varer i én silo = hver sin opgave
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
check("to varer -> to afsendelser", #r.dispatch, 2)
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
-- vogne der ikke kan
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
-- lagervalg
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
-- input muteres ikke
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
P.plan(input)
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
-- kør til produktion: foder først, overskud til fabrik
local function factory(id, x, free) return {id = id, kind = "production", markerId = id .. "m", x = x, z = 0, free = free} end
local farmAndFactory = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 20000}}))
check("P1 reserve ikke dækket -> gårdlager selv om fabrik er nærmere", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 150000}}))
check("P2 reserve dækket -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("P3 ikke foder (ingen reserve) -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 100000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 70000}}))
check("P4 første læs dækker resten af reserven -> gård", r.dispatch[1].storeId, "farm")
check("P4 andet læs er overskud -> fabrik", r.dispatch[2].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 60000}, loans = {{siloId = "B", fillType = WHEAT, liters = 50000, storeId = "farm"}}}))
check("P5 læs på vej til gården tæller med i reserven -> fabrik", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, farmStock = {[WHEAT] = 999999}}))
check("P6 ingen fabrik tager varen -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, feedReserve = {[WHEAT] = 100000}}))
check("P7 reserve mangler men ingen gård tager varen -> fabrik", r.dispatch[1].storeId, "mill")
check("P7 status ok (ikke noFarmStore)", r.status[1].state, "ok")
check("P7 status viser destination", r.status[1].toProduction, true)
-- fabrik kun når hele læsset kan være der (in-game 28/9: vogn med 252.000 l ventede timevis ved en fuld fabrik)
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P8 fabrik uden plads til hele læsset -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P9 kun fabrik uden plads til hele læsset -> ingen afsendelse", #r.dispatch, 0)
-- høsten først: tømmevogne holdes tilbage til høstere der arbejder
local vs = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000, {[CANOLA] = true}), vehicle("v3", 400, 50000)}
local left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 1}})
check("R1 én hvede-vogn holdt tilbage", #left, 2)
check("R1 raps-vognen er fri", left[1].id == "v2" or left[2].id == "v2", true)
left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 5}})
check("R2 alle hvede-vogne holdt tilbage", #left, 1)
check("R3 intet behov -> alle fri", #P.withoutHarvestReserve(vs, {}), 3)
check("R4 input muteres ikke", #vs, 3)
-- Tom silo: lånt vogn med last står ved en tom marksilo -> resten køres videre (AD venter ellers til fuld)
-- (in-game 28/9: Tømmevogn 2 og 4 stod ved tom F17 Unload med kartofler)
local restState = {atSilo = true, stillMs = 45000, siloStock = 0, cargo = 12000}
check("RS1 tom silo med last -> kør videre", P.restDue(restState), true)
check("RS2 siloen har stadig varer -> vent", P.restDue({atSilo = true, stillMs = 45000, siloStock = 30000, cargo = 12000}), false)
check("RS3 ikke ved siloen -> nej", P.restDue({atSilo = false, stillMs = 45000, siloStock = 0, cargo = 12000}), false)
check("RS4 lige ankommet -> vent", P.restDue({atSilo = true, stillMs = 5000, siloStock = 0, cargo = 12000}), false)
check("RS5 tom vogn -> nej", P.restDue({atSilo = true, stillMs = 45000, siloStock = 0, cargo = 0}), false)
local restStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 5000}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("RS6 fabrik uden plads til hele resten -> gårdsilo", P.restStore(restStores, {x = 0, z = 0}, 10, 12000).id, "gaard")
check("RS7 fabrik med plads -> fabrik", P.restStore(restStores, {x = 0, z = 0}, 10, 4000).id, "fab")
check("RS8 intet tager varen", P.restStore(restStores, {x = 0, z = 0}, 99, 4000), nil)
-- Fuld fabrik: lånt vogn med last holder ved et lager uden plads -> resten til et andet lager (gård)
check("SF1 fuld fabrik -> videre", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 0}), true)
check("SF2 fabrikken tager stadig imod -> vent", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 8000}), false)
check("SF3 lige ankommet -> vent", P.storeFullDue({atStore = true, stillMs = 20000, cargo = 12000, storeFree = 0}), false)
check("SF4 ikke ved lageret -> nej", P.storeFullDue({atStore = false, stillMs = 70000, cargo = 12000, storeFree = 0}), false)
local fullStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 0}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("SF5 andet lager end det fulde", P.restStore(fullStores, {x = 0, z = 0}, 10, 12000, "fab").id, "gaard")
check("SF6 kun det fulde tager varen -> intet", P.restStore({fullStores[1]}, {x = 0, z = 0}, 10, 12000, "fab"), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+159
View File
@@ -0,0 +1,159 @@
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
local S = ADFieldSilos
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 loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
do
local WHEAT = 1
Logging = {info = function() end, warning = function() end}
g_currentMission = {getFarmId = function() return 1 end}
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
function getWorldTranslation(node) return node.x, 0, node.z end
ADRuns = {vehicleLabel = function(v) return v.name end}
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
ADBuildings = {list = function() return {fieldSilo, farm} end,
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
local function makeVehicle(name, x)
local module = {active = false}
function module:setMode() end
function module:setFirstMarkerByWayPointId(id) self.first = id end
function module:setSecondMarkerByWayPointId(id) self.second = id end
function module:setFillType() end
function module:setLoopsDone() end
function module:raiseDirtyFlag() end
function module:isActive() return self.active end
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
end
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
getFillUnitCapacity = function() return 40000 end,
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
ADFieldWork = {
getRigs = function()
local rigs = {}
-- som spillets vehicleSystem: slettede køretøjer er ikke med
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
return rigs
end,
enableParkAtJobFinished = function() end,
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
}
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
local plan = S.planNow(adEnv)
check("RØG én afsendelse (x=1)", #started, 1)
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
check("RØG vognen er lånt", S.isLoaned(near), true)
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
check("RØG frakobling stopper AD", near.stopped, true)
check("RØG frakobling frigiver", S.isLoaned(near), false)
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
started = {}
noPark[near] = true
near.stopped = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
noPark[near] = nil
-- I3: ingen rute til siloen -> ingen afsendelse
started = {}
noRoute["fs-load"] = true
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I3 ingen rute -> ingen afsendelse", #started, 0)
noRoute["fs-load"] = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
-- I6: silo der er kilde/mål i et flow er ikke marksilo
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
local silos = S.collectBuildings(adEnv)
check("I6 silo i et flow tømmes ikke", #silos, 0)
ADRunsController.store.runs = {}
-- in-game 27/9: marksilo-flowet flyttede sukkerroer fra en Farma-silo til nabo-Farma-siloen (samme lagerklynge)
local neighbour = {id = "nb", name = "Nabo-Farma", kind = "silo", placeable = {rootNode = {x = 40, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADFields.fieldSilos = function() return {fieldSilo, farm, neighbour} end
local withNeighbour = S.collectBuildings(adEnv)
local names = {}
for _, silo in ipairs(withNeighbour) do table.insert(names, silo.id) end
check("gårdens nabosilo (40 m fra gårdlager) er ikke marksilo", table.concat(names, ","), "fs")
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- in-game 27/9: gårdens Farma-klynge er 300 m lang — siloer over 100 m fra et valgt gårdlager blev marksiloer.
-- En silo i samme klynge (≤ 60 m til en nabo) som et gårdlager er gårdens.
local c1 = {id = "c1", name = "Klynge 1", kind = "silo", placeable = {rootNode = {x = 50, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local c2 = {id = "c2", name = "Klynge 2", kind = "silo", placeable = {rootNode = {x = 105, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADBuildings.list = function() return {fieldSilo, farm, c1, c2} end
ADFields.fieldSilos = function() return {fieldSilo, farm, c1, c2} end
local clustered = {}
for _, silo in ipairs(S.collectBuildings(adEnv)) do table.insert(clustered, silo.id) end
check("gårdens klynge (105 m via nabo) er ikke marksilo", table.concat(clustered, ","), "fs")
ADBuildings.list = function() return {fieldSilo, farm} end
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- M5: rest under 100 l tæller ikke som last
residue = 50
local vehicles = S.collectVehicles(adEnv)
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
residue = 0
-- M4: slettet lånt vogn fjernes uden at vælte tick
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
started = {}
S.planNow(adEnv)
local loaned = started[1] ~= nil and started[1].v or nil
if loaned ~= nil then loaned.isDeleted = true end
local okTick = pcall(S.tick, adEnv, 2000)
check("M4 tick tåler slettet vogn", okTick, true)
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+68
View File
@@ -38,5 +38,73 @@ for key, value in pairs(record) do
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value)) check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
end end
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
dofile("FS25_ADSmartPickup/adCourseCache.lua")
local course = ADCourseCache.newRecord({vehicle = "v7", field = 22, width = 30.0, sig = "n=4;a=960000;c=800,300",
settings = ADCourseCache.settingsKey({multiTools = 2, lane = -1}), waypoints = 812})
for key in pairs(course) do
check("ST3 kursus-felt med i lagringen: " .. key, ADFieldStorage.COURSE_TYPES[key] ~= nil, true)
end
local cxml = fakeXml()
ADFieldStorage.writeCourseRecord(cxml, "c", course)
local cback = ADFieldStorage.readCourseRecord(cxml, "c")
for key, value in pairs(course) do
check("ST4 kursus-rundtur: " .. key, tostring(cback[key]), tostring(value))
end
check("ST4b matcher stadig efter rundtur", ADCourseCache.matches(cback, {field = 22, width = 30, sig = course.sig, settings = course.settings}), true)
-- hel fil: save -> load med falsk XMLFile (iterate over indekserede nøgler)
local function fileXml(store)
local xml = fakeXml()
for _, kind in ipairs({"Int", "String", "Float"}) do
xml["set" .. kind] = function(_, key, value) store[key] = value end
xml["get" .. kind] = function(_, key) return store[key] end
end
function xml.iterate(_, base, fn)
local index = 0
while true do
local prefix = string.format("%s(%d)", base, index)
local found = false
for key in pairs(store) do
if key:sub(1, #prefix + 1) == prefix .. "#" or key:sub(1, #prefix + 1) == prefix .. "." then found = true break end
end
if not found then return end
fn(index + 1, prefix)
index = index + 1
end
end
function xml.save() end
function xml.delete() end
return xml
end
local disk = {}
ADRunsStorage = {FILE_NAME = "runs.xml", getPath = function() return "/save/runs.xml" end}
XMLFile = {create = function() return fileXml(disk) end, load = function() return fileXml(disk) end}
function fileExists() return true end
check("ST5 gem", ADFieldStorage.save({fields = {[77] = field}, fleet = {}, fleetMode = "all",
courseRecords = ADCourseCache.withRecord({}, course)}), true)
local loaded = ADFieldStorage.load()
check("ST6 kursus indlæst", loaded.courseRecords.v7 ~= nil and loaded.courseRecords.v7.waypoints, 812)
check("ST6b mark indlæst", loaded.fields[77] ~= nil, true)
-- ST7: gammel fil uden <courses> -> tomt register (bagudkompatibelt)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
local old = ADFieldStorage.load()
check("ST7 gammel fil: tomt register", next(old.courseRecords), nil)
-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}})
check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true)
check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil)
-- ST9: pause ("Stop alt og parkér") gemmes, så intet starter af sig selv efter indlæsning
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", paused = true})
check("ST9 pause indlæst", ADFieldStorage.load().paused, true)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
check("ST9 ingen pause", ADFieldStorage.load().paused, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+24
View File
@@ -38,5 +38,29 @@ local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs =
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed") 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) check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
-- cropText: høstet = stub (afgrøde), bar = bar jord, ellers afgrøden
local texts = {spu_field_stubble = "stub (%s)", spu_field_bare = "bar jord"}
local function T(key) return texts[key] end
check("FVcrop stub", ADFieldView.cropText({kind = "bare", stubbleOf = "Hvede", cropTitle = "Hvede"}, T), "stub (Hvede)")
check("FVcrop bar", ADFieldView.cropText({kind = "bare"}, T), "bar jord")
check("FVcrop afgrøde", ADFieldView.cropText({kind = "harvest", cropTitle = "Majs"}, T), "Majs")
-- FVstore: marksilo først, gæringssilo, så afstand; valgte vises altid; kun de nærmeste af resten
local stores = {
{id = "fjern", distance = 3000},
{id = "nær", distance = 200},
{id = "mark", distance = 90, fieldSilo = true},
{id = "valgt", distance = 5000, chosen = true},
{id = "gær", distance = 800, fermenter = true},
{id = "midt", distance = 1200},
}
local storeRows, hiddenStores = ADFieldView.storeRows(stores, 2)
local order = {}
for _, e in ipairs(storeRows) do table.insert(order, e.id) end
check("FVstore rækkefølge", table.concat(order, ","), "mark,gær,nær,midt,valgt")
check("FVstore skjulte", hiddenStores, 1)
check("FVstore meter", ADFieldView.distanceText(90.4), "90 m")
check("FVstore km", ADFieldView.distanceText(1420), "1,4 km")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+22
View File
@@ -0,0 +1,22 @@
-- Kør: luajit tests/test_adFieldWorkBuying.lua (fra repo-roden)
-- Påfyldning ved købestation (in-game 26/9: "Liquid Buy Station", FarmFillStations buyingStationConfigurable):
-- ingen lagerbeholdning, så varen læses fra udbud/triggere/AI-varer.
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
Logging = {info = function() end, warning = function() end}
dofile("FS25_ADSmartPickup/adFieldWork.lua")
local HERBICIDE, LIME, SEEDS = 20, 21, 22
local W = ADFieldWork
check("BS1 udbudte varer", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, HERBICIDE), true)
check("BS2 triggerens varer", W.buyingSupports({loadTriggers = {{fillTypes = {[HERBICIDE] = true}}}}, HERBICIDE), true)
check("BS3 AI-varer", W.buyingSupports({getAISupportedFillTypes = function() return {[SEEDS] = true} end}, SEEDS), true)
check("BS4 sælger ikke kalk", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, LIME), false)
check("BS5 fejl i opslag = nej", W.buyingSupports({getAISupportedFillTypes = function() error("x") end}, LIME), false)
check("BS6 ingen station", W.buyingSupports(nil, LIME), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+33
View File
@@ -61,5 +61,38 @@ 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: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false) check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
-- F8: slåafgrøde fuldt udvokset (Dakota-græs 3 = mellemhøjde, 4-5 = fuld)
check("F8 prime græs", F.primeState({minHarvestingGrowthState = 3, maxHarvestingGrowthState = 5}), 4)
check("F8 prime ét klar-stadie", F.primeState({minHarvestingGrowthState = 6, maxHarvestingGrowthState = 6}), 6)
check("F8 prime ukendt", F.primeState({}), nil)
check("F8 mellemhøjde tæller ikke", F.isPrime(3, 4), false)
check("F8 fuld højde tæller", F.isPrime(5, 4), true)
check("F8 ukendt stadie tæller", F.isPrime(nil, 4), true)
check("F8 ingen krav", F.isPrime(3, nil), true)
-- F9: kartofler (Dakota): 6 = klar men kun forberedelig, 10 = toppet (høstklar)
local potato = {minHarvestingGrowthState = 10, maxHarvestingGrowthState = 10, minPreparingGrowthState = 6, maxPreparingGrowthState = 6, witheredState = 7, cutState = 8}
check("F9 utoppet er forberedelig", F.isPreparable(6, potato), true)
check("F9 voksende ikke", F.isPreparable(5, potato), false)
check("F9 fase klar", F.growthPhase(6, potato), "ready")
check("F9 toppet klar", F.growthPhase(10, potato), "ready")
check("F9 ingen forberedelse", F.isPreparable(6, {minPreparingGrowthState = -1}), false)
-- jordtype -> klargjort/pløjet (værdierne kommer fra spillets fieldGroundSystem)
local groundValues = {STUBBLE_TILLAGE = 1, CULTIVATED = 2, SEEDBED = 3, PLOWED = 4, ROLLED_SEEDBED = 5, SOWN = 6}
local function cls(v) local p, pl = ADFields.classifyGround(v, groundValues) return tostring(p) .. "|" .. tostring(pl) end
check("jord: pløjet", cls(4), "true|true")
check("jord: kultiveret", cls(2), "true|false")
check("jord: stubbearbejdet", cls(1), "true|false")
check("jord: sået", cls(6), "false|false")
check("jord: ukendt værdi", cls(nil), "nil|nil")
groundValues.NONE = 0
check("jord: ingen markjord tæller ikke med", cls(0), "nil|nil")
-- vin/oliven er aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen (in-game 29/9: F22 hvide druer fik
-- planen "Silage + så" og en skårlægger sendt ud)
check("vin: druer ikke slåbare", ADFields.isMowableFruit("GRAPE", {windrow = "GRASS_WINDROW"}), false)
check("vin: oliven ikke slåbare", ADFields.isMowableFruit("oliveb", {windrow = "X"}), false)
check("græs slåbart", ADFields.isMowableFruit("GRASS", {windrow = "GRASS_WINDROW"}), true)
check("ikke på MOWER-listen", ADFields.isMowableFruit("WHEAT", nil), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+99
View File
@@ -0,0 +1,99 @@
-- Kør: luajit tests/test_adFieldsPolygon.lua (fra repo-roden)
-- FS25-marker har ingen polygonPoints — omridset hentes med field:getDensityMapPolygon():getVerticesList().
-- In-game 26/9 (Dakota 16x, marker på 1,7 x 0,6 km): uden omrids blev kun markskiltet (posX/posZ) prøvet;
-- F108 med majs viste "bar jord" og "mangler markør" (markøren stod ved kanten, 890 m fra skiltet).
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
Logging = {warning = function() end, info = function() end}
local MAIZE = 10
function getWorldTranslation() return 0, 0, 0 end
function getTerrainHeightAtWorldPos() return 0 end
-- markskiltet står i marken, men ved en stribe uden afgrøde (x > 1400)
function cropAt(x) return x <= 1400 and MAIZE or nil end
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() return 1, 2 end,
getFruitTypeIndexAtWorldPos = function(x, z) return cropAt(x) end,
getFruitArea = function() return 0, 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function() return {name = "MAIZE", fillType = {index = 60, title = "Majs"}, minHarvestingGrowthState = 7, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
local polygon = {getVerticesList = function() return {0, 0, 1600, 0, 1600, 600, 0, 600} end}
local field = {getId = function() return 108 end, farmland = {id = 108}, posX = 1500, posZ = 50,
getDensityMapPolygon = function() return polygon end}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F108"}} end,
-- markøren står 5 m uden for marken ved den modsatte kant: 1505 m fra skiltet
getWayPointById = function() return {x = -5, z = 300} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local now = 0
for _ = 1, 1000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
local info = ADFields.getInfo(108)
check("PG1 marken er scannet", info ~= nil, true)
check("PG2 omrids fra getDensityMapPolygon", info and #info.verts, 4)
check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, "MAIZE")
check("PG4 ikke bar mark", info and info.kind, "harvest")
check("PG5 markør ved kanten findes (afstand til omridset)", info and info.markerId, 900)
check("PG6 afstand = til kanten", info and info.markerDistance, 5)
check("PG6b markens form-signatur (kursus-genbrug)", info and info.shapeSig, "n=4;a=960000;c=800,300")
-- PG7-9: field.fieldState er spillets sandhed (fields.xml: MAIZE growthState 2); jord-bits fra
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2}
FSDensityMapUtil.getFieldDataAtWorldPosition = function() return true, 0 end -- jord-bits 0 lignede "ingen vækst"
local function rescan()
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
for _ = 1, 2000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
return ADFields.getInfo(108)
end
info = rescan()
check("PG7 majs i vækst (fieldState), ikke bar", info and info.kind, "harvest")
check("PG7 fase vokser", info and info.phase, "growing")
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 9}
info = rescan()
check("PG8 høstet (cutState) = bar", info and info.kind, "bare")
check("PG8 stub af majs", info and info.stubbleOf, "Majs")
field.fieldState = {fruitTypeIndex = 0, growthState = 0}
cropAt = function() return nil end
info = rescan()
check("PG9 ingen afgrøde = bar uden stub", info and info.kind .. "/" .. tostring(info.stubbleOf), "bare/nil")
-- PG10-12: bygget-over mark (fields.xml: groundType NONE — gårdsplads/stald/biogas) er ikke med i listen
field.fieldState = {fruitTypeIndex = 0, growthState = 0, groundType = 0}
info = rescan()
check("PG10 ingen markjord = kan ikke dyrkes", info and info.cultivable, false)
check("PG10 ikke i marklisten", #ADFields.list(), 0)
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2, groundType = 7}
cropAt = function(x) return MAIZE end
info = rescan()
check("PG11 markjord = kan dyrkes", info and info.cultivable, true)
check("PG11 i marklisten", #ADFields.list(), 1)
check("PG12 ingen punkter på markjord", ADFields.isCultivable(nil, 0), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+94
View File
@@ -0,0 +1,94 @@
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
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
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
local GRASS, WHEAT = 1, 3
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
function getTerrainHeightAtWorldPos() return 0 end
local calls = 0
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() calls = calls + 1; return true, 170 end, -- på mark (2. værdi = jord-bits, IKKE vækst)
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
-- spillets egen marktilstand (field.fieldState, som fields.xml): afgrøde + vækststadie
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50,
fieldState = {fruitTypeIndex = WHEAT, growthState = 6}}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
getWayPointById = function(_, id) return {x = 40, z = 40} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local maxPerFrame, frames = 0, 0
local now = 0
for _ = 1, 200 do
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
frames = frames + 1
now = now + 16
if ADFields.getInfo(7) ~= nil then break end
end
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
now = now + 16
end
local info = ADFields.getInfo(7)
check("SC1 marken er scannet", info ~= nil, true)
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
check("SC3 art = høst", info and info.kind, "harvest")
check("SC4 klar-andel", info and info.readyRatio, 1)
check("SC5 fase klar", info and info.phase, "ready")
check("SC6 markør F7", info and info.markerId, 900)
check("SC7 prøvepunkter", info and #info.points, 5)
check("SC8 gødning læst", info and info.fert, 1)
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
check("SC10 marklisten", #ADFields.list(), 1)
-- SC11: med ur kører flere opgaver pr. frame inden for budgettet (0,5 ms), højst MAX_TASKS_PER_FRAME
local fakeTime = 0
netGetTime = function() fakeTime = fakeTime + 0.1; return fakeTime end -- hver opgave "koster" 0,1 ms
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
local frames2 = 0
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
frames2 = frames2 + 1
end
check("SC11 scannet igen med ur", ADFields.getInfo(7) ~= nil, true)
check("SC11 færre frames end én opgave pr. frame", frames2 < frames + 0, true)
print(" (frames uden ur: " .. frames .. ", med ur: " .. frames2 .. ")")
netGetTime = nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+44
View File
@@ -0,0 +1,44 @@
-- Kør: luajit tests/test_adFieldsShape.lua (fra repo-roden)
-- Markens form-fingeraftryk (genbrug af Courseplay-kurser): antal hjørner, areal og tyngdepunkt, afrundet så
-- små float-forskelle ikke tæller — men en ændret mark (nyt hjørne, flyttet kant) giver en ny signatur.
dofile("FS25_ADSmartPickup/adFields.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 rect(x0, z0, w, h)
return {{x = x0, z = z0}, {x = x0 + w, z = z0}, {x = x0 + w, z = z0 + h}, {x = x0, z = z0 + h}}
end
-- S1: rektangel 1600 x 600 -> 4 hjørner, 960.000 m², tyngdepunkt (800, 300)
check("S1 rektangel", ADFields.shapeSignature(rect(0, 0, 1600, 600)), "n=4;a=960000;c=800,300")
-- S2: omløbsretning er ligegyldig (arealet er absolut)
local reversed = {}
for index = 4, 1, -1 do table.insert(reversed, rect(0, 0, 1600, 600)[index]) end
check("S2 modsat omløb", ADFields.shapeSignature(reversed), "n=4;a=960000;c=800,300")
-- S3: små float-forskelle (cm) tæller ikke
local noisy = rect(0.0003, -0.0002, 1600.0004, 599.9997)
check("S3 float-støj", ADFields.shapeSignature(noisy), "n=4;a=960000;c=800,300")
-- S4: flyttet kant (1 m bredere) -> ny signatur
check("S4 kant flyttet", ADFields.shapeSignature(rect(0, 0, 1601, 600)) ~= ADFields.shapeSignature(rect(0, 0, 1600, 600)), true)
-- S5: ekstra hjørne på kanten (samme areal) -> ny signatur
local extra = {{x = 0, z = 0}, {x = 800, z = 0}, {x = 1600, z = 0}, {x = 1600, z = 600}, {x = 0, z = 600}}
check("S5 ekstra hjørne", ADFields.shapeSignature(extra), "n=5;a=960000;c=800,300")
-- S6: samme form et andet sted på kortet -> anden signatur
check("S6 flyttet mark", ADFields.shapeSignature(rect(100, 0, 1600, 600)), "n=4;a=960000;c=900,300")
-- S7: tom/ingen polygon -> nil
check("S7 tom", ADFields.shapeSignature({}), nil)
check("S8 nil", ADFields.shapeSignature(nil), nil)
-- S9: under 3 hjørner er ingen flade -> nil
check("S9 to punkter", ADFields.shapeSignature({{x = 0, z = 0}, {x = 1, z = 1}}), nil)
-- S10: trekant: areal 50, tyngdepunkt (10/3, 10/3) -> (3, 3)
check("S10 trekant", ADFields.shapeSignature({{x = 0, z = 0}, {x = 10, z = 0}, {x = 0, z = 10}}), "n=3;a=50;c=3,3")
-- S11: negative koordinater afrundes symmetrisk
check("S11 negative", ADFields.shapeSignature(rect(-1000, -500, 200, 100)), "n=4;a=20000;c=-900,-450")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+33
View File
@@ -0,0 +1,33 @@
-- Kør: luajit tests/test_adFleetNames.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFleetNames.lua")
local N = ADFleetNames
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 plan = N.plan({
{id = "b", model = "Puma 260 CVX"}, {id = "a", model = "Puma 260 CVX"}, {id = "c", model = "Fastrac 4220"},
{id = "d", model = "Cow Food", inRun = true, name = "Cow Food 1"}, {id = "e", model = "CR11", selfPropelled = true, name = "Mejetærsker"},
})
local byId = {}
for _, entry in ipairs(plan) do byId[entry.id] = entry end
check("navne: samme model nummereres (efter id)", byId.a.name, "Puma 260 CVX 1")
check("navne: anden af samme model", byId.b.name, "Puma 260 CVX 2")
check("navne: enlig model uden nummer", byId.c.name, "Fastrac 4220")
check("navne: flow-traktor beholder navn", byId.d.name, "Cow Food 1")
check("navne: selvkørende beholder navn", byId.e.name, "Mejetærsker")
check("parkering: P + navn", byId.a.parkName, "P Puma 260 CVX 1")
check("parkering: flow-traktor", byId.d.parkName, "P Cow Food 1")
check("omdøb: kun hvis ændret", byId.d.rename, false)
check("omdøb: ny navn", byId.a.rename, true)
-- R7: parkeringer omdøbes kun for maskiner der selv omdøbes, og ikke delte parkeringer
check("R7 flow-traktor: parkering røres ikke", byId.d.renamePark, false)
check("R7 selvkørende: parkering røres ikke", byId.e.renamePark, false)
check("R7 omdøbt traktor: parkering omdøbes", byId.a.renamePark, true)
local shared = N.plan({{id = "x", model = "M", park = 5}, {id = "y", model = "Q", park = 5}}, {[5] = 2})
check("R7 delt parkering røres ikke", shared[1].renamePark, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+298
View File
@@ -0,0 +1,298 @@
-- Kør: luajit tests/test_adFlowGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFlowGuide.lua")
local FG, G = ADFlowGuide, ADGuide
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 newRun(kind, changes)
local store, run = ADRuns.createRun(ADRuns.newStore(), "Mælk ud", kind)
if changes ~= nil then store = ADRuns.updateRun(store, run.id, changes) end
return ADRuns.getRun(store, run.id)
end
local function ctx(extra)
local context = {
buildings = {
{id = "b1", name = "Kostald", label = "Kostald M", hasMarker = true, text = "Kostald — M 5 m"},
{id = "b2", name = "Silo", label = "Silo M", hasMarker = true, text = "Silo — M 3 m"},
{id = "b3", name = "Lade", hasMarker = false, text = "Lade — INGEN MARKØR"},
},
fillTypes = {{name = "HAY", title = "Hø"}, {name = "STRAW", title = "Halm"}},
products = {{name = "MILK", title = "Mælk"}, {name = "EGG", title = "Æg"}},
storeBuildings = {{id = "s1", name = "Silo Nord", text = "Silo Nord — M 5 m"}, {id = "s2", name = "Palleskur", text = "Palleskur — M 2 m"}},
waitPools = {{group = "Wait A", count = 2}},
vehicles = {
{id = "t1", label = "Traktor 1", text = "Traktor 1 — 1000 l — OK"},
{id = "t2", text = "Traktor 2", otherRunId = 9, otherRunName = "Andet"},
{id = "t3", text = "Traktor 3", blocked = true},
{id = "t4", text = "Traktor 4"},
},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
local function textKeys(rows)
local keys = {}
for _, row in ipairs(rows) do if row.type == "text" and row.label.key ~= nil then table.insert(keys, row.label.key) end end
return table.concat(keys, ",")
end
local function at(guide, step) return G.jump(guide, step, "close") end
-- ---- trin ----
local supply = FG.open(newRun("supply"), ctx())
check("FL1 forsyning", steps(supply), "direction,buildings,goods,rules,wait,tractors,summary")
local out = FG.open(newRun("outbound"), ctx())
check("FL1 udkørsel uden lager-vare", steps(out), "direction,buildings,goods,rules,wait,tractors,summary")
local outStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sellElseStore"}}}), ctx())
check("FL1 udkørsel med lager", steps(outStore), "direction,buildings,goods,store,rules,wait,tractors,summary")
local new = FG.openNew("Flow 3", ctx())
check("FL1 nyt flow har navn", steps(new), "name,direction,buildings,goods,rules,wait,tractors,summary")
check("FL1 nyt flow starter på navn", new.step, "name")
check("FL1 nyt flow er forsyning", new.draft.kind, "supply")
check("FL1 nyt flow navn", new.draft.name, "Flow 3")
check("FL1 eksisterende starter på retning", supply.step, "direction")
-- ---- titler følger retningen ----
check("FL2 til", FG.stepKey("buildings", supply), "spu_guide_flow_step_to")
check("FL2 fra", FG.stepKey("buildings", out), "spu_guide_flow_step_from")
check("FL2 andre", FG.stepKey("rules", out), "spu_guide_flow_step_rules")
check("FL2 hint", FG.hintKey("goods", out), "spu_guide_flow_hint_goods_out")
check("FL2 hint forsyning", FG.hintKey("goods", supply), "spu_guide_flow_hint_goods_in")
-- ---- choose er immutabel ----
local toOut = FG.choose(supply, "kind", "outbound")
check("FL3 retning skiftet", toOut.draft.kind, "outbound")
check("FL3 gammel urørt", supply.draft.kind, "supply")
local withTarget = FG.choose(supply, "buildings", {item = "b1", on = true})
check("FL3 forsyning: mål", table.concat(withTarget.draft.targetBuildingIds, ","), "b1")
check("FL3 forsyning: kilder urørt", #withTarget.draft.sourceBuildingIds, 0)
local withSource = FG.choose(out, "buildings", {item = "b2", on = true})
check("FL3 udkørsel: kilde", table.concat(withSource.draft.sourceBuildingIds, ","), "b2")
local policy = FG.choose(out, "policy", {item = "MILK", value = "store"})
check("FL3 politik", policy.draft.products[1].policy, "store")
check("FL3 politik gør lager-trin synligt", FG.isVisible("store", policy), true)
local off = FG.choose(policy, "policy", {item = "MILK", value = "off"})
check("FL3 politik fra fjerner", #off.draft.products, 0)
local replaced = FG.choose(policy, "policy", {item = "MILK", value = "sell"})
check("FL3 politik erstattes", #replaced.draft.products, 1)
check("FL3 politik ny", replaced.draft.products[1].policy, "sell")
check("FL3 navn", FG.choose(new, "name", "Æg til salg").draft.name, "Æg til salg")
check("FL3 tomt navn ignoreres", FG.choose(new, "name", " ").draft.name, "Flow 3")
-- ---- rækker ----
local dirRows = FG.rows(supply)
check("FL4 retning valg", findRow(dirRows, "kind").type, "choice")
check("FL4 retning værdi", findRow(dirRows, "kind").value, "supply")
local buildingRows = FG.rows(at(supply, "buildings"))
check("FL4 bygninger med markør", countType(buildingRows, "check"), 2)
check("FL4 vis alle", findRow(buildingRows, "showAllBuildings").type, "toggle")
local allRows = FG.rows(at(FG.choose(supply, "showAllBuildings", true), "buildings"))
check("FL4 vis alle: tre", countType(allRows, "check"), 3)
local noHidden = FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {{id = "b1", name = "Kostald", hasMarker = true, text = "K"}}})), "buildings"))
check("FL4 ingen vis-alle uden skjulte", findRow(noHidden, "showAllBuildings"), nil)
local chosenFirst = FG.rows(at(FG.choose(supply, "buildings", {item = "b2", on = true}), "buildings"))
local firstCheck
for _, row in ipairs(chosenFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FL4 fast rækkefølge (navn)", firstCheck.item, "b1")
check("FL4 valgt markeret", findRow(chosenFirst, "buildings", "b2").value, true)
check("FL4 ingen bygninger", textKeys(FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {}})), "buildings"))), "spu_noBuildings")
local goodsAuto = FG.rows(at(supply, "goods"))
check("FL5 forsyning auto", findRow(goodsAuto, "ingredientMode").value, "auto")
check("FL5 forsyning auto: ingen liste", countType(goodsAuto, "check"), 0)
local goodsManual = FG.rows(at(FG.choose(supply, "ingredientMode", "manual"), "goods"))
check("FL5 forsyning vælg: varer", countType(goodsManual, "check"), 2)
check("FL5 forsyning vælg uden varer", textKeys(FG.rows(at(FG.choose(FG.open(newRun("supply"), ctx({fillTypes = {}})), "ingredientMode", "manual"), "goods"))), "spu_guide_flow_noGoodsTo")
local goodsOut = FG.rows(at(policy, "goods"))
check("FL5 udkørsel: politik pr. vare", countType(goodsOut, "choice"), 2)
check("FL5 udkørsel: mælk lager", findRow(goodsOut, "policy", "MILK").value, "store")
check("FL5 udkørsel: æg fra", findRow(goodsOut, "policy", "EGG").value, "off")
check("FL5 udkørsel: politikker", #findRow(goodsOut, "policy", "EGG").options, 4)
check("FL5 udkørsel uden varer", textKeys(FG.rows(at(FG.open(newRun("outbound"), ctx({products = {}})), "goods"))), "spu_guide_flow_noProducts")
local storeRows = FG.rows(at(outStore, "store"))
check("FL6 lager auto", findRow(storeRows, "storeAuto").value, true)
check("FL6 lager auto: ingen liste", countType(storeRows, "check"), 0)
local storeManual = FG.rows(at(FG.choose(outStore, "storeAuto", false), "store"))
check("FL6 lager vælg selv", countType(storeManual, "check"), 2)
local chosenStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}, storeBuildingIds = {"s2"}}), ctx())
check("FL6 valgt lager -> vælg selv", chosenStore.draft.storeAuto, false)
local rulesOut = FG.rows(at(out, "rules"))
check("FL7 udkørsel: sælg fra", findRow(rulesOut, "sellAtPercent").value, 90)
check("FL7 udkørsel: sælg-trin", #findRow(rulesOut, "sellAtPercent").options, 11)
check("FL7 udkørsel: lager over", findRow(rulesOut, "storeAbovePercent").value, 80)
check("FL7 udkørsel: samlerunde", findRow(rulesOut, "collectRound").type, "toggle")
check("FL7 udkørsel: ingen ture", findRow(rulesOut, "loops"), nil)
local rulesIn = FG.rows(at(supply, "rules"))
check("FL7 forsyning: ture", findRow(rulesIn, "loops").value, 0)
check("FL7 forsyning: ture-valg", #findRow(rulesIn, "loops").options, ADRuns.MAX_LOOPS + 1)
check("FL7 forsyning: 0 = uendeligt", findRow(rulesIn, "loops").options[1].label.key, "spu_loops_infinite")
check("FL7 forsyning: retur", findRow(rulesIn, "returnBelowPercent").value, 50)
check("FL7 forsyning: ingen salg", findRow(rulesIn, "sellAtPercent"), nil)
local waitRows = FG.rows(at(supply, "wait"))
check("FL8 vent: alle først", findRow(waitRows, "waitPoolGroup").options[1].value, "")
check("FL8 vent: puljer", #findRow(waitRows, "waitPoolGroup").options, 2)
local assigned = FG.open(newRun("supply", {}), ctx())
assigned = FG.choose(assigned, "vehicleIds", {item = "t4", on = true})
local tractorRows = FG.rows(at(assigned, "tractors"))
check("FL9 traktorer: kun egne + ledige", countType(tractorRows, "check"), 2)
check("FL9 egne først", (function() for _, r in ipairs(tractorRows) do if r.type == "check" then return r.item end end end)(), "t4")
check("FL9 skjult andre flows", findRow(tractorRows, "vehicleIds", "t2"), nil)
check("FL9 skjult blokeret", findRow(tractorRows, "vehicleIds", "t3"), nil)
check("FL9 linjer", textKeys(tractorRows), "spu_guide_flow_otherFlows")
-- blokerede traktorer: foldet sammen som standard (in-game 27/9: 37 linjer der ikke kunne bruges fyldte trinnet);
-- linjen med antallet er et link der folder navn + grund ud og ind
local function blockedLine(rows)
for _, r in ipairs(rows) do if r.type == "text" and r.label.text ~= nil and r.label.text:find("Traktor 3", 1, true) then return r end end
return nil
end
local function blockedLink(rows)
for _, r in ipairs(rows) do if r.type == "link" and r.id == "showBlocked" then return r end end
return nil
end
check("FL9 blokerede foldet sammen som standard", blockedLine(tractorRows), nil)
check("FL9 link med antal", blockedLink(tractorRows) ~= nil and blockedLink(tractorRows).label.key, "spu_guide_flow_blockedShow")
local expanded = FG.choose(assigned, "showBlocked", true)
local expandedRows = FG.rows(at(expanded, "tractors"))
check("FL9 foldet ud viser navn + grund", blockedLine(expandedRows) ~= nil, true)
check("FL9 foldet ud: link skjuler igen", blockedLink(expandedRows).label.key, "spu_guide_flow_blockedHide")
check("FL9 fold ud ændrer ikke kladden", expanded.draft.vehicleIds[1], "t4")
check("FL9 fold sammen igen", blockedLine(FG.rows(at(FG.choose(expanded, "showBlocked", true), "tractors"))), nil)
local inThisRun = FG.open(newRun("supply"), ctx({vehicles = {{id = "t2", text = "T2", otherRunId = 1, otherRunName = "Mælk ud"}}}))
check("FL9 traktor i dette flow er ikke 'andet flow'", countType(FG.rows(at(inThisRun, "tractors")), "check"), 1)
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partText(line)
local parts = {}
for _, part in ipairs(line.parts) do table.insert(parts, part.key or ("=" .. tostring(part.text))) end
return table.concat(parts, ",")
end
local sumIn = FG.summary(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}))
check("FL10 navn", partText(lineFor(sumIn, "name")), "=Mælk ud")
check("FL10 retning", partText(lineFor(sumIn, "direction")), "spu_direction_in")
check("FL10 til", partText(lineFor(sumIn, "buildings")), "=Kostald")
check("FL10 til-titel", lineFor(sumIn, "buildings").title.key, "spu_guide_flow_step_to")
check("FL10 varer auto", partText(lineFor(sumIn, "goods")), "spu_guide_flow_goodsAuto")
check("FL10 regler", partText(lineFor(sumIn, "rules")), "spu_guide_flow_sumLoopsInf,spu_guide_flow_sumReturn")
check("FL10 vent", partText(lineFor(sumIn, "wait")), "spu_waitPool_all")
check("FL10 traktorer", partText(lineFor(sumIn, "tractors")), "=Traktor 1")
check("FL10 intet lager-trin", lineFor(sumIn, "store"), nil)
-- traktor der ikke findes længere (solgt): aldrig rå id i resuméet (in-game 29/9)
local withGhost = FG.choose(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}), "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b resumé springer manglende over", partText(lineFor(FG.summary(withGhost), "tractors")), "=Traktor 1")
local onlyGhost = FG.choose(withTarget, "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b kun manglende -> ingen traktorer", partText(lineFor(FG.summary(onlyGhost), "tractors")), "spu_guide_flow_sumNoTractors")
local sumOut = FG.summary(FG.choose(FG.choose(policy, "collectRound", true), "sellAtPercent", 95))
check("FL11 retning ud", partText(lineFor(sumOut, "direction")), "spu_direction_out")
check("FL11 fra tom", partText(lineFor(sumOut, "buildings")), "spu_guide_flow_none")
check("FL11 varer", partText(lineFor(sumOut, "goods")), "spu_policy_store")
check("FL11 varenavn foran", lineFor(sumOut, "goods").parts[1].prefix, "Mælk: ")
check("FL11 lager auto", partText(lineFor(sumOut, "store")), "spu_guide_sum_auto")
check("FL11 regler", partText(lineFor(sumOut, "rules")), "spu_guide_flow_sumSellAt,spu_guide_flow_sumStoreAbove,spu_guide_flow_sumRound")
check("FL11 sælg 95", lineFor(sumOut, "rules").parts[1].args[1], 95)
check("FL11 ingen traktorer", partText(lineFor(sumOut, "tractors")), "spu_guide_flow_sumNoTractors")
local manyGoods = FG.summary(FG.choose(FG.choose(FG.choose(supply, "ingredientMode", "manual"), "ingredients", {item = "HAY", on = true}), "ingredients", {item = "STRAW", on = true}))
check("FL12 valgte varer", partText(lineFor(manyGoods, "goods")), "=Hø + Halm")
check("FL12 oversigt: links", countType(FG.rows(G.new(supply.def, supply.draft, supply.context, {step = "summary"})), "link"), 7)
-- ---- resultat ----
local r = FG.result(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))
check("FL13 ikke ny", r.isNew, false)
check("FL13 run-id", r.runId, 1)
check("FL13 retning", r.changes.kind, "outbound")
check("FL13 lager valgt", table.concat(r.changes.storeBuildingIds, ","), "s1")
check("FL13 varer", r.changes.products[1].fillType, "MILK")
check("FL13 lager-resumé bruger navn", (function() for _, l in ipairs(FG.summary(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))) do if l.step == "store" then return l.parts[1].text end end end)(), "Silo Nord")
local rAuto = FG.result(FG.choose(chosenStore, "storeAuto", true))
check("FL13 lager auto -> tom", #rAuto.changes.storeBuildingIds, 0)
local hiddenStore = FG.open(newRun("outbound", {storeBuildingIds = {"s2"}}), ctx())
check("FL13 lager usynligt: bevares", table.concat(FG.result(hiddenStore).changes.storeBuildingIds, ","), "s2")
local rNew = FG.result(FG.choose(new, "vehicleIds", {item = "t1", on = true}))
check("FL14 nyt", rNew.isNew, true)
check("FL14 navn", rNew.name, "Flow 3")
check("FL14 traktorer", table.concat(rNew.vehicleIds, ","), "t1")
check("FL14 ingen run-id", rNew.runId, nil)
local added, removed = FG.vehicleChanges({"a", "b"}, {"b", "c"})
check("FL15 tilføjet", table.concat(added, ","), "c")
check("FL15 fjernet", table.concat(removed, ","), "a")
-- ---- forhåndsvisning (til controllerens opslag) ----
local markers = {b1 = {101, 102}, b2 = {201}}
local preview = FG.previewRun(withSource.draft, function(id) return markers[id] or {} end)
check("FL16 kilder -> markører", table.concat(preview.sourceWayPointIds, ","), "201")
local previewIn = FG.previewRun(withTarget.draft, function(id) return markers[id] or {} end)
check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), "101,102")
check("FL16 primært mål", previewIn.targetWayPointId, 101)
check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true)
-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ----
local soloCtx = ctx({
vehicles = {
{id = "s1", label = "Solo 1", text = "Solo 1", solo = true},
{id = "s2", label = "Solo 2", text = "Solo 2", solo = true},
{id = "s3", label = "Solo 3", text = "Solo 3", solo = true},
{id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"},
},
wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}},
})
local soloGuide = FG.open(newRun("supply"), soloCtx)
local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true})
check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1")
local two = FG.choose(one, "vehicleIds", {item = "s2", on = true})
check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2")
check("WG2 første beholder sin", two.draft.wagons.s1, "w1")
local three = FG.choose(two, "vehicleIds", {item = "s3", on = true})
check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil)
local soloRows = FG.rows(at(three, "tractors"))
local wagonRow = findRow(soloRows, "wagon", "s1")
check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice")
check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1")
check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1)
local noFree = false
for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end
check("WG5 solo uden ledig vogn får besked", noFree, true)
check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil)
local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"})
check("WG6 skift vogn", swapped.draft.wagons.s1, "w2")
local off = FG.choose(two, "vehicleIds", {item = "s1", on = false})
check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil)
local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true})
check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1")
check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2")
check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil)
local remembered = newRun("supply", {vehicleIds = {"s2"}})
remembered.wagons = {s2 = "w2"}
local reopened = FG.open(remembered, soloCtx)
check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2")
check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1")
check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+24 -2
View File
@@ -57,13 +57,35 @@ local buildings = {
{id = "a", name = "Anlæg", hasMarker = true}, {id = "a", name = "Anlæg", hasMarker = true},
} }
local chosen = {s10 = true, y = 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") -- fast rækkefølge (et tryk må ikke flytte rækken — in-game 26/9 hoppede valgte drops op i toppen)
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,s10,a,s2,x") check("FV3 standard: fast rækkefølge efter navn, valgte bliver hvor de er", ids(V.buildingRows(buildings, chosen, false)), "y,a,s2,s10")
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,a,s2,s10,x")
-- ens bygningsnavne (5 x "BGA (1) : DOB 115") sorteres efter markørnavnet
local drops = {
{id = "d5", name = "DOB", label = "Biogas Drop 5", hasMarker = true},
{id = "d3", name = "DOB", label = "Biogas Drop 3", hasMarker = true},
{id = "d1", name = "DOB", label = "Biogas Drop 1", hasMarker = true},
{id = "d4", name = "DOB", label = "Biogas Drop 4", hasMarker = true},
{id = "d2", name = "DOB", label = "Biogas Drop 2", hasMarker = true},
}
check("FV3 ens navne efter markør", ids(V.buildingRows(drops, {d2 = true}, false)), "d1,d2,d3,d4,d5")
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst -- 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 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 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)") check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
-- FV-wrap: lange tekstrækker ombrydes ved mellemrum; indrykning gentages; kort tekst uændret
local wrapped = V.wrapText(" Prepared and sown with Sugar Beet as soon as it is sowing time for it", 30)
check("FVwrap flere linjer", #wrapped > 1, true)
check("FVwrap første linje indrykket", string.sub(wrapped[1], 1, 4), " ")
check("FVwrap anden linje indrykket", string.sub(wrapped[2], 1, 4), " ")
local maxLen = 0
for _, line in ipairs(wrapped) do if #line > maxLen then maxLen = #line end end
check("FVwrap ingen linje over grænsen", maxLen <= 30, true)
check("FVwrap intet ord tabt", table.concat(wrapped, " "):gsub("%s+", " "):gsub("^ ", ""), "Prepared and sown with Sugar Beet as soon as it is sowing time for it")
check("FVwrap kort tekst", V.wrapText("Kort", 30)[1], "Kort")
check("FVwrap langt ord deles ikke", V.wrapText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10)[1], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+140
View File
@@ -0,0 +1,140 @@
-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowWagons.lua")
local W = ADFlowWagons
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
-- gem/indlæs: traktor -> vogn
local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"})
check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2")
local parsed = W.parse(text)
check("indlæs: A", parsed.vehicleA, "wagon1")
check("indlæs: B", parsed.vehicleB, "wagon2")
check("indlæs: tom", next(W.parse("")), nil)
check("indlæs: nil", next(W.parse(nil)), nil)
check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil)
-- husk vogn: ny tabel, gammel urørt
local before = {t1 = "w1"}
local after = W.remember(before, "t1", "w9")
check("husk: ny vogn", after.t1, "w9")
check("husk: original urørt", before.t1, "w1")
check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after)
-- start: hent / vent / start
check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start")
check("start: ingen husket vogn", W.startAction({}), "start")
check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start")
check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start")
check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait")
check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait")
check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch")
-- ejer: flowets vogn er flowets — også når flowet er stoppet (in-game 29/9: marksilo lånte Cow Food-vognen)
local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}}
check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true)
check("ejer: stoppet flow holder stadig", W.isHeld(runs, "w2"), true)
check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false)
check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2)
-- bygning væk: to tjek i træk
local count, gone = W.missingStep(0, true)
check("væk: første tjek -> ikke endnu", gone, false)
count, gone = W.missingStep(count, true)
check("væk: andet tjek -> væk", gone, true)
count, gone = W.missingStep(1, false)
check("væk: fundet igen nulstiller", count, 0)
check("væk: fundet igen -> ikke væk", gone, false)
-- manglende bygninger i et flow
local exists = {a = true, b = true}
check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists, exists), 0)
check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists, {a = true, c = true})[1], "c")
-- review v1.51
check("R1 traktoren er lånt ud af puljen -> vent", W.startAction({tractorBusy = true, hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "wait")
local runs2 = {{id = 1, stopped = true, wagons = {t1 = "w1"}}, {id = 2, stopped = false, wagons = {t9 = "w1"}}}
check("R4 holdt hvis ET kørende flow har vognen", W.isHeld(runs2, "w1"), true)
local seen = {a = true, c = true}
check("R+ bygning aldrig set i sessionen tæller ikke som væk", #W.missingBuildings({targetBuildingIds = {"b"}}, {a = true}, seen), 0)
check("R+ bygning set og nu væk", W.missingBuildings({targetBuildingIds = {"c"}}, {a = true}, seen)[1], "c")
-- gæt flowets vogn ud fra parkeringsnavnet (ingen husket vogn: vognene var sat på plads før v1.51)
local cands = {{id = "v1", parkName = "P Cow Food vogn 1"}, {id = "v3", parkName = "P Cow Food vogn 3"},
{id = "g1", parkName = "P Goat Food vogn 1"}, {id = "v2", parkName = "P Cow Food vogn 2"}}
check("gæt: samme nummer", W.guessWagon("Cow Food", 3, cands, {}), "v3")
check("gæt: nummer optaget -> anden med flowets navn", W.guessWagon("Cow Food", 3, cands, {v3 = true}), "v1")
check("gæt: andet flow", W.guessWagon("Goat Food", 2, cands, {}), "g1")
check("gæt: intet passer", W.guessWagon("Pig Food", 1, cands, {}), nil)
check("gæt: store/små bogstaver", W.guessWagon("cow food", 2, cands, {}), "v2")
check("gæt: 'Cow Food 1' matcher ikke 'Cow Food 10'", W.guessWagon("Cow Food", 1, {{id = "x", parkName = "P Cow Food vogn 10"}, {id = "y", parkName = "P Cow Food vogn 1"}}, {}), "y")
-- tøm vognen før den parkeres (flow stoppet): mål -> silo/fabrik -> hjem
check("tøm: tom vogn -> hjem", W.emptyStep("start", {cargo = 0}), "home")
check("tøm: last -> flowets mål", W.emptyStep("start", {cargo = 5000, hasTarget = true}), "target")
check("tøm: last uden mål -> silo", W.emptyStep("start", {cargo = 5000, hasTarget = false}), "dump")
check("tøm: kører stadig -> vent", W.emptyStep("target", {cargo = 5000, adActive = true}), "wait")
check("tøm: mål fuldt, rest -> silo", W.emptyStep("target", {cargo = 2000}), "dump")
check("tøm: målet tog det hele -> hjem", W.emptyStep("target", {cargo = 0}), "home")
check("tøm: silo tog det -> hjem", W.emptyStep("dump", {cargo = 0}), "home")
check("tøm: silo kunne ikke -> giv op, hjem", W.emptyStep("dump", {cargo = 2000}), "giveUp")
check("tøm: for længe -> giv op", W.emptyStep("target", {cargo = 5000, adActive = true, elapsedMs = W.EMPTY_TIMEOUT_MS}), "giveUp")
-- flowet startes igen mens traktoren tømmer/kører hjem fra et stop: hjemturen afbrydes, den kører videre med vognen
-- (in-game 29/9: Stop+Start med 1 s mellemrum -> tømning ved forkert mål i 10 min, vogn sat af, traktor i garage)
check("genstart: tømmer -> afbryd", W.abortHomeTripOnStart({emptying = true}), true)
check("genstart: flowets hjemtur -> afbryd", W.abortHomeTripOnStart({flowHoming = true}), true)
check("genstart: puljens hjemtur (markarbejde) -> rør ikke", W.abortHomeTripOnStart({poolReturning = true}), false)
check("genstart: intet i gang", W.abortHomeTripOnStart({}), false)
-- tøm-mål: kun flowets egne mål — AD's 2. markør kan være forældet (æg-markør) eller et Wait-punkt (in-game 29/9)
check("tøm-mål: AD's mål er et af flowets", W.emptyTarget(12638, {12636, 12638}), 12638)
check("tøm-mål: forældet/Wait-markør -> flowets første mål", W.emptyTarget(12658, {12636, 12638}), 12636)
check("tøm-mål: intet AD-mål -> flowets første", W.emptyTarget(nil, {12636}), 12636)
check("tøm-mål: flow uden mål -> intet (silo)", W.emptyTarget(12658, {}), nil)
check("tøm-mål: ingen liste -> intet", W.emptyTarget(12658, nil), nil)
-- ledig traktor (puljen/markarbejde/marksilo må låne den): kun solo fra et stoppet flow
check("ledig: intet flow", W.tractorFree({}), true)
check("ledig: kørende flow", W.tractorFree({hasRun = true}), false)
check("ledig: stoppet flow, solo", W.tractorFree({hasRun = true, stopped = true}), true)
check("ledig: stoppet flow, flow-vogn bagpå", W.tractorFree({hasRun = true, stopped = true, carriesFlowWagon = true}), false)
check("ledig: stoppet flow, tømmer/kører hjem", W.tractorFree({hasRun = true, stopped = true, homeTrip = true}), false)
-- venteårsag i loggen (før stod der altid "i brug andetsteds")
check("vent-årsag: traktor optaget", W.waitReason({tractorBusy = true}), "tractorBusy")
check("vent-årsag: vogn på en anden", W.waitReason({wagonAttached = true}), "wagonAttached")
check("vent-årsag: vogn reserveret", W.waitReason({wagonBusy = true}), "wagonReserved")
-- tømning går i stå ved et fuldt mål (in-game 29/9: fåre-/BGA-vogne stod 10 min ved fuldt mål efter Stop alt)
check("tøm: står ved fuldt mål -> silo", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = true}), "dump")
check("tøm: står ved silo uden at tømme -> næste silo", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = 1}), "dump")
check("tøm: 3 siloer prøvet -> giv op", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = W.MAX_DUMP_ATTEMPTS}), "giveUp")
check("tøm: kører mod målet (ikke i stå) -> vent", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = false}), "wait")
check("i stå: tæt på, uændret længe", W.isStalled({nearTarget = true, sinceChangeMs = W.EMPTY_STALL_MS}), true)
check("i stå: tæt på, lige ankommet", W.isStalled({nearTarget = true, sinceChangeMs = 5000}), false)
check("i stå: på vej (langt væk)", W.isStalled({nearTarget = false, sinceChangeMs = 999999}), false)
-- hent vogn: ét nyt forsøg efter "stoppedAway" (AD standsede undervejs — fx spærret af en anden traktor)
check("hent igen: stoppedAway første gang", W.retryFetch("stoppedAway (37 m fra markøren)", 0), true)
check("hent igen: stoppedAway anden gang -> nej", W.retryFetch("stoppedAway", 1), false)
check("hent igen: noAttachable -> nej (markøren skal rettes)", W.retryFetch("noAttachable", 0), false)
-- manuel påkobling (spilleren sendte traktoren til Connect): traktor i et kørende flow startes bagefter
-- (in-game 29/9: Cow Food 3 hentede sin vogn manuelt efter et fejlet forsøg og blev holdende)
check("manuel kobling: kørende flow, egen vogn -> start", W.startAfterManualAttach({flowRunning = true, ownWagon = true}), true)
check("manuel kobling: stoppet flow -> nej", W.startAfterManualAttach({flowRunning = false, ownWagon = true}), false)
check("manuel kobling: AD kører allerede -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = true, adActive = true}), false)
check("manuel kobling: fremmed vogn -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = false}), false)
-- husk vogn ved hjemtur: kun når flowet KØRTE med den (stop); ikke ved oprydning efter indlæsning, hvor traktoren
-- kan have et puljeredskab på fra et lån (in-game 29/9: Biogas Silage "huskede" en læssevogn fra markarbejdet)
check("husk ved stop af kørende flow", W.shouldRememberOnHome("stop"), true)
check("husk ikke ved oprydning efter indlæsning", W.shouldRememberOnHome("afterLoad"), false)
-- forældreløs påkobling (hentet før genindlæsning, opgaven tabt): sættes tilbage efter 2 min uden ejer
check("forældreløs: ingen ejer, længe nok -> hjem", W.orphanAction({owned = false, idleMs = W.ORPHAN_MS}), "home")
check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = W.ORPHAN_MS}), "none")
check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait")
-- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad)
check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none")
-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk)
local ranked = W.rankWagons("Sugarbeet Cut", {
{id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"},
{id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}})
local order = {}
for _, w in ipairs(ranked) do table.insert(order, w.id) end
check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a")
check("rank: tom liste", #W.rankWagons("X", {}), 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+113
View File
@@ -0,0 +1,113 @@
-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
local G = ADGuide
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
-- trin a..e + summary; b vises kun når draft.showB, d aldrig
local def = {
order = {"a", "b", "c", "d", "e", "summary"},
isVisible = function(step, guide)
if step == "b" then return guide.draft.showB == true end
if step == "d" then return false end
return true
end,
}
local guide = G.new(def, {showB = false, value = 1}, {x = 1})
check("G1 start på første synlige", guide.step, "a")
local index, total = G.position(guide)
check("G1 position", index, 1)
check("G1 total over synlige", total, 4)
check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
-- næste springer skjulte over
local g2, event = G.next(guide)
check("G2 næste springer b over", g2.step, "c")
check("G2 intet event", event, nil)
check("G2 gammel guide urørt", guide.step, "a")
local g3 = G.next(g2)
check("G3 springer d over", g3.step, "e")
check("G3 position", (G.position(g3)), 3)
local g4 = G.next(g3)
check("G4 summary er sidst", G.isLast(g4), true)
local _, done = G.next(g4)
check("G4 næste på sidste = done", done, "done")
-- tilbage
local back = G.back(g3)
check("G5 tilbage til c", back.step, "c")
local _, cancel = G.back(guide)
check("G5 tilbage på første = cancel", cancel, "cancel")
-- valg der gør et trin synligt ændrer tælling og næste
local shown = G.withDraft(guide, {showB = true, value = 1})
check("G6 withDraft ny guide", guide.draft.showB, false)
check("G6 b synlig", G.next(shown).step, "b")
check("G6 total", select(2, G.position(shown)), 5)
-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
local onB = G.new(def, {showB = true}, {}, {step = "b"})
local hidden = G.withDraft(onB, {showB = false})
check("G7 position nil for skjult trin", (G.position(hidden)), nil)
check("G7 næste efter skjult", G.next(hidden).step, "c")
check("G7 tilbage fra skjult", G.back(hidden).step, "a")
-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
local fromSummary = G.jump(g4, "c", "summary")
check("G8 spring trin", fromSummary.step, "c")
check("G8 single", fromSummary.single, true)
local returned, ev = G.next(fromSummary)
check("G8 retur til summary", returned.step, "summary")
check("G8 ikke single mere", returned.single, false)
check("G8 intet event", ev, nil)
-- tilbage i spring gendanner kladden fra før springet
local changed = G.withDraft(fromSummary, {showB = false, value = 99})
local undone = G.back(changed)
check("G9 tilbage gendanner kladde", undone.draft.value, 1)
check("G9 tilbage til summary", undone.step, "summary")
-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
local _, singleDone = G.next(single)
check("G10 single close næste = done", singleDone, "done")
local _, singleCancel = G.back(single)
check("G10 single close tilbage = cancel", singleCancel, "cancel")
check("G10 single isLast", G.isLast(single), true)
-- withContext
local ctxGuide = G.withContext(guide, {x = 2})
check("G11 ny context", ctxGuide.context.x, 2)
check("G11 gammel context", guide.context.x, 1)
-- deepCopy / toggleItem
local original = {list = {1, 2}, nested = {a = {b = 1}}}
local copy = G.deepCopy(original)
copy.list[1] = 9
copy.nested.a.b = 7
check("G12 deepCopy liste", original.list[1], 1)
check("G12 deepCopy indlejret", original.nested.a.b, 1)
local list = {"x", "y"}
check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
check("G13 original urørt", #list, 2)
-- tekster
local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
local function T(key) return texts[key] or key end
check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
check("G14 prefix", G.labelText({key = "plain", prefix = "Mælk: "}, T), "Mælk: Ingen")
check("G14 nil label", G.labelText(nil, T), "")
check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
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