61 Commits

Author SHA1 Message Date
masterdraco bb287a8154 feat: spuSellBlock slår salgssteder fra/til pr. savegame 2026-09-30 23:35:01 +02:00
masterdraco e229a27624 fix: pålæsningsdiagnose kun ved stationen (ikke Wait/mark) 2026-09-30 23:28:58 +02:00
masterdraco ba98cf83d3 docs: parkering og hentning kun ved at bakke 2026-09-30 23:28:08 +02:00
masterdraco a6662d6da1 feat: diagnose når AD står ved LOAD-mål uden at fylde 2026-09-30 22:54:28 +02:00
masterdraco bf3b99aca4 fix: kald ikke AutoDrives getMarkerAtStation (opretter ekstra stationsmarkører) 2026-09-30 22:18:16 +02:00
masterdraco cbc9b2c8e3 fix: påfyldningsmarkør ved stationens egne læssetriggere (langt sæt fik anden stations markør) 2026-09-30 22:15:32 +02:00
masterdraco 1bda82a270 feat: kæder respekterer fabrikkens udgang (Fordel = intet flow, Sælg på mellemvare -> Behold) 2026-09-30 22:01:42 +02:00
masterdraco 21b48d7664 fix: objektlager tæller alle grupper af varen (Sell Egg stoppede halvfuld) 2026-09-30 21:55:01 +02:00
masterdraco 88ddb3d6cd feat(kæde): kæder slås selv til ([Kæde til]/[Fjern]); tomme kædeflows slettes; runsView med traktorer (v1.62.0.0) 2026-09-30 21:43:30 +02:00
masterdraco 37cf84c101 fix(udkørsel): hold AD's pålæsning mens palleskuret udleverer paller (v1.61.13.0) 2026-09-30 21:35:06 +02:00
masterdraco 991252848f fix(oversigt): kædeflows uden traktor foldet sammen, 'mangler traktor/vogn' som status (v1.61.12.0) 2026-09-30 21:30:45 +02:00
masterdraco 6b533e62eb fix(bygninger): markørnavn afgør mellem tætte markører (Pickup til pålæsning, Unload til aflæsning) (v1.61.11.0) 2026-09-30 18:19:31 +02:00
masterdraco c92c870297 fix(flåde): korte maskinnavne i puljelisten (v1.61.10.0) 2026-09-30 18:16:19 +02:00
masterdraco 18b6c82871 feat(udkørsel): 'Altid (uanset pris)' i Sælg fra (v1.61.9.0) 2026-09-30 18:06:51 +02:00
masterdraco 47afdebf64 feat(kæde): læs i køretøjer tæller som lager, ingen skift under høst, varelinje åbner flow (v1.61.8.0) 2026-09-30 18:03:56 +02:00
masterdraco 2290bdde95 fix(kæde): 'Gård' ved slutprodukter gården selv bruger; Sælg/Behold kun ved salgbare (v1.61.7.0) 2026-09-30 17:55:10 +02:00
masterdraco 0c21ba49a0 fix(kæde): slutprodukter gården selv bruger sælges aldrig straks; tidligere salg sættes tilbage (v1.61.6.0) 2026-09-30 17:53:56 +02:00
masterdraco e1f7b19fef feat(marker): redskaber over 50 m bruges, CP-kursus begrænses til 50 m (v1.61.5.0) 2026-09-30 17:47:34 +02:00
masterdraco 74537f7ec2 feat(kæde): ét flow pr. bygning, slutprodukter sælges straks (Sælg/Behold), tomme kædeflows foldes sammen; såmaskine ikke gødningsspreder (v1.61.4.0) 2026-09-30 17:44:06 +02:00
masterdraco d2970a1bda feat(kæde): forslag samlet øverst uden dubletter; kæder sorteret efter status, bygning, produkt (v1.61.3.0) 2026-09-30 17:16:05 +02:00
masterdraco bd106a2176 fix(flows): egne flows øverst, kædeflows samlet nederst uden 'Kæde: ' (v1.61.2.0) 2026-09-30 17:14:08 +02:00
masterdraco acf94c8e88 fix(gui): fanen Kæder efter Marker; kædelistens navne afkortes ikke (v1.61.1.0) 2026-09-30 16:32:29 +02:00
masterdraco a3d234a3a2 chore(tools): AD-vejnet-værktøjer (45°-kurver, parkering, reparationer) brugt på savegame2 30/9 2026-09-30 15:32:21 +02:00
masterdraco 68eb26d71d docs: numpad * som tast, samlede kædeposter, markvarer og README-funktioner
Manualer (DA/EN) og ændringslog: statusvinduets tast er numpad *, kædeproblemer
står i én post i Kræver handling, beskeder højst 3 ad gangen, markvarer står
som "fra marker", slukket men nødvendig højst én post med +k. README nævner
statusvinduet og afhængigheds-advarslen.
2026-09-30 15:29:51 +02:00
masterdraco 1c60529dfd fix(status): numpad * som tast og log når vinduet ikke kan tegnes
Standardtasten skifter fra venstre Alt + Insert (kolliderer med
FS25_UniversalAutoload) til numpad *. Mangler info-boksens skalering eller
overlays, logges det én gang. Statusvinduet henter "start afvist" fra den
rigtige ESC-fane, så et flow hvis start fejlede vises rødt som i Flows-listen.
2026-09-30 15:28:56 +02:00
masterdraco b33f2a3b01 fix(kæde): højst 3 beskeder og samlede poster i Kræver handling
notifyProblems sender højst 3 beskeder pr. tick og samler resten i én
("+N manglende led — se fanen Kæder"). Alle kædeproblemer står i én rød post
("Kæder: N manglende led", scoret under rød fuld; klik folder kæderne ud) og
slukkede nødvendige fabrikker i højst én post ("Slukket men nødvendig: X +k").
2026-09-30 15:27:08 +02:00
masterdraco d2a4cf0d23 fix(kæde): markvarer som hø og hakkelse kommer fra markerne
ADChainData leverer data.fieldGoods (vindue-varer, spillets konvertere, tørrede
varianter og kendte markvarer der findes som fill type). En markvare uden
producent/stald får kilden "fra marker" uden afgrødeplan og uden problem, så
fermenteringssiloernes hø/lucerne/kløver/hakkelse ikke meldes som køb.
2026-09-30 15:25:47 +02:00
masterdraco 928299c8ee fix(kæde): kædens egne flows giver ikke dobbelt advarsel
Flows med run.chain (Kæde: X → Y) tæller som kæde-behov i neededOutputs,
så onlyChain forbliver sand, når kun kæden bruger varen, og Task 10 alene
melder det. Et almindeligt flow med samme vare advarer stadig.
2026-09-30 15:14:42 +02:00
masterdraco e9f1251471 fix(kæde): afhængigheds-advarsel uden støj fra auto-flows, alternative opskrifter og kæder
- Auto-flowets mål tæller kun for varer gården forbruger (eller en kæde vil
  have); stald-foder/auto-behov uden kendt forbrug er ikke nødvendigt. Manuel
  flow-vare og kæde-input gælder også uden kendte lagerdage.
- En slukket opskrift springes over, når en anden tændt opskrift i samme
  fabrik laver varen.
- Behov kun fra kæder giver ingen advarsel/post (Task 10 melder kæderne).
2026-09-30 15:13:15 +02:00
masterdraco 9fde03f950 fix(status): farve som Flows-listen, solgte køretøjer skjules og tasten er Alt+Insert
- En flow-traktors linje følger flowets niveau (venter/fejl) som Flows-listen.
- Solgte/slettede køretøjer (isDeleted, ingen/død rootNode) vises ikke under
  stået stille.
- Standardtasten er venstre Alt + Insert (S er spillets gå/bremse-akse).
2026-09-30 15:08:33 +02:00
masterdraco 979b7f72d5 feat(kæde): advarsel når en fabrik gården er afhængig af slukkes
Slukker spilleren en produktion, hvis vare et tændt flow kører (manuel vare
eller målet tager den i auto), en stald æder eller en aktiv kæde bruger,
kommer én besked pr. slukning med brugere og lagerdage, og en rød post
'Slukket men nødvendig' i Kræver handling og statusvinduet så længe den er
slukket og nødvendig. Ingen advarsel når en anden tændt produktion dækker,
lageret rækker over 60 dage, kæden selv slukkede den eller den var slukket
ved indlæsning. Tjekket kører i kædens tick også uden kæder; ren logik i
adDependency.lua.
2026-09-30 15:08:28 +02:00
masterdraco ba2a3e94f1 feat(status): statusvindue på skærmen slås til med en tast 2026-09-30 14:52:35 +02:00
masterdraco 131b1ad10c fix(kæde): stald-mangel forklares rigtigt og beskeder nævner kæden
Køb af en rest efter gårdens stalde giver nu spu_chain_problem_barn (køb resten
eller flere dyr) i stedet for 'byg en fabrik'. Engangsbeskeden får kædens navn
foran, så to kæder der mangler samme vare kan skelnes.
2026-09-30 14:42:53 +02:00
masterdraco 19a10ab67c feat(kæde): manglende led forklares i Kræver handling, fanen og som besked
problemText giver årsag og råd for køb/flaskehals/kredsløb og 'slukket af dig'
(userOff-kopi på ADChain.last); modellens problemer får need. Fanen Kæder viser
teksten som rød tekstrække under varen (foldet ud). Kræver handling får én rød
post pr. kæde (højst 2, score 3500+alvor mellem rød tom og rød fuld); klik åbner
fanen med kæden foldet ud. Engangsbesked pr. nyt led (rod|vare|art) kun fra
tick, ikke i opstartsfristen eller på delvise data/ufærdig markscanning.
Manualer og CHANGELOG opdateret.
2026-09-30 14:40:29 +02:00
masterdraco 1e7b2ec699 docs(kæde): tydelig advarsel om tændte produktioner og manualen opdateret efter slutreview-rettelserne 2026-09-30 14:28:43 +02:00
masterdraco 817fc388a2 fix(kæde): palle-varer får egen status og linjen med frie marker passer i fanen 2026-09-30 14:27:48 +02:00
masterdraco 83394aabfd perf(kæde): priser slås kun op for kandidatmarker, fanen genbruger resultater under 30 s og kæden måles af profileren 2026-09-30 14:27:08 +02:00
masterdraco 9e34a16dfa fix(kæde): højst 3 forslag pr. afgrøde og ét samlet kædepunkt i Kræver handling 2026-09-30 14:26:09 +02:00
masterdraco d2453f78fb fix(kæde): ingen markskift eller forslag før markscanningen har set alle marker 2026-09-30 14:25:14 +02:00
masterdraco 605c4e8d45 fix(kæde): spillerens slukninger huskes, pause beskytter kun sin egen kæde, stoppede flows slippes og forældede nøgler glemmes 2026-09-30 14:24:16 +02:00
masterdraco 9c9c2badeb fix(kæde): spillerens tændte produktioner leverer til andre rødder med fuld kapacitet 2026-09-30 14:21:23 +02:00
masterdraco e394a77833 docs(kæde): produktionskæder i manual, changelog og README (v1.61.0.0) 2026-09-30 14:10:18 +02:00
masterdraco b77bda5334 fix(kæde): pausede kæder kan genoptages, træet folder ud og teksten passer i cellerne 2026-09-30 14:07:37 +02:00
masterdraco f0b8df8ae3 feat(kæde): fanen Kæder og forslag i Kræver handling 2026-09-30 13:59:25 +02:00
masterdraco b389fa3564 fix(kæde): kæden tager aldrig spillerens traktorer, respekterer slukkede produktioner og handler ikke på delvise data 2026-09-30 13:52:15 +02:00
masterdraco 32f2886a55 feat(kæde): kæden kører — skifter frie marker, tænder mellemled, opretter flows 2026-09-30 13:46:01 +02:00
masterdraco 2a7769d090 feat(kæde): læse-adapter for produktioner, stalde, marker og udbytte 2026-09-30 13:37:58 +02:00
masterdraco 49eed51202 feat(kæde): flows kan tilhøre en kæde 2026-09-30 13:34:09 +02:00
masterdraco 6dd1b5cac0 feat(kæde): marker kan markeres fri til kæden; målafgrødens kilde huskes 2026-09-30 13:32:48 +02:00
masterdraco 217498aefa feat(kæde): mellemled og manglende flows 2026-09-30 13:30:54 +02:00
masterdraco 2e7a7105c1 feat(kæde): afgrødeplan vælger marker til manglende afgrøder 2026-09-30 13:28:57 +02:00
masterdraco 27017c0f62 fix(kæde): opbrugt fælles kapacitet meldes som flaskehals 2026-09-30 13:26:51 +02:00
masterdraco f813d4b3d8 feat(kæde): kædemodel sporer produkter baglæns til råvarer 2026-09-30 13:24:04 +02:00
masterdraco 4f676135ac fix(flow-vogne): husk kun redskaber med lastrum som flow-vogn (v1.60.21.0)
Sell Slurry 2 havde ploven husket som gyllevogn. Manualerne: fast sæt/bundet
omfatter nu også flowets vogne.
2026-09-30 12:34:39 +02:00
masterdraco 2a36de0837 docs: engelsk README-afsnit med install-trin 3 og Development 2026-09-30 11:53:12 +02:00
masterdraco c415bdc7a4 fix(marker): vinhøster — lille CP-bredde erstattes af 3 m (v1.60.20.0) 2026-09-29 23:50:34 +02:00
masterdraco a6506fea4e fix(marker): vinhøster uden bredde får vinrækkens 3 m (v1.60.19.0) 2026-09-29 23:40:08 +02:00
masterdraco 8abcbcdcb8 fix(marker): vinhøsterens afgrøde fra inputFruitTypeIndex (FS25) (v1.60.18.0) 2026-09-29 23:34:38 +02:00
masterdraco 3c67591cb3 fix(marker): vinhøstere genkendes via spec_vineCutter (v1.60.17.0) 2026-09-29 23:26:31 +02:00
masterdraco 784144cd4e fix(aflæsning): tipvogn skiftes til en tip-side AD må aflæsse fra
EDK 650 Unrealistic har kun allowAIDischarge på bagudtip; på venstre/højre/
kornluge åbnede AD aflæsningen, men intet løb ud (Chicken Food 2 786.712 l
hvede ved Farma med plads; Disponibel Vogn 3 fuld af løg). CP-genstart logger
kun ved ændring (v1.60.16.0).
2026-09-29 23:23:20 +02:00
masterdraco 13e5dbf188 fix(marker): humle (HOPS) er vinafgrøde — aldrig slåafgrøde (v1.60.15.0) 2026-09-29 23:12:16 +02:00
62 changed files with 5847 additions and 82 deletions
+39
View File
@@ -6,8 +6,47 @@ står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUA
--- ---
## 1.62 — Kæder slås selv til
- **1.62.7** Nyt: konsolkommandoen `spuSellBlock` slår salgssteder fra og til (pr. savegame). Et salgssted, der er slået fra, bruges hverken til udkørsel eller i priserne. Til stationer, hvor aflæsningen går i stå, fx "Market Price Liquid Fill Station".
- **1.62.6** Pålæsningsdiagnosen logger kun, når en trigger med varen er inden for 20 m (køretøjer der holder ved Wait-punkter eller marker gav falske meldinger).
- Manualerne understreger nu, at parkering og hentning af redskaber kun virker ved at bakke: ruten til `P <navn>` og `Connect P <navn>` skal slutte som en bakrute.
- **1.62.5** Diagnose: står AutoDrive 30 s ved et LOAD-mål uden at fylde, skriver loggen én linje med markør, vare og for hver vogn den nærmeste AD-læssetrigger (afstand, har varen, står vognen i triggerens boks). Ingen indgreb.
- **1.62.4** Rettet: påfyldning af et langt sæt fik AutoDrive til at oprette ekstra markører med stationens navn (fx "Buying Station Multifruit"). Det gør AutoDrives `getMarkerAtStation`, når ingen markør passer til togets længde. Moden kalder den ikke længere; markøren findes ved stationens læssetriggere eller ved bygningen.
- **1.62.3** Rettet: påfyldning ved købestation valgte en anden stations markør for lange sæt. AutoDrive vil have markøren togets længde + 3–13 m forbi triggeren, så Puma + Kinze 4900 ventede på frø ved "Market Price Liquid Fill Station" i stedet for "Buying Station Multifruit". Nu vælges den nærmeste markør ved stationens egne læssetriggere; P-, Connect- og Wait-markører springes over. Loggen skriver, hvor et tomt redskab fyldes.
- **1.62.2** Kæderne læser fabrikkernes udgange (Sælg / Fordel / Behold). **Fordel:** spillet leverer selv til gårdens fabrikker, så kæden opretter intet flow, og fanen Kæder viser *fordeles af spillet*. **Behold** (spawn eller lager): et flow henter varen ved fabrikken som hidtil. **Sælg på en mellemvare** betyder, at næste led i kæden intet får, så kæden sætter den til Behold og henter den.
- **1.62.1** Rettet: udkørsel fra objektlager (fx Sell Egg fra Æglageret) stoppede halvfuld med "spawner ikke … (30 s uden fremdrift)". Lageret deler samme vare i flere grupper (paller med forskellig fyldning), og moden talte kun den første gruppe — når en lille gruppe blev tom, så lageret ud til at vokse. Nu tælles alle grupper. Advarslen viser lager- og vogntal, hvis den alligevel opstår.
- **1.62.0** **En kæde er noget du selv slår til:** tændte produktioner er ikke længere automatisk kæder (FS25 tænder nye produktioner som standard — 7 ensilagetårne gav 56 kæder og fik AIO-fabrikkens høproduktion tændt). Fanen **Kæder** har nederst *N tændte produktioner uden kæde* (foldet sammen); **[Kæde til]** gør en produktion til en kæde. En pauset kæde har **[Fjern]**. Ikke-valgte produktioner bruges stadig som kilde. Når en kæde fjernes, slettes dens tomme kædeflows (uden traktor), og mellemled kæden selv tændte slukkes igen. Rettet: kædens flowoversigt manglede traktorerne, så sammenlægning/oprydning kunne ramme et kædeflow med traktor. Udkørsel fra palleskur holder AutoDrive, mens moden tager paller ud.
## 1.61 — Produktionskæder
- **1.61.13** Udkørsel fra palleskur: mens moden tager paller ud (én ad gangen), holdes AutoDrives egen pålæsning, så vognen ikke kører halvfuld, når triggeren et øjeblik er tom. Pålæsningen slutter, når vognen er fuld, lageret er tomt, eller skuret ikke har udleveret noget i 30 s.
- **1.61.12** Oversigt-tavlen folder også kædeflows uden traktor sammen og viser navnene uden "Kæde: "; et kædeflow uden traktor har status *mangler traktor/vogn* (tavle og Flows-liste).
- **1.61.11** Markørvalg ved bygninger: når to markører står inden for 20 m af hinanden, afgør navnet — pålæsning foretrækker *Pickup/Hent/Læs/Load*, aflæsning *Unload/Drop/Aflæs*. Før vandt den nærmeste (Sell Egg kørte til "Chicken Storage Unload" i stedet for "Egg Storage Pickup").
- **1.61.10** Markflådens puljeliste viser korte maskinnavne (uden " Unrealistic" og parenteser som "(+ Custom Crops)"), så lange linjer ikke ombrydes med traktorens nummer alene på næste linje.
- **1.61.9** *Sælg fra* i udkørselsflows har valget **Altid (uanset pris)** øverst: flowet sælger, så snart der er noget at køre med, uden at vente på en god pris. Resumé og status viser *sælg altid*.
- **1.61.8** Kæderne regner **læs i vogne og høstertanke** med som lager (afgrøder og markvarer), og der kommer ingen forslag eller automatiske markskift for en afgrøde, mens den høstes. Klik på en varelinje i fanen **Kæder** åbner flowet, der kører varen, i redigeringsvinduet (så en stoppet kæde kan få traktor og vogn).
- **1.61.7** Kædelinjen viser *Gård* (uden klik) ved slutprodukter, gården selv bruger (fx ensilage, TMR, roesnitter), og *Sælg*/*Behold* kun ved varer, der faktisk kan sælges straks.
- **1.61.6** Slutprodukter sælges kun straks, hvis gården ikke selv bruger varen (stald, tændt produktion, forbrug eller flow). Varer kæden allerede havde sat til salg, sættes automatisk tilbage til *Behold*, når de ikke længere må sælges (in-game: TMR og roesnitter blev sat til salg).
- **1.61.5** Redskaber bredere end Courseplays 50 m (Unrealistic kan sættes til op til 120 m) afvises ikke længere: de bruges, og kursusbredden sættes til 50 m, før Courseplay laver kurset. Markflåden viser *kursus 50 m* ved dem.
- **1.61.4** Kæder mere realistiske: **ét kædeflow pr. bygning** med alle bygningens manglende varer (før ét pr. vare og bygning); nye varer lægges i bygningens eksisterende kædeflow, og tidligere tomme kædeflows (uden traktor) til samme bygning lægges sammen. **Slutprodukter sælges straks** som standard (fabrikkens udgang sættes til *Sælg direkte*); feltet *Sælg*/*Behold* på kædelinjen skifter, og *Behold* sætter varer, kæden selv satte til salg, tilbage. I Flows-listen foldes kædeflows uden traktor sammen i én linje. En såmaskine med gødningstank (fx Kinze 4905) bruges kun til såning, ikke som gødningsspreder.
- **1.61.3** Fanen **Kæder** sorteret: forslag til ny afgrøde står samlet øverst, én gang pr. mark og efter marknummer (før gentaget under hver kæde, der bruger afgrøden); kæderne står efter status (mangel/fejl, venter, dækket), så bygning og produkt. Kædelinjen viser stadig "· N forslag".
- **1.61.2** Flows-listen: dine egne flows står øverst; kædernes auto-oprettede flows samles nederst under *Kædernes egne flows* og vises uden "Kæde: ", så bygningen kan ses.
- **1.61.1** Fanen **Kæder** står nu efter **Marker**, og navne i kædelisten afkortes ikke længere (fx "+ Fermented Chaff", "+ Whole Crop Silage").
- **1.61.0** Produktionskæder: slå et produkt til i en fabrik, og moden holder det kørende helt tilbage til marken. Kæden regner baglæns gennem dine egne fabrikker og stalde til afgrøderne og ser 12 måneder frem. Ny fane **Kæder** med træ pr. rod-produktion, farvet status, fold ud/ind og **[Pause]**/**[Start]**. Marker markeret **Fri til kæden** skifter afgrøde automatisk (højst 2 i minuttet); andre marker får forslag med **[Godkend]**/**[Afvis]** (højst 3 pr. afgrøde) i fanen og som ét samlet punkt i **Kræver handling**. En afgrøde, du selv har valgt, ændres aldrig. Mellemled tændes og slukkes kun igen, hvis kæden tændte dem. Flows **Kæde: <vare> → <bygning>** oprettes med en ledig pool-traktor og tipvogn (ellers stoppet: *mangler traktor/vogn*; paller altid stoppet) og stoppes, ikke slettes, når kæden slås fra. Dine egne tændte produktioner bruges som kilde først; slukker du en produktion, tænder kæden den ikke igen, før du tænder en ny. Stoppede kædeflows slippes af kæden. Ingen handlinger de første 2 minutter, ingen markskift før alle marker er scannet, og intet slukkes ved delvise læsefejl. Begrænsninger: intet køb, ingen dyrekøb, flerårige afgrøder og græsmarker skiftes aldrig, grundudbytte uden gødningsbonus. **Vigtigt:** hver produktion, du tænder i en fabrik, prøver moden at forsyne — tænd kun det, du vil have produceret; sluk produktionen eller tryk [Pause] for at stoppe kæden. Manglende led forklares i klart sprog med råd (*Ingen af dine fabrikker laver Mel — byg en eller køb*, *Mølle kan kun lave 800 af 1.0k/md*, *Dine stalde leverer ikke nok Mælk …*, *… er slukket af dig*, *Kredsløb omkring …*): som rød tekst under varen i fanen Kæder, som én samlet rød linje i **Kræver handling** (*Kæder: 3 manglende led*, under lagerets røde linjer; klik åbner fanen Kæder med kæderne foldet ud) og som engangsbesked med kædens navn foran, når et nyt led mangler — højst 3 ad gangen, resten samlet i *+N manglende led — se fanen Kæder* (ikke i opstartsfristen eller mens markerne scannes). Markvarer, der ikke er en afgrødes hovedvare (hø, tørret lucerne/kløver, hakkelse, helsæd, halm …), står med kilden *fra marker* uden afgrødeplan eller advarsel, så fermenteringssiloer ikke giver falske *byg en eller køb*. Nyt **statusvindue** på skærmen (i stil med Simple Inspector), som slås til/fra med **numpad \*** (Insert bruges af Universal Autoload; kan ændres under Styring; virker til fods og i køretøjer): øverst til højre under info-boksen vises flow-traktorernes handling og fyld, markarbejde i gang, kæder med manglende led og advarsler (slukkede nødvendige fabrikker, køretøjer der har stået stille over 2 min — solgte vises ikke —, ventende kædeforslag); traktorlinjernes farve følger flowets niveau som Flows-listen — højst 18 linjer, opdateres hvert sekund, skjules ved menu/dialog, skjult HUD og søvn; tændt/slukket gemmes pr. bruger i `modSettings/FS25_ADSmartPickup/status.xml`. **Advarsel når en nødvendig fabrik slukkes** (også uden kæder): slukker du en produktion, hvis vare et tændt flow kører (valgt vare, eller målet tager den i auto og gården forbruger den), en stald æder eller en aktiv kæde bruger sammen med et flow/en stald, kommer én besked pr. slukning (*Advarsel: du slukkede Fermenteringssilo 3 (Ensilage) — Cow Food, Goat Food +1 bruger det. Lageret rækker ca. 12 dage.*) og en rød linje *Slukket men nødvendig: …* i **Kræver handling** (højst én, flere tælles som *+k*; klik åbner fanen Fabrikker) og i statusvinduet, så længe den er slukket og nødvendig. Ingen advarsel når en anden opskrift i samme fabrik laver varen og er tændt, en anden tændt produktion dækker med nok kapacitet, kun kæder bruger varen (meldes som manglende led), lageret rækker over 60 dage, kæden selv slukkede den, eller den allerede var slukket ved indlæsning; produktioner ingen bruger, advarer aldrig. Manual og ordliste opdateret.
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden ## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
- **1.60.21** Flow-vogne: kun et redskab med lastrum (ikke plov, kultivator eller såmaskine) huskes som flowets vogn. Før kunne en plov, der hang bag en flow-traktor, blive gemt som gyllevogn, og gyllen blev aldrig kørt.
- **1.60.20** Vinhøsterens bredde: også en lille bredde fra Courseplay (under 1,5 m) erstattes af vinrækkens 3 m.
- **1.60.19** Vinhøster uden kendt arbejdsbredde får vinrækkens 3 m (før "bredde ukendt", og marken blev ikke startet).
- **1.60.18** Vinhøsterens afgrøde læses fra `inputFruitTypeIndex` (feltnavnet i FS25).
- **1.60.17** Vinhøstere (fx Grégoire GL86) genkendes som høstere af den afgrøde, de er konfigureret til
(spec_vineCutter). Før fik vinmarker "ingen kan" (in-game: F22 hvide druer).
- **1.60.16** Tipvogne skiftes under AD-aflæsning til en tip-side, AutoDrive må aflæsse fra (`allowAIDischarge`).
Farmtech EDK 650 (Unrealistic) tillader kun bagudtip; stod den på venstre, højre eller kornluge, åbnede AD
aflæsningen, men intet løb ud, og vognen holdt med fuldt læs. CP-genstartslisten logges kun, når den ændrer sig.
- **1.60.15** Humle (HOPS, Dakota) behandles som vinafgrøde: aldrig slåafgrøde, og markmaskiner kører på rækkerne som ved druer.
- **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står - **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står
med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway". med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway".
- **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets - **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets
+44 -2
View File
@@ -251,12 +251,43 @@ local function markerCandidates(adEnv, nodes)
return found return found
end end
-- Markørnavnets rolle: "unload" (unload/drop/aflæs/aflever/indkast/tip) eller "load" (pickup/hent/læs/load) —
-- aflæsning tjekkes først ("unload" og "aflæs" indeholder "load"/"læs"). nil = neutralt navn.
local UNLOAD_WORDS = {"unload", "drop", "aflæs", "aflev", "indkast", "tip"}
local LOAD_WORDS = {"pickup", "pick up", "hent", "læs", "load"}
function ADBuildings.nameRole(name)
local lower = tostring(name or ""):lower()
for _, word in ipairs(UNLOAD_WORDS) do if lower:find(word, 1, true) then return "unload" end end
for _, word in ipairs(LOAD_WORDS) do if lower:find(word, 1, true) then return "load" end end
return nil
end
-- Når to markører står tæt (inden for NAME_WINDOW_M af den nærmeste), afgør navnet: den første med den ønskede rolle
-- flyttes forrest. Ellers urørt (nærmeste først). (in-game 30/9: Sell Egg tog "Chicken Storage Unload" 17 m frem for
-- "Egg Storage Pickup" 27 m ved Æglageret)
ADBuildings.NAME_WINDOW_M = 20
function ADBuildings.preferByName(candidates, wantRole)
local list = candidates or {}
if #list < 2 then return list end
local limit = (list[1].distance or 0) + ADBuildings.NAME_WINDOW_M
if ADBuildings.nameRole(list[1].name) == wantRole then return list end
for index, candidate in ipairs(list) do
if (candidate.distance or math.huge) > limit then break end
if ADBuildings.nameRole(candidate.name) == wantRole then
local reordered = {candidate}
for other, item in ipairs(list) do if other ~= index then table.insert(reordered, item) end end
return reordered
end
end
return list
end
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør — -- 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å -- 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", -- 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) -- 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) function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
for _, candidate in ipairs(candidates or {}) do for _, candidate in ipairs(ADBuildings.preferByName(candidates, "load")) do
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
if not isUnloads then if not isUnloads then
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)} return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
@@ -314,7 +345,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
local x, z = getNodePosition(node) local x, z = getNodePosition(node)
if x ~= nil then table.insert(positions, {x = x, z = z}) end if x ~= nil then table.insert(positions, {x = x, z = z}) end
end end
local best, bestOther = nil, nil local best, bestOther, nearbyAll = nil, nil, {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
if not ADSupplyPlanner.isWaitMarker(marker) then if not ADSupplyPlanner.isWaitMarker(marker) then
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id) local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
@@ -323,6 +354,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z
local distance = math.sqrt(dx * dx + dz * dz) local distance = math.sqrt(dx * dx + dz * dz)
if distance < ADBuildings.MAX_MARKER_DISTANCE then if distance < ADBuildings.MAX_MARKER_DISTANCE then
table.insert(nearbyAll, {marker = marker, distance = distance})
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
bestOther = {marker = marker, distance = distance} bestOther = {marker = marker, distance = distance}
@@ -338,6 +370,16 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
chosen = bestOther chosen = bestOther
end end
-- navnet afgør mellem tætte markører: en "Unload/Drop"-markør frem for en "Pickup"-markør (in-game 30/9)
table.sort(nearbyAll, function(a, b) return a.distance < b.distance end)
if chosen ~= nil and ADBuildings.nameRole(chosen.marker.name) == "load" then
for _, entry in ipairs(nearbyAll) do
if entry.distance <= chosen.distance + ADBuildings.NAME_WINDOW_M and ADBuildings.nameRole(entry.marker.name) == "unload" then
chosen = entry
break
end
end
end
if chosen == nil then if chosen == nil then
return nil return nil
end end
+643
View File
@@ -0,0 +1,643 @@
-- AD Chain (kæde-kørsel)
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
-- Spiladgang kun inde i funktioner (filen dofile's i test).
ADChain = {}
ADChain.FILE_NAME = "ADSmartPickup_chain.xml"
ADChain.ROOT = "ADSmartPickupChain"
ADChain.TICK_MS = 60000
ADChain.STARTUP_GRACE_MS = 120000
ADChain.MAX_FIELD_SWITCHES = 2
ADChain.MAX_NEW_FLOWS = 2
ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
ADChain.DISMISS_CHANGE = 0.25
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
ADChain.last = nil
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste tick — til at se spillerens slukninger
ADChain.prevEnabled = nil
-- true fra indlæsning til første tick: kædens produktioner der er slukket, glemmes (spillet blev ikke gemt)
ADChain.afterLoad = false
-- run-id -> "noVehicle"|"pallet" (true = "noVehicle"): flows kæden oprettede stoppet, fordi der ikke var
-- traktor/vogn eller varen er på paller (kun i hukommelsen)
ADChain.noVehicle = {}
ADChain.switchCount = 0
ADChain.now = 0
ADChain.timer = 0
ADChain.collectFailed = false
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
ADChain.problemKeys = {}
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
ADChain.depPrevEnabled = nil
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
return false
end
local function sortedKeys(map)
local keys = {}
for key in pairs(map or {}) do table.insert(keys, key) end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
return keys
end
-- ---- rene hjælpere ----
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
function ADChain.newState()
-- finalKeep = {[rodnøgle] = true}: slutproduktet beholdes (standard: sælg straks); soldByChain = {["rod|vare"] = true}
-- optIn = {[opskriftsnøgle] = true}: produktioner spilleren selv har slået til som kæde (tændte produktioner er
-- ellers ikke kæder — FS25 tænder dem som standard)
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil, finalKeep = {}, soldByChain = {}, optIn = {}}
end
function ADChain.stateToRecords(state)
local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {},
roots = state.roots ~= nil and sortedKeys(state.roots) or nil,
finalKeep = sortedKeys(state.finalKeep or {}), soldByChain = sortedKeys(state.soldByChain or {}), optIn = sortedKeys(state.optIn or {})}
local ids = {}
for id in pairs(state.dismissed or {}) do table.insert(ids, id) end
table.sort(ids)
for _, id in ipairs(ids) do
local entry = state.dismissed[id]
table.insert(records.dismissed, {id = id, fruit = entry.fruit, missingHa = entry.missingHa})
end
return records
end
function ADChain.stateFromRecords(records)
local state = ADChain.newState()
if records == nil then return state end
for _, key in ipairs(records.byChain or {}) do state.byChain[key] = true end
for _, key in ipairs(records.paused or {}) do state.paused[key] = true end
for _, key in ipairs(records.userOff or {}) do state.userOff[key] = true end
for _, key in ipairs(records.finalKeep or {}) do state.finalKeep[key] = true end
for _, key in ipairs(records.soldByChain or {}) do state.soldByChain[key] = true end
for _, key in ipairs(records.optIn or {}) do state.optIn[key] = true end
if records.roots ~= nil then
state.roots = {}
for _, key in ipairs(records.roots) do state.roots[key] = true end
end
for _, entry in ipairs(records.dismissed or {}) do
local id = tonumber(entry.id)
if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then
state.dismissed[id] = {fruit = entry.fruit, missingHa = tonumber(entry.missingHa) or 0}
end
end
return state
end
-- Afviste forslag beholdes kun mens manglen for afgrøden findes og har ændret sig højst 25 %.
function ADChain.pruneDismissed(dismissed, shortages)
local byFruit = {}
for _, shortage in ipairs(shortages or {}) do byFruit[shortage.fruit] = shortage.missingHa end
local kept = {}
for id, entry in pairs(dismissed or {}) do
local now = byFruit[entry.fruit]
local before = entry.missingHa or 0
if now ~= nil and math.abs(now - before) <= math.abs(before) * ADChain.DISMISS_CHANGE then
kept[id] = {fruit = entry.fruit, missingHa = before}
end
end
return kept
end
-- {[mark] = {fruit, missingHa}} -> {[mark] = afgrøde} (ADCropPlan.choose's dismissed)
function ADChain.dismissedFruits(dismissed)
local result = {}
for id, entry in pairs(dismissed or {}) do result[id] = entry.fruit end
return result
end
function ADChain.unionFruits(usedFruits, fieldDemand)
local result = {}
for fruit in pairs(usedFruits or {}) do result[fruit] = true end
for fruit in pairs(fieldDemand or {}) do result[fruit] = true end
return result
end
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
-- optIn = {[nøgle] = true}: kun de produktioner er rødder (andre tændte markeres notChosen: kører for sig selv som
-- kilde, men kæden skaber intet for dem). optIn = nil = alle tændte er rødder (som før).
function ADChain.modelRecipes(recipes, paused, userOff, optIn)
local result = {}
for index, recipe in ipairs(recipes or {}) do
local copy = {}
for key, value in pairs(recipe) do copy[key] = value end
if (paused or {})[recipe.key] then copy.paused = true end
if optIn ~= nil and recipe.enabled and not recipe.byChain and not optIn[recipe.key] then copy.notChosen = true end
if (userOff or {})[recipe.key] and not recipe.enabled then copy.cyclesPerMonth = 0 end
result[index] = copy
end
return result
end
local function copySet(map)
local result = {}
for key, value in pairs(map or {}) do result[key] = value end
return result
end
-- Tændte produktioner der ikke er kædens (= rødderne, inkl. pausede).
function ADChain.enabledKeys(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
end
return result
end
-- Spillerens valg og forældede nøgler, før kæden regner (rent: state røres ikke).
-- opts = {prevEnabled, isPartial, firstEval}. -> {userOff, byChain, paused, roots, added, forgotten, droppedPaused, cleared}
-- * kædens produktion er slukket: af spilleren -> userOff (added); første tick efter indlæsning -> glemt (forgotten)
-- * en produktion (ikke kædens) der var tændt sidste tick, er slukket -> userOff (added)
-- * userOff ryddes kun når der kommer en ny rod (spilleren tænder noget) — ikke når noget "ikke længere bruges"
-- (det gav tænd/sluk-løkker); nye slukninger i samme tick beholdes
-- * pausede nøgler for produktioner der er væk eller slukket, droppes (droppedPaused)
-- Delvise data: userOff ryddes ikke, rødderne opdateres ikke og pausede droppes ikke.
function ADChain.reconcile(state, recipes, opts)
opts = opts or {}
local roots = ADChain.enabledKeys(recipes)
local rootAdded = false
if state.roots ~= nil then
for key in pairs(roots) do if not state.roots[key] then rootAdded = true end end
end
local result = {userOff = copySet(state.userOff), byChain = copySet(state.byChain), paused = copySet(state.paused),
roots = opts.isPartial and state.roots or roots, added = {}, forgotten = {}, droppedPaused = {}, cleared = false}
if rootAdded and not opts.isPartial and next(result.userOff) ~= nil then
result.userOff, result.cleared = {}, true
end
local present = {}
for _, recipe in ipairs(recipes or {}) do
present[recipe.key] = recipe
if not recipe.enabled and recipe.byChain then
result.byChain[recipe.key] = nil
if opts.firstEval then
table.insert(result.forgotten, recipe.key)
elseif not result.userOff[recipe.key] then
result.userOff[recipe.key] = true
table.insert(result.added, recipe.key)
end
elseif not recipe.enabled and (opts.prevEnabled or {})[recipe.key] and not result.userOff[recipe.key] then
result.userOff[recipe.key] = true
table.insert(result.added, recipe.key)
end
end
if not opts.isPartial then
for key in pairs(state.paused or {}) do
if present[key] == nil or not present[key].enabled then
result.paused[key] = nil
table.insert(result.droppedPaused, key)
end
end
end
table.sort(result.added); table.sort(result.forgotten); table.sort(result.droppedPaused)
return result
end
-- Rødder hvis flows ikke må stoppes: kædens aktuelle rødder + pausede rødder (pause rører intet).
function ADChain.activeRoots(chain, paused)
local result = {}
for _, root in ipairs((chain or {}).roots or {}) do result[root.key] = true end
for key in pairs(paused or {}) do result[key] = true end
return result
end
-- Mest brugte Wait-pulje blandt forsyningsflows til bygningen, ellers "".
function ADChain.waitPoolFor(runs, building)
local counts, best, bestCount = {}, "", 0
for _, run in ipairs(runs or {}) do
local group = run.waitPoolGroup
if run.kind == "supply" and group ~= nil and group ~= "" and contains(run.targetBuildingIds, building) then
counts[group] = (counts[group] or 0) + 1
end
end
for _, group in ipairs(sortedKeys(counts)) do
if counts[group] > bestCount then best, bestCount = group, counts[group] end
end
return best
end
function ADChain.flowName(fillTitle, buildingName)
return "Kæde: " .. tostring(fillTitle) .. " → " .. tostring(buildingName)
end
local function covers(run, edge)
return contains(run.targetBuildingIds, edge.consumer)
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType))
end
-- Bruger en af de aktuelle kanter stadig flowet? -> kantens rodnøgle (flowet flyttes dertil), ellers nil.
function ADChain.stillNeeded(runView, edges)
for _, edge in ipairs(edges or {}) do
if covers(runView, edge) then return edge.rootKey end
end
return nil
end
-- {[forbruger|vare] = "ok"|"auto"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
function ADChain.flowStatus(edges, runs, noVehicle)
local result = {}
for _, edge in ipairs(edges or {}) do
-- udgangen står på Fordel: spillet leverer selv, intet flow
local status = edge.delivery == "auto" and "auto" or "missing"
for _, run in ipairs(edge.delivery == "auto" and {} or runs or {}) do
if run.kind == "supply" and covers(run, edge) then
if not run.stopped then status = "ok"; break end
local reason = (noVehicle or {})[run.id]
if reason == "pallet" then status = "pallet"
elseif reason then status = "noVehicle"
elseif status == "missing" then status = "stopped" end
end
end
result[tostring(edge.consumer) .. "|" .. tostring(edge.fillType)] = status
end
return result
end
-- Flows i det format ADChainActions regner på.
function ADChain.runsView(runs)
local list = {}
for _, run in ipairs(runs or {}) do
table.insert(list, {id = run.id, name = run.name, kind = run.kind, targetBuildingIds = run.targetBuildingIds,
ingredientMode = run.ingredientMode, ingredients = run.ingredients, stopped = run.stopped == true,
chain = run.chain, waitPoolGroup = run.waitPoolGroup,
-- traktorerne: uden dem ligner alle kædeflows tomme (sammenlægning/sletning må kun ramme tomme)
vehicleIds = run.vehicleIds or {}})
end
return list
end
-- Manglende led der ikke var der sidst (nøgle = rodnøgle|vare|art). -> nye problemer (hver nøgle én gang), nøglerne nu.
-- Et problem der forsvinder, glemmes; kommer det igen, er det nyt. prevKeys røres ikke.
function ADChain.problemKey(problem)
return tostring(problem.rootKey) .. "|" .. tostring(problem.fillType) .. "|" .. tostring(problem.kind)
end
function ADChain.newProblems(prevKeys, problems)
local fresh, keys = {}, {}
for _, problem in ipairs(problems or {}) do
local key = ADChain.problemKey(problem)
if not keys[key] then
keys[key] = true
if not (prevKeys or {})[key] then table.insert(fresh, problem) end
end
end
return fresh, keys
end
-- ---- evaluering ----
local function farmId()
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
end
local function plan(data, state)
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff, state.optIn)
local chain = ADChainModel.build(recipes, data.husbandry, data.crops, data.fieldGoods)
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
local protected = {}
if next(state.paused) ~= nil then
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff, state.optIn), data.husbandry, data.crops, data.fieldGoods).needRecipes
end
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
local shortages, actions = {}, {}
if not data.fieldsIncomplete then
shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields)
state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages)
actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.priceOf or data.prices, ADChain.dismissedFruits(state.dismissed), data.harvesting)
end
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain, data.recipes)}
end
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
-- Spillerens valg og forældede nøgler ind i tilstanden (kun ved tick, før der regnes).
local function applyReconcile(state, data)
local result = ADChain.reconcile(state, data.recipes, {prevEnabled = ADChain.prevEnabled, isPartial = data.partial == true,
firstEval = ADChain.afterLoad})
if result.cleared then logInfo("ny produktion tændt — slukkede produktioner må bruges igen") end
for _, key in ipairs(result.added) do logInfo("spilleren slukkede %s — kæden tænder den ikke igen", key) end
for _, key in ipairs(result.forgotten) do logInfo("kæden glemmer %s (ikke tændt efter indlæsning)", key) end
for _, key in ipairs(result.droppedPaused) do logInfo("pause for %s fjernet: produktionen er væk eller slukket", key) end
state.userOff, state.byChain, state.paused, state.roots = result.userOff, result.byChain, result.paused, result.roots
ADChain.prevEnabled = ADChain.enabledKeys(data.recipes)
ADChain.afterLoad = false
end
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
-- mellemtiden meldes ved næste hele tjek).
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
local function dependencyCheck(data, plan, runs, act, previous)
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
if data.readFailed then
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
end
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
local enabling, recipes = {}, {}
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
for _, recipe in ipairs(data.recipes or {}) do
if not enabling[recipe.key] then table.insert(recipes, recipe) end
end
local prev = act and ADChain.depPrevEnabled or nil
local result = {
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
}
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
return result
end
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
function ADChain.evaluate(adEnv, act)
local state = ADChain.state
local ok, data = pcall(ADChainData.collect, adEnv, farmId(), state.byChain)
if not ok then
if not ADChain.collectFailed then Logging.warning(ADChain.LOG_PREFIX .. "gården kunne ikke læses: %s", tostring(data)) end
ADChain.collectFailed = true
return ADChain.last
end
ADChain.collectFailed = false
if act then
local okState, errState = pcall(applyReconcile, state, data)
if not okState then Logging.warning(ADChain.LOG_PREFIX .. "tilstanden kunne ikke opdateres: %s", tostring(errState)) end
end
local okPlan, result = pcall(plan, data, state)
if not okPlan then
Logging.warning(ADChain.LOG_PREFIX .. "kæden kunne ikke beregnes: %s", tostring(result))
return ADChain.last
end
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
local runs = ADChain.runsView(ADRunsController.store.runs)
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
if not okDep then
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
dependency = nil
end
local paused = {}
for key in pairs(state.paused) do paused[key] = true end
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
paused = paused, userOff = copySet(state.userOff), finalKeep = copySet(state.finalKeep or {}),
sellable = ADChain.sellableRoots(data, result, runs), candidates = ADChain.candidates(data.recipes, state), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
dependency = dependency}
return ADChain.last
end
-- Alder (ms, spillets ur) af det sidste resultat; math.huge når der intet er. Fanen genbruger et friskt resultat.
function ADChain.lastAge()
if ADChain.last == nil then return math.huge end
return ADChain.now - (ADChain.last.at or 0)
end
-- Engangsbesked pr. nyt manglende led (kun fra tick). Ikke i opstartsfristen og ikke mens gården er læst delvist
-- eller markscanningen kører (ellers falske alarmer lige efter indlæsning) — så huskes intet, og det meldes senere.
-- Højst MAX_PROBLEM_NOTES beskeder pr. tick; resten samles i én besked (alle logges stadig).
ADChain.MAX_PROBLEM_NOTES = 3
function ADChain.notifyProblems(last)
if last == nil or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
if last.fieldsIncomplete or (last.data ~= nil and last.data.partial) then return 0 end
local fresh, keys = ADChain.newProblems(ADChain.problemKeys, last.chain ~= nil and last.chain.problems or {})
ADChain.problemKeys = keys
if #fresh == 0 or ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
local fmt = SmartPickupFrame ~= nil and SmartPickupFrame.formatCompact or nil
local rootNames = {}
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do
rootNames[root.key] = tostring((last.titles or {})[root.name] or root.name)
end
for index, problem in ipairs(fresh) do
-- kædens navn foran, så to kæder der mangler samme vare kan skelnes
local text = tostring(rootNames[problem.rootKey] or problem.rootKey) .. ": " .. ADChainView.problemText(problem, last, textOf, nil, fmt)
Logging.info(ADChain.LOG_PREFIX .. "manglende led: %s", text)
if index <= ADChain.MAX_PROBLEM_NOTES then ADFieldJobs.notify(text, true) end
end
-- resten samles i én besked (fx mange fermenteringssiloer efter indlæsning); alle står i fanen Kæder
if #fresh > ADChain.MAX_PROBLEM_NOTES then
local ok, text = pcall(string.format, textOf("spu_chain_notifyMore"), #fresh - ADChain.MAX_PROBLEM_NOTES)
ADFieldJobs.notify(ok and text or ("+" .. tostring(#fresh - ADChain.MAX_PROBLEM_NOTES)), true)
end
return #fresh
end
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
function ADChain.notifyDependencies(last)
local dependency = last ~= nil and last.dependency or nil
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
local warnings = dependency.warnings
dependency.warnings = {}
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
for _, warning in ipairs(warnings) do
local text = ADDependency.notifyText(warning, textOf, titleOf)
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
ADFieldJobs.notify(text, true)
end
return #warnings
end
function ADChain.tick(adEnv, dt)
if adEnv == nil then return end
ADChain.now = ADChain.now + (dt or 0)
ADChain.timer = ADChain.timer + (dt or 0)
if ADChain.now < ADChain.STARTUP_GRACE_MS or ADChain.timer < ADChain.TICK_MS then return end
ADChain.timer = 0
local last = ADChain.evaluate(adEnv, true)
local ok, err = pcall(ADChain.notifyProblems, last)
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
end
-- ---- GUI-handlinger ----
local function suggestionFor(fieldId)
for _, action in ipairs(ADChain.last ~= nil and ADChain.last.actions or {}) do
if action.fieldId == fieldId then return action end
end
return nil
end
-- Godkend forslag: marken får kædens afgrøde som mål (kilde "chain").
function ADChain.approve(fieldId)
local action = suggestionFor(fieldId)
if action == nil then return false end
local ok, err = pcall(function()
-- et auto-skift må aldrig overskrive en afgrøde spilleren har sat siden; et forslag godkender spilleren selv
if action.mode == "auto" then
local plan = ADFieldJobs.getField(fieldId).plan or {}
if plan.targetSource == "user" then error("spilleren har selv valgt afgrøde") end
end
ADFieldJobs.setTarget(fieldId, action.to, "chain")
end)
if not ok then
Logging.warning(ADChain.LOG_PREFIX .. "F%s: forslag kunne ikke godkendes: %s", tostring(fieldId), tostring(err))
return false
end
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag godkendt: %s -> %s", tostring(fieldId), tostring(action.from), tostring(action.to))
return true
end
-- Afvis forslag: vises ikke igen, før manglen for afgrøden ændrer sig > 25 %.
function ADChain.dismiss(fieldId)
local action = suggestionFor(fieldId)
if action == nil then return false end
local missingHa = action.hectares or 0
for _, shortage in ipairs(ADChain.last.shortages or {}) do
if shortage.fruit == action.to then missingHa = shortage.missingHa end
end
ADChain.state.dismissed[fieldId] = {fruit = action.to, missingHa = missingHa}
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag %s afvist", tostring(fieldId), tostring(action.to))
return true
end
-- Tændte produktioner der ikke er valgt som kæde (og ikke pauset): {{key, name, buildingName}} til fanen Kæder.
function ADChain.candidates(recipes, state)
local list = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled and not recipe.byChain and not (state.optIn or {})[recipe.key] and not (state.paused or {})[recipe.key] then
table.insert(list, {key = recipe.key, name = recipe.name, buildingName = recipe.buildingName})
end
end
table.sort(list, function(a, b)
if tostring(a.buildingName) ~= tostring(b.buildingName) then return tostring(a.buildingName) < tostring(b.buildingName) end
return tostring(a.name) < tostring(b.name)
end)
return list
end
-- Slå en produktion til/fra som kæde (fra = også pausen ophæves, kæden slipper den ved næste tick).
function ADChain.setOptIn(recipeKey, on)
if recipeKey == nil then return end
ADChain.state.optIn[recipeKey] = on and true or nil
if not on then ADChain.state.paused[recipeKey] = nil end
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(recipeKey), on and "slået til som kæde" or "fjernet fra kæderne")
end
-- Rødder med et slutprodukt der kan sælges straks (gården bruger det ikke selv). -> {[rodnøgle] = true}
function ADChain.sellableRoots(data, result, runs)
local set = {}
local ok, sales = pcall(ADChainActions.finalSales, result.chain, data.recipes, result.edges, {}, ADChainActions.farmUses(data, runs))
for _, sale in ipairs(ok and sales or {}) do set[sale.key] = true end
return set
end
-- Slutprodukt for en kæde: keep = true -> "Behold", ellers "Sælg straks" (standard). Anvendes ved næste tick.
function ADChain.setFinalKeep(rootKey, keep)
if rootKey == nil then return end
ADChain.state.finalKeep[rootKey] = keep and true or nil
Logging.info(ADChain.LOG_PREFIX .. "%s: slutprodukt %s", tostring(rootKey), keep and "beholdes" or "sælges straks")
end
function ADChain.setPaused(rootKey, isPaused)
if rootKey == nil then return end
ADChain.state.paused[rootKey] = isPaused and true or nil
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(rootKey), isPaused and "på pause" or "genoptaget")
end
-- ---- gem / indlæs (samme sti-opslag som runs; skrives kun fra ADRunsController.save) ----
local function getPath()
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
if path == nil then return nil end
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADChain.FILE_NAME))
end
function ADChain.save()
local path = getPath()
if path == nil then return false end
local xmlFile = XMLFile.create("ADSmartPickupChain", path, ADChain.ROOT)
if xmlFile == nil then return false end
local records = ADChain.stateToRecords(ADChain.state)
for index, key in ipairs(records.byChain) do
xmlFile:setString(string.format("%s.byChain.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.paused) do
xmlFile:setString(string.format("%s.paused.root(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.userOff) do
xmlFile:setString(string.format("%s.userOff.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.finalKeep) do
xmlFile:setString(string.format("%s.finalKeep.root(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.soldByChain) do
xmlFile:setString(string.format("%s.soldByChain.output(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.optIn) do
xmlFile:setString(string.format("%s.optIn.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
if records.roots ~= nil then
xmlFile:setBool(ADChain.ROOT .. ".roots#known", true)
for index, key in ipairs(records.roots) do
xmlFile:setString(string.format("%s.roots.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
end
for index, entry in ipairs(records.dismissed) do
local key = string.format("%s.dismissed.field(%d)", ADChain.ROOT, index - 1)
xmlFile:setInt(key .. "#id", entry.id)
xmlFile:setString(key .. "#fruit", entry.fruit)
xmlFile:setString(key .. "#missingHa", string.format("%.2f", entry.missingHa or 0))
end
xmlFile:save()
xmlFile:delete()
return true
end
function ADChain.load()
ADChain.state = ADChain.newState()
ADChain.noVehicle = {}
ADChain.prevEnabled, ADChain.afterLoad = nil, true
ADChain.problemKeys = {}
ADChain.depPrevEnabled = nil
local path = getPath()
if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
if xmlFile == nil then return end
local records = {byChain = {}, paused = {}, userOff = {}, dismissed = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
xmlFile:iterate(ADChain.ROOT .. ".optIn.recipe", function(_, key)
table.insert(records.optIn, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".finalKeep.root", function(_, key)
table.insert(records.finalKeep, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".soldByChain.output", function(_, key)
table.insert(records.soldByChain, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".byChain.recipe", function(_, key)
table.insert(records.byChain, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".paused.root", function(_, key)
table.insert(records.paused, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".userOff.recipe", function(_, key)
table.insert(records.userOff, xmlFile:getString(key .. "#key"))
end)
if xmlFile:getBool(ADChain.ROOT .. ".roots#known", false) then
records.roots = {}
xmlFile:iterate(ADChain.ROOT .. ".roots.recipe", function(_, key)
table.insert(records.roots, xmlFile:getString(key .. "#key"))
end)
end
xmlFile:iterate(ADChain.ROOT .. ".dismissed.field", function(_, key)
table.insert(records.dismissed, {id = xmlFile:getInt(key .. "#id"), fruit = xmlFile:getString(key .. "#fruit"),
missingHa = tonumber(xmlFile:getString(key .. "#missingHa"))})
end)
xmlFile:delete()
ADChain.state = ADChain.stateFromRecords(records)
Logging.info(ADChain.LOG_PREFIX .. "%d produktioner tændt af kæden, %d pausede kæder indlæst", #records.byChain, #records.paused)
end
+230
View File
@@ -0,0 +1,230 @@
-- AD Chain Actions (ren logik): mellemled der skal tændes/slukkes, flows der mangler eller skal stoppes.
ADChainActions = {}
-- protected = {[nøgle] = cyklusser}: behov fra pausede rødder — slukkes ikke, men tændes heller ikke.
function ADChainActions.productions(recipes, needRecipes, protected)
local result = {enable = {}, disable = {}}
for _, recipe in ipairs(recipes or {}) do
local needed = ((needRecipes or {})[recipe.key] or 0) > 0
local kept = ((protected or {})[recipe.key] or 0) > 0
if needed and not recipe.enabled then table.insert(result.enable, recipe.key) end
if not needed and not kept and recipe.enabled and recipe.byChain then table.insert(result.disable, recipe.key) end
end
table.sort(result.enable); table.sort(result.disable)
return result
end
local SUPPLIED = {production = true, husbandry = true, field = true}
-- Hvordan varen når frem, ud fra producenternes udgang: "auto" = alle fordeler (spillet leverer selv til gårdens
-- fabrikker; kædens forbrugere er altid fabrikker) -> intet flow; "sold" = en producent sælger mellemvaren væk;
-- ellers "haul" (Behold: spawn/lager ved fabrikken, eller stald/mark) -> et flow henter. recipesByKey = {[nøgle] = opskrift}.
function ADChainActions.delivery(edge, recipesByKey)
local producers = edge.producers or {}
if #producers == 0 or recipesByKey == nil then return "haul" end
local all, sold = true, false
for _, key in ipairs(producers) do
local mode = ADChainActions.producerMode(recipesByKey, key, edge.fillType)
if mode == "sell" then sold = true end
if mode ~= "distribute" then all = false end
end
if sold then return "sold" end
return all and "auto" or "haul"
end
function ADChainActions.producerMode(recipesByKey, key, fillType)
local recipe = (recipesByKey or {})[key]
return recipe ~= nil and (recipe.outputModes or {})[fillType] or nil
end
local function producerKeys(node)
local keys = {}
for _, producer in ipairs(node.producers or {}) do table.insert(keys, producer.key) end
return keys
end
-- recipes (valgfri): gårdens opskrifter med outputModes -> kanten får delivery (se ADChainActions.delivery).
function ADChainActions.edges(chain, recipes)
local list, seen, byKey = {}, {}, nil
if recipes ~= nil then
byKey = {}
for _, recipe in ipairs(recipes) do byKey[recipe.key] = recipe end
end
local function walk(nodes, rootKey)
for _, node in ipairs(nodes or {}) do
local id = tostring(node.consumer) .. "|" .. tostring(node.fillType)
if SUPPLIED[node.source] and node.consumer ~= nil and not seen[id] then
seen[id] = true
local edge = {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey,
producers = producerKeys(node)}
edge.delivery = ADChainActions.delivery(edge, byKey)
edge.soldBy = {}
for _, key in ipairs(edge.producers) do
if ADChainActions.producerMode(byKey, key, edge.fillType) == "sell" then table.insert(edge.soldBy, key) end
end
table.insert(list, edge)
end
walk(node.children, rootKey)
end
end
for _, root in ipairs((chain or {}).roots or {}) do walk(root.children, root.key) end
return list
end
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
return false
end
function ADChainActions.isCovered(edge, runs)
for _, run in ipairs(runs or {}) do
if run.kind == "supply" and contains(run.targetBuildingIds, edge.consumer)
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType)) then
return true
end
end
return false
end
function ADChainActions.missingFlows(edges, runs)
local list = {}
for _, edge in ipairs(edges or {}) do
if edge.delivery ~= "auto" and not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
end
return list
end
-- Mellemvarer en producent sælger væk (kanten er "sold") -> {{key, fillType}} én gang hver; kæden sætter dem til Behold.
function ADChainActions.soldIntermediates(edges)
local list, seen = {}, {}
for _, edge in ipairs(edges or {}) do
if edge.delivery == "sold" then
for _, key in ipairs(edge.soldBy or {}) do
local id = key .. "|" .. edge.fillType
if not seen[id] then
seen[id] = true
table.insert(list, {key = key, fillType = edge.fillType})
end
end
end
end
return list
end
function ADChainActions.flowsToStop(runs, activeRootKeys)
local list = {}
for _, run in ipairs(runs or {}) do
-- også stoppede kædeflows uden traktor (de ryddes op); stoppede med traktor er allerede sluppet/spillerens
local idle = #(run.vehicleIds or {}) == 0
if run.chain ~= nil and run.chain ~= "" and (not run.stopped or idle) and not (activeRootKeys or {})[run.chain] then
table.insert(list, run)
end
end
return list
end
-- ---- ét kædeflow pr. bygning (in-game 30/9: ét flow pr. vare og bygning gav mange stoppede flows) ----
-- Manglende kanter samlet pr. modtagerbygning: {consumer, consumerName, rootKey, fillTypes} i fundet rækkefølge.
function ADChainActions.groupMissing(missing)
local groups, byConsumer = {}, {}
for _, edge in ipairs(missing or {}) do
local group = byConsumer[edge.consumer]
if group == nil then
group = {consumer = edge.consumer, consumerName = edge.consumerName, rootKey = edge.rootKey, fillTypes = {}}
byConsumer[edge.consumer] = group
table.insert(groups, group)
end
if not contains(group.fillTypes, edge.fillType) then table.insert(group.fillTypes, edge.fillType) end
end
return groups
end
local function isChainRun(run)
return run.chain ~= nil and run.chain ~= "" and run.kind == "supply" and run.ingredientMode == "manual"
end
-- Kædens eget flow (manuelle varer) til bygningen -> run-id, så nye varer lægges i det i stedet for et nyt flow.
function ADChainActions.chainRunFor(runs, consumer)
for _, run in ipairs(runs or {}) do
if isChainRun(run) and contains(run.targetBuildingIds, consumer) then return run.id end
end
return nil
end
-- Flere tomme kædeflows (uden traktorer) til samme bygning: det første beholdes med alle varerne, resten slettes.
-- -> {{keepId, add = {varer}, deleteIds = {id}}}
function ADChainActions.mergeIdle(runs)
local byConsumer, order = {}, {}
for _, run in ipairs(runs or {}) do
local target = (run.targetBuildingIds or {})[1]
if isChainRun(run) and target ~= nil and #(run.targetBuildingIds or {}) == 1 and #(run.vehicleIds or {}) == 0 then
if byConsumer[target] == nil then byConsumer[target] = {}; table.insert(order, target) end
table.insert(byConsumer[target], run)
end
end
local merges = {}
for _, target in ipairs(order) do
local list = byConsumer[target]
if #list > 1 then
local keep, add, deleteIds = list[1], {}, {}
for index = 2, #list do
for _, name in ipairs(list[index].ingredients or {}) do
if not contains(keep.ingredients, name) and not contains(add, name) then table.insert(add, name) end
end
table.insert(deleteIds, list[index].id)
end
table.insert(merges, {keepId = keep.id, add = add, deleteIds = deleteIds})
end
end
return merges
end
-- Varer gården selv bruger: stalde æder dem, en tændt produktion bruger dem, gården har forbrug af dem, eller et
-- flow (ikke udkørsel) kører dem. -> {[vare] = true}. Den slags sælges aldrig straks (in-game 30/9: TMR og roesnitter).
function ADChainActions.farmUses(data, runs)
local uses = {}
for name in pairs((data or {}).barnFood or {}) do uses[name] = true end
for name, rate in pairs((data or {}).rates or {}) do
if (rate.consumption or 0) > 0 then uses[name] = true end
end
for _, recipe in ipairs((data or {}).recipes or {}) do
if recipe.enabled then
for name in pairs(recipe.inputs or {}) do uses[name] = true end
end
end
for _, run in ipairs(runs or {}) do
if run.kind ~= "outbound" and run.ingredientMode == "manual" then
for _, name in ipairs(run.ingredients or {}) do uses[name] = true end
end
end
return uses
end
-- Slutprodukter der skal sælges straks: aktive rødders udgange, som ikke selv er en kant i kæden (mellemvare) og
-- som gården ikke selv bruger (farmUses). keep = {[rodnøgle] = true} for rødder sat til "Behold". -> {{key, fillType}}
function ADChainActions.finalSales(chain, recipes, edges, keep, farmUses)
local intermediate, byKey, list = {}, {}, {}
for _, edge in ipairs(edges or {}) do intermediate[edge.fillType] = true end
for _, recipe in ipairs(recipes or {}) do byKey[recipe.key] = recipe end
for _, root in ipairs((chain or {}).roots or {}) do
local recipe = byKey[root.key]
if recipe ~= nil and not (keep or {})[root.key] then
local names = {}
for name in pairs(recipe.outputs or {}) do table.insert(names, name) end
table.sort(names)
for _, name in ipairs(names) do
if not intermediate[name] and not (farmUses or {})[name] then table.insert(list, {key = root.key, fillType = name}) end
end
end
end
return list
end
-- Kædeflow for en kæde der er slået fra: uden traktor slettes det (auto-oprettet og tomt), ellers stoppes og slippes
-- det (bliver spillerens eget). (in-game 30/9: ellers 12 tomme "Kæde: …"-flows blandt spillerens egne)
function ADChainActions.releaseAction(run)
if #(run.vehicleIds or {}) == 0 then return "delete" end
return "release"
end
+511
View File
@@ -0,0 +1,511 @@
-- AD Chain Data (læse-adapter)
-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
ADChainData = {}
ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
ADChainData.warned = {}
-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
function ADChainData.isPermanent(fruitName)
if type(fruitName) ~= "string" or fruitName == "" then return false end
return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
end
function ADChainData.recipeKey(placeableUid, productionId)
return tostring(placeableUid) .. ":" .. tostring(productionId)
end
-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
function ADChainData.cyclesPerMonth(production)
return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
end
function ADChainData.warnOnce(key, fmt, ...)
if ADChainData.warned[key] then return end
ADChainData.warned[key] = true
if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
end
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
local function failed(data, key, fmt, ...)
data.partial, data.readFailed = true, true
ADChainData.warnOnce(key, fmt, ...)
end
-- ---- hjælpere ----
-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
local function nameResolver(data)
return function(index)
if index == nil or g_fillTypeManager == nil then return nil end
local name = g_fillTypeManager:getFillTypeNameByIndex(index)
if name == nil then return nil end
if data.fillIndex[name] == nil then
data.fillIndex[name] = index
local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
end
return name
end
end
-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
local function amountsByName(list, nameOf)
local result = {}
for _, entry in ipairs(list or {}) do
local name = nameOf(entry.type)
if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
end
return result
end
local function placeableId(placeable)
if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
end
-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
local function placeableName(placeable, fallback)
local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
if own ~= nil then return own end
if placeable.getName ~= nil then
local ok, name = pcall(placeable.getName, placeable)
if ok and name ~= nil then return tostring(name) end
end
return fallback
end
local function isEnabled(point, production)
if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
for _, active in ipairs(point.activeProductions or {}) do
if active == production then return true end
end
return false
end
-- ---- læsning pr. del ----
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
function ADChainData.outputModeName(mode)
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
return "keep"
end
local function outputModes(point, outputs, nameOf)
local modes = {}
if point.getOutputDistributionMode == nil then return modes end
for _, output in ipairs(outputs or {}) do
local name = output.type ~= nil and nameOf(output.type) or nil
if name ~= nil then
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
modes[name] = ADChainData.outputModeName(ok and mode or nil)
end
end
return modes
end
local function readFactory(placeable, point, byChain, nameOf)
local uid = placeableId(placeable)
if uid == nil then return {}, {} end
local buildingName = placeableName(placeable, uid)
local recipes, points = {}, {}
for _, production in ipairs(point.productions or {}) do
local key = ADChainData.recipeKey(uid, production.id)
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
outputModes = outputModes(point, production.outputs, nameOf)})
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
end
return recipes, points
end
local function readHerd(placeable, nameOf)
local uid = placeableId(placeable)
local outputs = {}
for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
local name = nameOf(index)
if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
end
if uid == nil or next(outputs) == nil then return nil end
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
end
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
local function readBarnFood(data, placeable, nameOf)
if ADSources.getHusbandryConsumption == nil then return end
local barnName
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
local name = nameOf(index)
if name ~= nil and (perDay or 0) > 0 then
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
data.barnFood[name] = data.barnFood[name] or {}
table.insert(data.barnFood[name], barnName)
end
end
end
local function readPlaceables(data, farmId, byChain, nameOf)
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
for index, placeable in pairs(placeables) do
local ok, err = pcall(function()
if not ADPrices.isOwnedBy(placeable, farmId) then return end
local uid = placeableId(placeable)
if uid ~= nil then data.placeables[uid] = placeable end
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
if point ~= nil then
local recipes, points = readFactory(placeable, point, byChain, nameOf)
for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
for key, entry in pairs(points) do data.points[key] = entry end
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
local herd = readHerd(placeable, nameOf)
if herd ~= nil then table.insert(data.husbandry, herd) end
readBarnFood(data, placeable, nameOf)
end
end)
if not ok then
local key = tostring(placeable ~= nil and placeable.uniqueId or index)
failed(data, "placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
end
end
table.sort(data.recipes, function(a, b) return a.key < b.key end)
end
-- Markedspris for en afgrøde, først når kæden spørger (kun kandidatmarkernes afgrøder, kun ved mangel);
-- hver afgrøde slås højst op én gang pr. læsning. Fejl -> 0 (logget én gang).
local function priceResolver(data)
local cache = {}
return function(fruitName)
if cache[fruitName] ~= nil then return cache[fruitName] end
local index = data.fruitFillIndex[fruitName]
local price = 0
if index ~= nil then
local ok, quote = pcall(ADSources.getMarketQuote, index)
if ok then
price = quote ~= nil and quote.price or 0
else
ADChainData.warnOnce("price:" .. tostring(fruitName), "pris for %s kunne ikke læses: %s", tostring(fruitName), tostring(quote))
end
end
cache[fruitName] = price
return price
end
end
local function readCrops(data, nameOf)
local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
for _, fruit in ipairs(fruits) do
if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
local ok, err = pcall(function()
local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
if fillName == nil then return end
if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
data.crops[fillName] = fruit.name
data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
data.fruitFillIndex[fruit.name] = fruit.fillType.index
end)
if not ok then failed(data, "fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
end
end
end
-- ---- markvarer (hø, hakkelse, halm …) ----
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
local function windrowIndex(fruit)
local windrow = fruit.windrowFillType
if type(windrow) == "number" then return windrow end
if type(windrow) == "table" then
if windrow.index ~= nil then return windrow.index end
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
end
end
return nil
end
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
local function converterTargets(converter, add)
for _, entry in pairs(converter or {}) do
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
local function readFieldGoods(data, nameOf)
local indices = {}
local function add(index) if type(index) == "number" then indices[index] = true end end
local function guarded(key, fn)
local ok, err = pcall(fn)
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
end
if g_fillTypeManager == nil then return end
local fruitManager = g_fruitTypeManager
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
end
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
end
end
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
end
end
-- tørrede varianter (vender: græs-vindue -> hø)
if g_fillTypeManager.getConverterDataByName ~= nil then
guarded("TEDDER", function()
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
local windrows = {}
for index in pairs(indices) do table.insert(windrows, index) end
for _, index in ipairs(windrows) do
local entry = tedder[index]
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end)
end
for index in pairs(indices) do
local ok, name = pcall(nameOf, index)
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
end
end
-- Læs i gårdens køretøjer (vogne, høstertanke, marksilo-vogne) tæller som lager: kun afgrøder og markvarer, ikke
-- diesel o.l. loads = {{name, liters}} (in-game 30/9: høst på vej hjem gav falsk mangel og forslag).
function ADChainData.addVehicleLoads(data, loads)
for _, load in ipairs(loads or {}) do
local name, liters = load.name, load.liters or 0
local fruit = name ~= nil and (data.crops or {})[name] or nil
if liters > 0 and (fruit ~= nil or (data.fieldGoods or {})[name]) then
data.stockByFill[name] = (data.stockByFill[name] or 0) + liters
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
end
end
end
local function readVehicleLoads(data, farmId, nameOf)
local ok, err = pcall(function()
local loads = {}
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
local spec = vehicle.spec_fillUnit
if spec ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
for index, unit in ipairs(spec.fillUnits or {}) do
local level = vehicle:getFillUnitFillLevel(index) or 0
if level > 0 then
table.insert(loads, {name = nameOf(vehicle:getFillUnitFillType(index) or unit.fillType), liters = level})
end
end
end
end
ADChainData.addVehicleLoads(data, loads)
end)
if not ok then ADChainData.warnOnce("vehicleLoads", "læs i køretøjer kunne ikke læses: %s", tostring(err)) end
end
local function readStock(data, adEnv, farmId, nameOf)
local ok, err = pcall(function()
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
local name = nameOf(index)
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
local fruit = data.crops[name or ""]
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
end
end)
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
end
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
local function readRates(data, adEnv, farmId, nameOf)
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
local ok, err = pcall(function()
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
local name = nameOf(index)
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
end
end)
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
end
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
local function readUsedFruits(data, adEnv, farmId, nameOf)
for _, recipe in ipairs(data.recipes) do
if recipe.enabled then
for name in pairs(recipe.inputs) do
if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
end
end
end
local ok, err = pcall(function()
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, true) or {}) do
local fruit = data.crops[nameOf(index) or ""]
if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
end
end)
if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
end
-- Markscanningen har ikke nået alle marker endnu (efter indlæsning ~én mark pr. 2 s): marklisten er ufuldstændig,
-- så udbud undervurderes -> fieldsIncomplete (kæden rører ingen marker) og partial (intet slukkes/stoppes).
local function readFields(data)
if ADFields.isScanComplete ~= nil and not ADFields.isScanComplete() then
data.partial, data.fieldsIncomplete = true, true
end
for _, info in ipairs(ADFields.list() or {}) do
local ok, entry = pcall(function()
local field = ADFieldJobs.getField(info.id)
local plan = field.plan or {}
local fruit = info.growing and info.fruitName or field.lastFruit
-- høst i gang: ingen forslag/skift for afgrøden, mens læssene er på vej hjem
if field.stage == "harvesting" and info.fruitName ~= nil then data.harvesting[info.fruitName] = true end
return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
end)
if ok then
table.insert(data.fields, entry)
else
failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
end
end
end
-- ---- afhængighedstjek (spec afsnit 8) ----
local function unloadStationsOf(placeable)
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
local result = {}
if storage == nil or storage.getUnloadingStations == nil then return result end
for _, station in pairs(storage:getUnloadingStations() or {}) do
if station.owningPlaceable == placeable then table.insert(result, station) end
end
return result
end
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
function ADChainData.acceptsMap(data, flows, goods)
local result = {}
for _, flow in ipairs(flows or {}) do
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
for _, id in ipairs(flow.targetBuildingIds or {}) do
local placeable = data.placeables[id]
if result[id] == nil and placeable ~= nil then
result[id] = {}
local ok, err = pcall(function()
local stations = unloadStationsOf(placeable)
for _, name in ipairs(goods) do
local index = data.fillIndex[name]
for _, station in ipairs(stations) do
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
end
end
end)
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
end
end
end
end
return result
end
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
function ADChainData.chainInputs(chain, titles)
local result = {}
local function walk(nodes, rootTitle)
for _, node in ipairs(nodes or {}) do
result[node.fillType] = result[node.fillType] or {}
local list = result[node.fillType]
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
walk(node.children, rootTitle)
end
end
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
walk(root.children, tostring((titles or {})[root.name] or root.name))
end
return result
end
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
function ADChainData.dependencyInputs(data, chain, flows)
local goods, seen = {}, {}
for _, recipe in ipairs(data.recipes or {}) do
if not recipe.enabled and not recipe.byChain then
for name in pairs(recipe.outputs or {}) do
if not seen[name] then seen[name] = true; table.insert(goods, name) end
end
end
end
table.sort(goods)
-- auto-flowets mål tager mange varer (fx biogas: korn, ensilage, gylle): kun varer gården faktisk forbruger
-- (stald eller fabrik) eller en kæde vil have, tæller
local consumed = {}
for _, name in ipairs(goods) do
local rate = (data.rates or {})[name]
if (rate ~= nil and (rate.consumption or 0) > 0) or ((chain ~= nil and chain.demand or {})[name] or 0) > 0 then
table.insert(consumed, name)
end
end
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
#consumed > 0 and ADChainData.acceptsMap(data, flows, consumed) or {})
local adjustment = data.timeAdjustment or 1
local capacity, demand, stockDays = {}, {}, {}
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
for _, name in ipairs(goods) do
local rate = (data.rates or {})[name] or {}
local consumption, production = rate.consumption or 0, rate.production or 0
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
demand[name] = math.max(consumption, chainDemand)
if consumption > production then
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
elseif consumption > 0 then
stockDays[name] = math.huge
end
end
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
end
-- ---- samlet ----
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
-- ikke har nået alle marker.
function ADChainData.collect(adEnv, farmId, byChain)
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}, harvesting = {}}
local nameOf = nameResolver(data)
data.priceOf = priceResolver(data)
readPlaceables(data, farmId, byChain, nameOf)
readCrops(data, nameOf)
readFieldGoods(data, nameOf)
readStock(data, adEnv, farmId, nameOf)
readVehicleLoads(data, farmId, nameOf)
readRates(data, adEnv, farmId, nameOf)
readUsedFruits(data, adEnv, farmId, nameOf)
readFields(data)
return data
end
+167
View File
@@ -0,0 +1,167 @@
-- AD Chain Model (ren logik)
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
ADChainModel = {}
ADChainModel.MAX_DEPTH = 8
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
-- producenter pr. vare: tændte først, så stabil rækkefølge
local function indexProducers(recipes)
local index = {}
for _, recipe in ipairs(recipes or {}) do
for name, amount in pairs(recipe.outputs or {}) do
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
index[name] = index[name] or {}
table.insert(index[name], recipe)
end
end
end
for _, list in pairs(index) do
table.sort(list, function(a, b)
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
return byKey(a, b)
end)
end
return index
end
local function indexHerds(husbandry)
local index = {}
for _, herd in ipairs(husbandry or {}) do
for name, perMonth in pairs(herd.outputs or {}) do
if (perMonth or 0) > 0 then
index[name] = index[name] or {}
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
end
end
end
return index
end
local function sortedNames(map)
local names = {}
for name in pairs(map or {}) do table.insert(names, name) end
table.sort(names)
return names
end
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
-- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion).
-- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den.
local capacity, roots, standalone = {}, {}, {}
for _, recipe in ipairs(recipes or {}) do
capacity[recipe.key] = recipe.cyclesPerMonth or 0
if recipe.enabled and not recipe.byChain then
if not recipe.paused and not recipe.notChosen then table.insert(roots, recipe) end
standalone[recipe.key] = true
end
end
table.sort(roots, byKey)
-- detail = manglende liter/md (flaskehals/køb), need = varens behov pr. måned i den gren
local function problem(kind, fillType, rootKey, detail, need)
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail, need = need})
end
local need
local function inputsOf(recipe, cycles, depth, path, rootKey)
local children = {}
for _, name in ipairs(sortedNames(recipe.inputs)) do
local amount = recipe.inputs[name] or 0
if amount > 0 and cycles > 0 then
local child = need(name, amount * cycles, depth + 1, path, rootKey)
child.consumer, child.consumerName = recipe.building, recipe.buildingName
table.insert(children, child)
end
end
return children
end
need = function(name, perMonth, depth, path, rootKey)
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
chain.demand[name] = (chain.demand[name] or 0) + perMonth
if depth > ADChainModel.MAX_DEPTH or path[name] then
node.source = "cycle"
problem("cycle", name, rootKey, nil, perMonth)
return node
end
path[name] = true
local rest = perMonth
-- a) gårdens fabrikker
local makers = producers[name]
if makers ~= nil then
node.source = "production"
for _, recipe in ipairs(makers) do
local perCycle = recipe.outputs[name]
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
if cycles > 0 then
capacity[recipe.key] = capacity[recipe.key] - cycles
rest = rest - cycles * perCycle
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
if not standalone[recipe.key] then
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
end
end
if rest <= 1e-9 then break end
end
if rest > 1e-9 and #node.producers > 0 then
problem("bottleneck", name, rootKey, rest, perMonth)
node.shortPerMonth = rest
end
if rest > 1e-9 and #node.producers == 0 then
node.source = nil
end
end
-- b) stalde
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
node.source = "husbandry"
for _, herd in ipairs(herds[name]) do
local take = math.min(rest, herd.left)
if take > 0 then
herd.left = herd.left - take
rest = rest - take
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
end
end
end
-- c) mark
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
node.source, node.fruit = "field", crops[name]
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
rest = 0
end
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
node.source, node.fruit = "field", nil
rest = 0
end
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
node.source = "production"
node.shortPerMonth = rest
problem("bottleneck", name, rootKey, rest, perMonth)
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
if node.source == nil then node.source = "buy" end
node.shortPerMonth = rest
problem("buy", name, rootKey, rest, perMonth)
end
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
path[name] = nil
return node
end
for _, root in ipairs(roots) do
local cycles = root.cyclesPerMonth or 0
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
children = inputsOf(root, cycles, 0, {}, root.key)})
end
return chain
end
+362
View File
@@ -0,0 +1,362 @@
-- AD Chain Run (kædens handlinger i spillet)
-- Udfører ADChain's plan: skifter frie marker (kun "auto", højst MAX_FIELD_SWITCHES pr. tick), tænder/slukker
-- mellemled, opretter manglende forsyningsflows (højst MAX_NEW_FLOWS pr. tick) og stopper flows for rødder der
-- er slået fra. Hver del kører i pcall, så en fejl ikke stopper resten. Spilkald kun inde i funktioner.
ADChainRun = {}
local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end
local UNLOADER = "unloader"
-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool).
local function isUnloader(implement)
return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true)
end
-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle
-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig.
local function belongsToFlow(tractor)
return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil
end
-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og
-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu).
function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked)
if fillTypeIndex == nil then return nil end
used, blocked = used or {}, blocked or {}
local free = {}
for _, tractor in ipairs(tractors or {}) do
if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then
table.insert(free, tractor)
end
end
for _, implement in ipairs(implements or {}) do
if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then
local tractor = ADPool.bestFit(free, implement, "unloader")
if tractor ~= nil then return {implement = implement, tractor = tractor} end
end
end
return nil
end
-- ---- marker ----
local function fieldById(fields, id)
for _, field in ipairs(fields or {}) do
if field.id == id then return field end
end
return nil
end
function ADChainRun.switchFields(data, actions)
local switched = 0
for _, action in ipairs(actions or {}) do
if switched >= ADChain.MAX_FIELD_SWITCHES then break end
local field = fieldById(data.fields, action.fieldId)
-- brugerens egen målafgrøde røres aldrig (dobbelttjek oven i ADCropPlan)
if action.mode == "auto" and field ~= nil and field.targetSource ~= "user" then
local ok, err = pcall(ADFieldJobs.setTarget, action.fieldId, action.to, "chain")
if ok then
switched = switched + 1
ADChain.switchCount = ADChain.switchCount + 1
local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from),
tostring(action.to), action.hectares or 0, tostring(action.reason))
log("%s", text)
pcall(ADFieldJobs.notify, text, false)
else
warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err))
end
end
end
end
-- ---- mellemled ----
local function setProduction(data, key, isOn)
local entry = data.points[key]
if entry == nil or entry.point == nil or entry.point.setProductionState == nil then
warn("produktion %s findes ikke (længere)", tostring(key))
return false
end
local ok, err = pcall(entry.point.setProductionState, entry.point, entry.productionId, isOn)
if not ok then warn("produktion %s kunne ikke %s: %s", tostring(key), isOn and "tændes" or "slukkes", tostring(err)) end
return ok
end
-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen.
-- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra).
function ADChainRun.toggleProductions(data, prod)
local state = ADChain.state
local byChain = state.byChain
for _, key in ipairs(prod.enable or {}) do
if not state.userOff[key] and setProduction(data, key, true) then
byChain[key] = true
log("tænder %s (mellemled i kæden)", key)
end
end
if data.partial then return end
for _, key in ipairs(prod.disable or {}) do
if setProduction(data, key, false) then
byChain[key] = nil
log("slukker %s (kæden bruger den ikke længere)", key)
end
end
end
-- ---- flows ----
local function collectPool(adEnv)
local ok, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
end)
if not ok then
warn("maskinpuljen kunne ikke læses: %s", tostring(implements))
return {}, {}
end
return implements or {}, tractors or {}
end
-- kind = "noVehicle"|"pallet": vises som status på kanten i fanen Kæder
local function createStopped(run, kind, reason, ...)
run.stopped = true
ADChain.noVehicle[run.id] = kind
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
end
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
local function flowTitle(data, fillTypes)
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
if #fillTypes == 1 then return first end
return string.format("%s +%d", tostring(first), #fillTypes - 1)
end
local function startWithPair(adEnv, run, runId, pair, used)
used[pair.tractor.id], used[pair.implement.id] = true, true
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
run = ADRuns.getRun(ADRunsController.store, runId)
local wagons = ADRuns.copyWagons(run.wagons)
wagons[pair.tractor.id] = pair.implement.id
run.wagons = wagons
ADRunsController.resolveMarkers(adEnv, run)
local results = ADRunsController.startRun(adEnv, runId)
local started = 0
for _, result in ipairs(results or {}) do if result.ok then started = started + 1 end end
log("flow '%s' oprettet med '%s' + vogn '%s' (%d startet)", tostring(run.name), tostring(pair.tractor.name),
tostring(pair.implement.name), started)
end
local function createGroupFlow(adEnv, data, group, fillTypes, pool, used)
local title = flowTitle(data, fillTypes)
local created = ADRunsController.createRun(ADChain.flowName(title, group.consumerName or group.consumer), "supply")
local runId = created.id
ADRunsController.updateRun(runId, {targetBuildingIds = {group.consumer}, ingredientMode = "manual", ingredients = fillTypes,
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, group.consumer), chain = group.rootKey})
-- updateRun erstatter store'en: slå altid run'et op igen
local run = ADRuns.getRun(ADRunsController.store, runId)
local fillIndex = data.fillIndex[fillTypes[1]]
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
return
end
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
if pair == nil then
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
return
end
startWithPair(adEnv, run, runId, pair, used)
end
-- Nye varer til en bygning, der allerede har et kædeflow: lægges i det flow.
local function extendFlow(runId, fillTypes)
local run = ADRuns.getRun(ADRunsController.store, runId)
if run == nil then return false end
local ingredients = {}
for _, name in ipairs(run.ingredients or {}) do table.insert(ingredients, name) end
local added = {}
for _, name in ipairs(fillTypes) do
local present = false
for _, have in ipairs(ingredients) do if have == name then present = true end end
if not present then table.insert(ingredients, name); table.insert(added, name) end
end
if #added == 0 then return false end
ADRunsController.updateRun(runId, {ingredients = ingredients})
log("flow '%s' får også %s", tostring(run.name), table.concat(added, ", "))
return true
end
-- Tidligere kædeflows (ét pr. vare) til samme bygning uden traktorer lægges sammen i ét.
function ADChainRun.mergeIdleFlows(adEnv)
local merged = 0
for _, merge in ipairs(ADChainActions.mergeIdle(ADChain.runsView(ADRunsController.store.runs))) do
if #merge.add > 0 then extendFlow(merge.keepId, merge.add) end
for _, id in ipairs(merge.deleteIds) do
local ok, err = pcall(ADRunsController.deleteRun, id, adEnv)
if ok then
ADChain.noVehicle[id] = nil
merged = merged + 1
else
warn("kædeflow %s kunne ikke lægges sammen: %s", tostring(id), tostring(err))
end
end
end
if merged > 0 then log("%d tomme kædeflows lagt sammen (ét flow pr. bygning)", merged) end
return merged
end
function ADChainRun.createFlows(adEnv, data, edges)
local runs = ADChain.runsView(ADRunsController.store.runs)
local missing = ADChainActions.missingFlows(edges, runs)
local pool, used, count = {}, {}, 0
for _, group in ipairs(ADChainActions.groupMissing(missing)) do
if count >= ADChain.MAX_NEW_FLOWS then break end
local bulk, pallets = {}, {}
for _, name in ipairs(group.fillTypes) do
local index = data.fillIndex[name]
table.insert((index ~= nil and ADSources.isPalletFillType(index)) and pallets or bulk, name)
end
local ok, err = pcall(function()
if #bulk > 0 then
local existing = ADChainActions.chainRunFor(runs, group.consumer)
if existing ~= nil then
if extendFlow(existing, bulk) then count = count + 1 end
else
createGroupFlow(adEnv, data, group, bulk, pool, used)
count = count + 1
end
end
for _, name in ipairs(pallets) do
createGroupFlow(adEnv, data, group, {name}, pool, used)
count = count + 1
end
end)
if not ok then warn("flow til %s kunne ikke oprettes: %s", tostring(group.consumerName), tostring(err)) end
end
return count
end
-- Slutprodukter: sælg straks (fabrikkens udgang -> "Sælg direkte"), medmindre kæden står på "Behold". Varer kæden
-- selv satte til salg, sættes tilbage til "Behold", når spilleren vælger det.
local function outputMode(point, fillIndex)
if point.getOutputDistributionMode == nil then return nil end
local ok, mode = pcall(point.getOutputDistributionMode, point, fillIndex)
return ok and mode or nil
end
function ADChainRun.applyFinalSales(data, chain, edges)
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
local state, changed = ADChain.state, 0
local wanted = {}
local uses = ADChainActions.farmUses(data, ADChain.runsView(ADRunsController.store.runs))
for _, sale in ipairs(ADChainActions.finalSales(chain, data.recipes, edges, state.finalKeep, uses)) do
local id = sale.key .. "|" .. sale.fillType
wanted[id] = true
local entry, fillIndex = (data.points or {})[sale.key], data.fillIndex[sale.fillType]
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
and outputMode(entry.point, fillIndex) ~= ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.DIRECT_SELL)
state.soldByChain[id] = true
changed = changed + 1
log("slutprodukt %s i %s sælges straks", tostring(sale.fillType), tostring(entry.buildingName or sale.key))
end
end
for id in pairs(state.soldByChain) do
local key, fillType = id:match("^(.*)|([^|]+)$")
-- sat til salg af kæden, men ikke længere ønsket (Behold, gården bruger varen, kæden væk): tilbage til Behold
if key ~= nil and not wanted[id] then
local entry, fillIndex = (data.points or {})[key], data.fillIndex[fillType]
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
changed = changed + 1
log("slutprodukt %s beholdes igen", tostring(fillType))
end
state.soldByChain[id] = nil
end
end
return changed
end
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
function ADChainRun.keepIntermediates(data, edges)
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
local changed = 0
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
changed = changed + 1
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
tostring(entry.buildingName or item.key))
end
end
return changed
end
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
function ADChainRun.stopFlows(adEnv, chain, edges)
local changed = 0
local active = ADChain.activeRoots(chain, ADChain.state.paused)
for _, run in ipairs(ADChainActions.flowsToStop(ADChain.runsView(ADRunsController.store.runs), active)) do
local rootKey = ADChain.stillNeeded(run, edges)
if rootKey ~= nil then
ADRunsController.updateRun(run.id, {chain = rootKey})
log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey))
changed = changed + 1
elseif ADChainActions.releaseAction(run) == "delete" then
local ok, err = pcall(ADRunsController.deleteRun, run.id, adEnv)
if ok then
ADChain.noVehicle[run.id] = nil
log("tomt kædeflow '%s' slettet: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
changed = changed + 1
else
warn("flow '%s' kunne ikke slettes: %s", tostring(run.name), tostring(err))
end
else
local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id)
if ok then
ADRunsController.updateRun(run.id, {chain = ""})
log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
changed = changed + 1
else
warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err))
end
end
end
return changed
end
-- Hele planen; hver del for sig i pcall.
function ADChainRun.apply(adEnv, data, result)
local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions)
if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end
local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod)
if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end
local okMerge, mergedCount = pcall(ADChainRun.mergeIdleFlows, adEnv)
if not okMerge then warn("sammenlægning af kædeflows fejlede: %s", tostring(mergedCount)) end
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
local okStop, stopped = true, 0
if not data.partial then
okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges)
if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end
end
if (okNew and created > 0) or (okStop and stopped > 0) then
local okSave, errSave = pcall(ADRunsController.save)
if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end
end
end
+453
View File
@@ -0,0 +1,453 @@
-- AD Chain View (ren logik)
-- Rækkerne til fanen Kæder ud fra ADChain.last. Hver rod er foldet sammen som standard (én linje med samlet
-- status + antal forslag); foldet ud vises varerne rekursivt (kilde, behov pr. måned, status, lager). Forslag
-- vises altid (under roden eller under afgrøden). Pausede rødder (som modellen ikke længere ser som rødder)
-- får en grå linje med [Genoptag]. Nederst frie marker + antal automatiske skift (+ "marker scannes endnu"). Ingen spiladgang:
-- titleOf(navn) -> titel, fmt(liter) -> kompakt tal, textOf(l10n-nøgle) -> tekst gives udefra.
-- Manglende led (Task 10): foldet ud står problemteksten (problemText) som en rød tekstrække ("problem", ingen celler)
-- lige under varen — teksten er for lang til en celle og ombrydes af fanen.
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty"|"problem", indent, level, cells={c1..c5}, text,
-- actions={[celle]={kind="approve"|"dismiss"|"pause"|"resume", fieldId|rootKey}}, toggle=rootKey, fillType}
ADChainView = {}
ADChainView.INDENT = " "
-- Tegnbudget pr. celle i flowListPrefab (gui/SmartPickupFrame.xml: textMaxWidth 192/162/62/244/92 px).
-- Antaget ~11,5 px pr. tegn (spuInfoText: 800 px ~ 70 tegn), fed ~12,5 px; c3 har kun tal (~10 px pr. tegn).
ADChainView.CELL_CHARS = {22, 20, 7, 28, 10} -- ca. tegn pr. celle (c1 192 px fed, c2 162, c3 62, c4 244, c5 92 fed)
ADChainView.MAX_SOURCE_NAMES = 2
-- Tegnbudget for en tekstlinje (addTextRow/spuInfoText: 800 px ~ 70 tegn), fx linjen med frie marker.
ADChainView.TEXT_ROW_CHARS = 70
-- "stramt": lager for en mark-afgrøde under så mange måneders behov -> gul
ADChainView.TIGHT_MONTHS = 3
-- nøgler rækkerne bruger (til l10n-testen)
ADChainView.TEXT_KEYS = {"spu_chain_optIn", "spu_chain_optOut", "spu_chain_notChosen", "spu_chain_candidatesShow", "spu_chain_candidatesHide", "spu_chain_finalFarm", "spu_chain_finalSell", "spu_chain_finalKeep", "spu_chain_suggestHeader", "spu_chain_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
"spu_chain_flowOk", "spu_chain_flowMissing", "spu_chain_flowPallet", "spu_chain_flowNone", "spu_chain_flowStopped", "spu_chain_bottleneck",
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource", "spu_chain_fromFields",
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
"spu_chain_flowAuto", "spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
pallet = "spu_chain_flowPallet", auto = "spu_chain_flowAuto"}
local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError", waiting = "spu_chain_rootWaiting", stopped = "spu_chain_rootPaused"}
local function identity(value) return tostring(value) end
-- samme som SmartPickupFrame.formatCompact (standard-fmt når ingen gives, fx i beskeden fra kædens tick)
function ADChainView.compact(liters)
liters = math.floor((liters or 0) + 0.5)
if liters >= 1000000 then return string.format("%.1fM", liters / 1000000)
elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end
return string.format("%d", liters)
end
-- ---- UTF-8-sikker længde/afkortning (æøå og → er flere bytes) ----
function ADChainView.textLength(text)
local count = 0
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
return count
end
local function utf8Prefix(text, chars)
local result, count = {}, 0
for char in text:gmatch("[^\128-\191][\128-\191]*") do
if count >= chars then break end
table.insert(result, char)
count = count + 1
end
return table.concat(result)
end
-- Afkort til celle-budgettet med ".." til sidst.
function ADChainView.fit(text, chars)
text = tostring(text or "")
if ADChainView.textLength(text) <= chars then return text end
return utf8Prefix(text, math.max(0, chars - 2)) .. ".."
end
local function fitCells(cells)
local result = {}
for index = 1, 5 do result[index] = ADChainView.fit(cells[index] or "", ADChainView.CELL_CHARS[index]) end
return result
end
local function haText(ha)
return string.format("%.0f", ha or 0)
end
local function plannedFruit(field)
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
return field.fruit
end
local function edgeKey(node)
return tostring(node.consumer) .. "|" .. tostring(node.fillType)
end
local function shortageByFruit(last)
local result = {}
for _, shortage in ipairs(last.shortages or {}) do result[shortage.fruit] = shortage end
return result
end
local function stockOf(last, fruit)
local stock = last.data ~= nil and last.data.stock or nil
return stock ~= nil and fruit ~= nil and stock[fruit] or nil
end
-- Lager for en mark-node: afgrødens lager, eller varens eget lager for en markvare uden afgrøde (hø, hakkelse …).
local function fieldStockOf(last, node)
if node.fruit ~= nil then return stockOf(last, node.fruit) end
local byFill = last.data ~= nil and last.data.stockByFill or nil
return byFill ~= nil and byFill[node.fillType] or nil
end
-- Mark-afgrøde uden mangel men med lager under TIGHT_MONTHS måneders behov.
local function isTight(node, last, short)
if node.source ~= "field" or node.fruit == nil or short[node.fruit] ~= nil then return false end
return (stockOf(last, node.fruit) or 0) < ADChainView.TIGHT_MONTHS * (node.perMonth or 0)
end
local function eachNode(nodes, fn, depth)
for _, node in ipairs(nodes or {}) do
fn(node, depth or 1)
eachNode(node.children, fn, (depth or 1) + 1)
end
end
function ADChainView.rootLevel(root, last)
last = last or {}
if (last.paused or {})[root.key] then return "stopped" end
for _, problem in ipairs((last.chain or {}).problems or {}) do
if problem.rootKey == root.key then return "error" end
end
local short, flowStatus = shortageByFruit(last), last.flowStatus or {}
local hasShortage, hasWaiting = false, false
eachNode(root.children, function(node)
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
if (status ~= nil and status ~= "ok" and status ~= "auto") or isTight(node, last, short) then hasWaiting = true end
end)
if hasShortage then return "error" end
if hasWaiting then return "waiting" end
return "running"
end
-- Højst MAX_SOURCE_NAMES navne + " +k".
local function joinCapped(names)
local shown = {}
for index = 1, math.min(#names, ADChainView.MAX_SOURCE_NAMES) do shown[index] = names[index] end
local text = table.concat(shown, ", ")
if #names > #shown then text = text .. " +" .. tostring(#names - #shown) end
return text
end
-- Opskrifter (alle, også slukkede) der laver varen, i datarækkefølge.
local function producersOf(last, fillType)
local list = {}
for _, recipe in ipairs(last.data ~= nil and last.data.recipes or {}) do
if ((recipe.outputs or {})[fillType] or 0) > 0 then table.insert(list, recipe) end
end
return list
end
-- Årsag i klart sprog med råd for et problem fra kædemodellen ({kind="buy"|"bottleneck"|"cycle", fillType, rootKey,
-- detail = manglende l/md, need = behov l/md}). Køb af en rest efter gårdens stalde -> stald-tekst. En producent spilleren selv har slukket (last.userOff, stadig
-- slukket) slår igennem som "slukket af dig" — det er den reelle årsag, uanset om modellen så køb eller flaskehals.
function ADChainView.problemText(problem, last, textOf, titleOf, fmt)
last, textOf, fmt = last or {}, textOf or identity, fmt or ADChainView.compact
titleOf = titleOf or function(name) return (last.titles or {})[name] or tostring(name) end
local item = titleOf(problem.fillType)
if problem.kind == "cycle" then return string.format(textOf("spu_chain_problem_cycle"), item) end
local userOff, names, seen = last.userOff or {}, {}, {}
for _, recipe in ipairs(producersOf(last, problem.fillType)) do
if userOff[recipe.key] and not recipe.enabled then
return string.format(textOf("spu_chain_problem_off"), tostring(recipe.buildingName), titleOf(recipe.name))
end
local name = tostring(recipe.buildingName)
if (recipe.cyclesPerMonth or 0) > 0 and not seen[name] then
seen[name] = true
table.insert(names, name)
end
end
if problem.kind == "bottleneck" and #names > 0 then
local need = problem.need or problem.detail or 0
local made = math.max(0, need - (problem.detail or 0))
return string.format(textOf("spu_chain_problem_bottleneck"), joinCapped(names), fmt(math.floor(made + 0.5)), fmt(math.floor(need + 0.5)))
end
-- modellen melder også "køb" for resten, når gårdens stalde leverer for lidt: så er rådet ikke "byg en fabrik"
for _, herd in ipairs(last.data ~= nil and last.data.husbandry or {}) do
if ((herd.outputs or {})[problem.fillType] or 0) > 0 then
return string.format(textOf("spu_chain_problem_barn"), item)
end
end
return string.format(textOf("spu_chain_problem_buy"), item)
end
local function sourceText(node, last, textOf)
local names = {}
if node.source == "production" then
for _, producer in ipairs(node.producers or {}) do table.insert(names, tostring(producer.buildingName)) end
elseif node.source == "husbandry" then
for _, herd in ipairs(node.herds or {}) do table.insert(names, tostring(herd.buildingName)) end
elseif node.source == "field" then
-- markvare uden egen afgrøde (hø, hakkelse …): ingen afgrødeplan, bare "fra marker"
if node.fruit == nil then return textOf("spu_chain_fromFields") end
for _, field in ipairs(last.fields or {}) do
if plannedFruit(field) == node.fruit then table.insert(names, "F" .. tostring(field.id)) end
end
if #names == 0 then return textOf("spu_chain_noField") end
elseif node.source == "buy" then
return textOf("spu_chain_buySource")
else
return "-"
end
return joinCapped(names)
end
-- Nodens status (tekst, niveau): kredsløb/flaskehals/køb/mangel (rød) > flow ikke ok (gul) > stramt lager (gul) > flow ok.
local function nodeStatus(node, last, fmt, textOf, short)
if node.source == "cycle" then return textOf("spu_chain_cycle"), "error" end
if (node.shortPerMonth or 0) > 0 then
local key = node.source == "production" and "spu_chain_bottleneck" or "spu_chain_buy"
return string.format(textOf(key), fmt(node.shortPerMonth)), "error"
end
local shortage = node.fruit ~= nil and short[node.fruit] or nil
if shortage ~= nil then return string.format(textOf("spu_chain_fieldShort"), haText(shortage.missingHa)), "error" end
local status = node.consumer ~= nil and (last.flowStatus or {})[edgeKey(node)] or nil
-- udgangen står på Fordel: spillet leverer selv
if status == "auto" then return textOf("spu_chain_flowAuto"), "running" end
if status ~= nil and status ~= "ok" then return textOf(FLOW_KEYS[status] or "spu_chain_flowNone"), "waiting" end
if isTight(node, last, short) then return textOf("spu_chain_tight"), "waiting" end
if status == "ok" then return textOf("spu_chain_flowOk"), "running" end
return "", "running"
end
local function joinText(cells)
local parts = {}
for _, cell in ipairs(cells) do
if cell ~= nil and cell ~= "" then table.insert(parts, cell) end
end
return table.concat(parts, " · ")
end
local function makeRow(kind, indent, level, cells, actions, toggle)
local fitted = fitCells(cells)
return {kind = kind, indent = indent, level = level, cells = fitted, actions = actions or {}, toggle = toggle, text = joinText(fitted)}
end
local function suggestRow(action, indent, titleOf, textOf)
local pad = string.rep(ADChainView.INDENT, indent)
local from = (action.from ~= nil and action.from ~= "") and titleOf(action.from) or "-"
local cells = {pad .. "F" .. tostring(action.fieldId) .. " " .. from, "→ " .. titleOf(action.to), haText(action.hectares) .. " ha"}
if action.mode == "suggest" then
cells[4], cells[5] = textOf("spu_chain_approve"), textOf("spu_chain_dismiss")
return makeRow("suggest", indent, "waiting", cells,
{[4] = {kind = "approve", fieldId = action.fieldId}, [5] = {kind = "dismiss", fieldId = action.fieldId}})
end
cells[4] = textOf("spu_chain_auto")
return makeRow("auto", indent, "running", cells)
end
-- Handlinger for de afgrøder (i rækkefølge) der hentes fra mark under roden.
local function rootFruits(root)
local fruits, seen = {}, {}
eachNode(root.children, function(node)
if node.source == "field" and node.fruit ~= nil and not seen[node.fruit] then
seen[node.fruit] = true
table.insert(fruits, node.fruit)
end
end)
return fruits
end
local function actionsFor(last, fruit, modes)
local list = {}
for _, action in ipairs(last.actions or {}) do
if action.to == fruit and modes[action.mode] then table.insert(list, action) end
end
return list
end
-- Alle forslag (mode "suggest") én gang pr. mark, efter marknummer (in-game 30/9: samme forslag stod under
-- hver kæde der bruger afgrøden, spredt mellem kæderne).
function ADChainView.suggestionList(last)
local byField, list = {}, {}
for _, action in ipairs((last or {}).actions or {}) do
if action.mode == "suggest" and action.fieldId ~= nil and not byField[action.fieldId] then
byField[action.fieldId] = true
table.insert(list, action)
end
end
table.sort(list, function(a, b) return (tonumber(a.fieldId) or 0) < (tonumber(b.fieldId) or 0) end)
return list
end
-- Kæder efter status (fejl/mangel, venter, dækket, pause), så bygning, så produkt.
local LEVEL_RANK = {error = 1, waiting = 2, running = 3, stopped = 4}
function ADChainView.sortRoots(roots, last)
local keyed = {}
for index, root in ipairs(roots or {}) do
table.insert(keyed, {root = root, rank = LEVEL_RANK[ADChainView.rootLevel(root, last)] or 5,
building = tostring(root.buildingName or ""), name = tostring(root.name or ""), index = index})
end
table.sort(keyed, function(a, b)
if a.rank ~= b.rank then return a.rank < b.rank end
if a.building ~= b.building then return a.building < b.building end
if a.name ~= b.name then return a.name < b.name end
return a.index < b.index
end)
local sorted = {}
for _, item in ipairs(keyed) do table.insert(sorted, item.root) end
return sorted
end
local function rootRows(rows, root, last, ctx, isExpanded)
local level = ADChainView.rootLevel(root, last)
local paused = (last.paused or {})[root.key] == true
local suggestions = {}
for _, fruit in ipairs(rootFruits(root)) do
for _, action in ipairs(actionsFor(last, fruit, {suggest = true})) do table.insert(suggestions, action) end
end
local status = ctx.textOf(ROOT_STATUS[level])
if #suggestions > 0 then status = string.format(ctx.textOf("spu_chain_rootSummary"), status, #suggestions) end
local marker = (#(root.children or {}) > 0) and (isExpanded and "– " or "+ ") or ""
-- slutproduktet: "Sælg straks" (standard) eller "Behold" — klik på feltet skifter
-- varer gården selv bruger (last.sellable uden roden) sælges aldrig: "Gård" uden klik
local keep = (last.finalKeep or {})[root.key] == true
local sellable = last.sellable == nil or last.sellable[root.key] == true
local finalText = not sellable and "spu_chain_finalFarm" or (keep and "spu_chain_finalKeep" or "spu_chain_finalSell")
local actions = {[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}
if sellable then actions[3] = {kind = "finalMode", rootKey = root.key, keep = not keep} end
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""),
ctx.textOf(finalText), status, ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")}, actions, root.key))
if not isExpanded then return end
local shown, problems, told = {}, {}, {}
for _, problem in ipairs((last.chain or {}).problems or {}) do
if problem.rootKey == root.key then table.insert(problems, problem) end
end
local function problemRows(fillType, indent)
for _, problem in ipairs(problems) do
local key = tostring(problem.fillType) .. "|" .. tostring(problem.kind)
if (fillType == nil or problem.fillType == fillType) and not told[key] then
told[key] = true
table.insert(rows, {kind = "problem", indent = indent, level = "error",
text = string.rep(ADChainView.INDENT, indent) .. ADChainView.problemText(problem, last, ctx.textOf, ctx.titleOf, ctx.fmt)})
end
end
end
eachNode(root.children, function(node, depth)
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
local stock = node.source == "field" and fieldStockOf(last, node) or nil
local row = makeRow("node", depth, nodeLevel, {string.rep(ADChainView.INDENT, depth) .. ctx.titleOf(node.fillType),
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
row.fillType = node.fillType
-- klik åbner flowet der kører varen til modtageren (in-game 30/9: "flow stopped" kunne ikke redigeres)
if node.consumer ~= nil then row.openConsumer, row.openFillType = node.consumer, node.fillType end
table.insert(rows, row)
problemRows(node.fillType, depth + 1)
if node.source == "field" and node.fruit ~= nil and not shown[node.fruit] then
shown[node.fruit] = true
for _, action in ipairs(actionsFor(last, node.fruit, {auto = true})) do
table.insert(rows, suggestRow(action, depth + 1, ctx.titleOf, ctx.textOf))
end
end
end)
-- problemer uden en vist vare (bør ikke ske) står sidst under roden
problemRows(nil, 1)
end
-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag].
local function pausedRows(rows, last, ctx)
local present, keys = {}, {}
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do present[root.key] = true end
for key in pairs(last.paused or {}) do
if not present[key] then table.insert(keys, key) end
end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
for _, key in ipairs(keys) do
local recipe
for _, candidate in ipairs(last.data ~= nil and last.data.recipes or {}) do
if candidate.key == key then recipe = candidate end
end
local name = recipe ~= nil and recipe.name ~= nil and ctx.titleOf(recipe.name) or tostring(key)
-- pauset kæde: [Start] genoptager, [Fjern] tager den ud af kæderne
table.insert(rows, makeRow("root", 0, "stopped", {name, recipe ~= nil and tostring(recipe.buildingName or "") or "", "",
ctx.textOf("spu_chain_optOut"), ctx.textOf("spu_chain_resume")},
{[4] = {kind = "optOut", rootKey = key}, [5] = {kind = "resume", rootKey = key}}))
end
return #keys
end
-- Tændte produktioner uden kæde: foldet sammen under én linje; udfoldet med [Kæde til] pr. produktion.
local function candidateRows(rows, last, ctx, isExpanded)
local list = last.candidates or {}
if #list == 0 then return end
local key = isExpanded and "spu_chain_candidatesHide" or "spu_chain_candidatesShow"
table.insert(rows, makeRow("root", 0, nil, {(isExpanded and "– " or "+ ") .. string.format(ctx.textOf(key), #list), "", "", "", ""}, {}, "__candidates"))
if not isExpanded then return end
for _, candidate in ipairs(list) do
table.insert(rows, makeRow("root", 1, nil, {ADChainView.INDENT .. ctx.titleOf(candidate.name), tostring(candidate.buildingName or ""), "",
ctx.textOf("spu_chain_notChosen"), ctx.textOf("spu_chain_optIn")}, {[5] = {kind = "optIn", rootKey = candidate.key}}))
end
end
-- "Marker fri til kæden: F1, F2 +k" — så mange navne som tekstlinjen kan rumme, resten talt.
function ADChainView.freeText(names, textOf)
local format = textOf("spu_chain_free")
if #names == 0 then return string.format(format, "-") end
for count = #names, 1, -1 do
local shown = {}
for index = 1, count do shown[index] = names[index] end
local list = table.concat(shown, ", ")
if count < #names then list = list .. " +" .. tostring(#names - count) end
local text = string.format(format, list)
if ADChainView.textLength(text) <= ADChainView.TEXT_ROW_CHARS then return text end
end
return ADChainView.fit(string.format(format, "+" .. tostring(#names)), ADChainView.TEXT_ROW_CHARS)
end
local function footerRows(rows, last, textOf)
local free = {}
for _, field in ipairs(last.fields or {}) do
if field.chainFree then table.insert(free, field.id) end
end
table.sort(free)
local names = {}
for _, id in ipairs(free) do table.insert(names, "F" .. tostring(id)) end
table.insert(rows, {kind = "info", indent = 0, text = ADChainView.freeText(names, textOf)})
table.insert(rows, {kind = "info", indent = 0, text = string.format(textOf("spu_chain_switches"), last.switchCount or 0)})
if last.fieldsIncomplete then table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_scanning")}) end
end
-- expanded = {[rodnøgle] = true} for udfoldede rødder (alle andre er foldet sammen).
function ADChainView.rows(last, titleOf, fmt, textOf, expanded)
textOf, fmt, expanded = textOf or identity, fmt or identity, expanded or {}
local titles = last ~= nil and last.titles or {}
titleOf = titleOf or function(name) return (titles or {})[name] or tostring(name) end
local rows = {}
local roots = last ~= nil and last.chain ~= nil and last.chain.roots or {}
local hasPaused = last ~= nil and next(last.paused or {}) ~= nil
if #roots == 0 and not hasPaused then
table.insert(rows, {kind = "empty", indent = 0, text = textOf("spu_chain_empty")})
end
if last == nil then return rows end
if #roots > 0 or hasPaused then
table.insert(rows, makeRow("header", 0, nil, {textOf("spu_chain_col_item"), textOf("spu_chain_col_source"),
textOf("spu_chain_col_perMonth"), textOf("spu_chain_col_status"), textOf("spu_chain_col_stock")}))
end
local ctx = {titleOf = titleOf, fmt = fmt, textOf = textOf, short = shortageByFruit(last)}
local suggestions = ADChainView.suggestionList(last)
if #suggestions > 0 then
table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_suggestHeader")})
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 0, titleOf, textOf)) end
end
for _, root in ipairs(ADChainView.sortRoots(roots, last)) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
pausedRows(rows, last, ctx)
candidateRows(rows, last, ctx, expanded["__candidates"] == true)
footerRows(rows, last, textOf)
return rows
end
+66 -4
View File
@@ -169,12 +169,43 @@ function ADCpBridge.getSpecs(vehicle)
return specs return specs
end end
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster. -- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter
-- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere).
function ADCpBridge.vineFruitSet(spec)
local fruits = {}
if spec == nil then return fruits end
for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do
if type(spec[key]) == "number" then fruits[spec[key]] = true end
end
if type(spec.fruitTypes) == "table" then
for key, value in pairs(spec.fruitTypes) do
if type(value) == "number" then fruits[value] = true
elseif value == true and type(key) == "number" then fruits[key] = true end
end
end
return fruits
end
local vineLogged = setmetatable({}, {__mode = "k"})
-- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere i kæden). nil = ikke en høster.
function ADCpBridge.getHarvestFruits(vehicle) function ADCpBridge.getHarvestFruits(vehicle)
if vehicle.spec_combine == nil then return nil end if vehicle.spec_combine == nil then return nil end
local fruits = {} local fruits = {}
for _, object in ipairs(collect(vehicle)) do for _, object in ipairs(collect(vehicle)) do
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
if object.spec_vineCutter ~= nil then
local vine = ADCpBridge.vineFruitSet(object.spec_vineCutter)
for fruit in pairs(vine) do fruits[fruit] = true end
if next(vine) == nil and not vineLogged[object] then
-- ukendt feltnavn i denne spilversion: vis nøglerne, så reglen kan rettes
vineLogged[object] = true
local keys = {}
for key, value in pairs(object.spec_vineCutter) do table.insert(keys, tostring(key) .. "=" .. type(value)) end
table.sort(keys)
Logging.info("ADSmartPickup: vinhøster uden afgrøde fundet — spec_vineCutter: %s", table.concat(keys, ", "))
end
end
end end
return fruits return fruits
end end
@@ -360,9 +391,21 @@ local function courseplayWidth(vehicle)
return nil return nil
end end
-- Vinhøster uden AI-markører: bredden er vinrækken (3 m = Courseplays vin-gitter for druer).
ADCpBridge.VINE_ROW_WIDTH = 3
function ADCpBridge.widthOrVine(width, hasVineCutter)
-- under markarbejdets minimum (1,5 m) — også en lille bredde fra Courseplay (in-game 29/9: stadig widthUnknown)
if hasVineCutter and (width or 0) < 1.5 then return ADCpBridge.VINE_ROW_WIDTH end
return width
end
function ADCpBridge.getWorkWidth(vehicle) function ADCpBridge.getWorkWidth(vehicle)
local hasVine = false
for _, object in ipairs(collect(vehicle)) do
if object.spec_vineCutter ~= nil then hasVine = true end
end
local cpWidth = courseplayWidth(vehicle) local cpWidth = courseplayWidth(vehicle)
if cpWidth ~= nil then return cpWidth end if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) end
local best = 0 local best = 0
for _, object in ipairs(collect(vehicle)) do for _, object in ipairs(collect(vehicle)) do
local variable = variableWidth(object) local variable = variableWidth(object)
@@ -374,7 +417,7 @@ function ADCpBridge.getWorkWidth(vehicle)
if ok and type(width) == "number" and width > best then best = width end if ok and type(width) == "number" and width > best then best = width end
end end
end end
return best return ADCpBridge.widthOrVine(best, hasVine)
end end
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait -- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
@@ -476,6 +519,24 @@ function ADCpBridge.courseHasHeadlands(course)
return not ok or type(n) ~= "number" or n > 0 return not ok or type(n) ~= "number" or n > 0
end end
-- Kursusbredde til Courseplay: højst 50 m (CP's grænse; Unrealistic-redskaber kan sættes bredere). nil = urørt.
ADCpBridge.CP_MAX_WIDTH = 50
function ADCpBridge.cpCourseWidth(width)
if type(width) == "number" and width > ADCpBridge.CP_MAX_WIDTH then return ADCpBridge.CP_MAX_WIDTH end
return nil
end
local function clampCourseWidth(vehicle)
pcall(function()
local setting = vehicle:getCourseGeneratorSettings().workWidth
local capped = ADCpBridge.cpCourseWidth(setting:getValue())
if capped ~= nil then
setting:setValue(capped)
Logging.info("ADSmartPickup: kursusbredde for '%s' begrænset til %d m (Courseplays maksimum)", tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), capped)
end
end)
end
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus). -- 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)
@@ -500,7 +561,7 @@ end
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører -- 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 -- 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). -- 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} ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
function ADCpBridge.vineOffsetFor(fruitName, role) function ADCpBridge.vineOffsetFor(fruitName, role)
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
if role == "harvester" then return 0 end if role == "harvester" then return 0 end
@@ -576,6 +637,7 @@ 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)
clampCourseWidth(vehicle)
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens -- kursusberegningen kører over mange frames på store marker: moden venter længere imens
local function generate(headlands) local function generate(headlands)
ADCpBridge.setHeadlands(vehicle, headlands) ADCpBridge.setHeadlands(vehicle, headlands)
+5
View File
@@ -110,7 +110,12 @@ function ADCpResume.save(adEnv)
end end
xmlFile:save() xmlFile:save()
xmlFile:delete() xmlFile:delete()
-- gemmes ved hver runs-gem (flere gange i minuttet): log kun når listen ændrer sig
local signature = table.concat(ids, ",")
if signature ~= ADCpResume.lastLogged then
ADCpResume.lastLogged = signature
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids) Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
end
return true return true
end end
+91
View File
@@ -0,0 +1,91 @@
-- AD Crop Plan (ren logik)
-- Mangel pr. afgrøde over 12 måneder (lager + forventet høst mod kædens behov) og valg af marker der skal skifte
-- målafgrøde: "fri til kæden" -> auto, ellers forslag. Brugerens egen målafgrøde ændres aldrig automatisk.
ADCropPlan = {}
ADCropPlan.HORIZON_MONTHS = 12
ADCropPlan.MIN_SHARE = 0.10
ADCropPlan.MIN_HA = 0.5
-- højst så mange forslag (ikke auto-skift) pr. afgrøde, så listen ikke vokser til snesevis af rækker
ADCropPlan.MAX_SUGGEST_PER_FRUIT = 3
function ADCropPlan.plannedFruit(field)
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
return field.fruit
end
function ADCropPlan.shortages(demand, stock, fields, yields)
local list = {}
for fruit, perMonth in pairs(demand or {}) do
local perHa = (yields or {})[fruit] or 0
if perHa > 0 and (perMonth or 0) > 0 then
local need = perMonth * ADCropPlan.HORIZON_MONTHS
local supply = (stock or {})[fruit] or 0
for _, field in ipairs(fields or {}) do
if ADCropPlan.plannedFruit(field) == fruit then supply = supply + (field.areaHa or 0) * perHa end
end
local missing = need - supply
local missingHa = missing / perHa
if missing > need * ADCropPlan.MIN_SHARE and missingHa > ADCropPlan.MIN_HA then
table.insert(list, {fruit = fruit, needLiters = need, supplyLiters = supply, missingLiters = missing, missingHa = missingHa})
end
end
end
table.sort(list, function(a, b)
if a.missingHa ~= b.missingHa then return a.missingHa > b.missingHa end
return a.fruit < b.fruit
end)
return list
end
local function isCandidate(field, used, dismissedFruit, fruit)
if field.permanent or field.excluded then return false end
local planned = ADCropPlan.plannedFruit(field)
if planned ~= nil and planned ~= "" and used[planned] then return false end
return dismissedFruit ~= fruit
end
-- prices: {[afgrøde] = pris} eller en funktion afgrøde -> pris (slås kun op for kandidaternes afgrøder).
-- harvesting = {[afgrøde] = true}: afgrøder der høstes lige nu — ingen forslag/skift for dem (læssene er på vej).
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed, harvesting)
local actions, taken = {}, {}
local used = usedFruits or {}
local priceOf = type(prices) == "function" and prices or function(fruit) return (prices or {})[fruit] or 0 end
local priceCache = {}
local function valueOf(fruit)
if fruit == "" then return 0 end
if priceCache[fruit] == nil then priceCache[fruit] = ((yields or {})[fruit] or 0) * (priceOf(fruit) or 0) end
return priceCache[fruit]
end
for _, shortage in ipairs(shortages or {}) do
local remaining = (harvesting or {})[shortage.fruit] and 0 or shortage.missingHa
local candidates = {}
for _, field in ipairs(fields or {}) do
if not taken[field.id] and isCandidate(field, used, (dismissed or {})[field.id], shortage.fruit) then
local planned = ADCropPlan.plannedFruit(field) or ""
local value = valueOf(planned)
table.insert(candidates, {field = field, value = value})
end
end
table.sort(candidates, function(a, b)
if a.value ~= b.value then return a.value < b.value end
local da, db = math.abs((a.field.areaHa or 0) - remaining), math.abs((b.field.areaHa or 0) - remaining)
if da ~= db then return da < db end
return a.field.id < b.field.id
end)
local suggested = 0
for _, candidate in ipairs(candidates) do
if remaining <= 0 then break end
local field = candidate.field
local auto = field.chainFree == true and field.targetSource ~= "user"
if auto or suggested < ADCropPlan.MAX_SUGGEST_PER_FRUIT then
table.insert(actions, {fieldId = field.id, from = ADCropPlan.plannedFruit(field) or "", to = shortage.fruit,
hectares = field.areaHa or 0, reason = shortage.fruit, mode = auto and "auto" or "suggest"})
taken[field.id] = true
remaining = remaining - (field.areaHa or 0)
if not auto then suggested = suggested + 1 end
end
end
end
return actions
end
+190
View File
@@ -0,0 +1,190 @@
-- AD Dependency (ren logik)
-- Advarsel når spilleren slukker en produktion gården er afhængig af (spec 2026-09-30-produktionskaeder afsnit 8).
-- En vare er "nødvendig" når et tændt forsyningsflow kører den (manuel vare, eller flowets mål tager den i
-- auto-tilstand), en stald æder den, eller en aktiv kæde har den som input. Varer er NAVNE; kapacitet og forbrug
-- i samme enhed (adapteren i adChain.lua vælger), lagerdage i spillets døgn. Ingen spiladgang.
ADDependency = {}
ADDependency.STOCK_DAYS_OK = 60 -- lager til mere end så mange døgn: ingen advarsel
ADDependency.MAX_USERS = 2 -- brugere nævnt i beskeden; resten "+k"
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
return false
end
local function sortedKeys(map)
local keys = {}
for key in pairs(map or {}) do table.insert(keys, key) end
table.sort(keys)
return keys
end
-- flows = {{name, kind, stopped, targetBuildingIds, ingredientMode, ingredients, chain}} (chain = kædens rodnøgle for kædens flows)
-- barnFood = {[vare] = {staldnavn...}}, chainInputs = {[vare] = {kædenavn...}}
-- accepts = {[bygnings-id] = {[vare] = true}}: hvad auto-flowenes mål tager (adapteren slår op)
-- -> {[vare] = {users = {navn...}, onlyChain, strong}} (brugere i rækkefølgen flows, stalde, kæder; hver kun én gang).
-- strong = varen er valgt som manuel vare i et flow eller er kæde-input: behovet gælder også uden kendt forbrug.
-- Stald-foder og auto-flowenes mål er "svage": uden kendt forbrug (lagerdage) er de ikke nødvendige.
function ADDependency.neededOutputs(flows, barnFood, chainInputs, accepts)
local result = {}
local function add(name, user, isChain, isStrong)
if name == nil or user == nil then return end
local entry = result[name]
if entry == nil then
entry = {users = {}, onlyChain = true, strong = false}
result[name] = entry
end
if not isChain then entry.onlyChain = false end
if isStrong then entry.strong = true end
if not contains(entry.users, user) then table.insert(entry.users, user) end
end
for _, flow in ipairs(flows or {}) do
if flow.kind ~= "outbound" and not flow.stopped then
local user = tostring(flow.name or "?")
-- kædens egne flows (flow.chain) er kæde-behov: Task 10 melder kæderne, så de gør ikke onlyChain falsk
local isChainFlow = flow.chain ~= nil and flow.chain ~= ""
if flow.ingredientMode == "manual" then
for _, name in ipairs(flow.ingredients or {}) do add(name, user, isChainFlow, true) end
else
for _, building in ipairs(flow.targetBuildingIds or {}) do
for _, name in ipairs(sortedKeys((accepts or {})[building])) do add(name, user, isChainFlow, isChainFlow) end
end
end
end
end
for _, name in ipairs(sortedKeys(barnFood)) do
for _, barn in ipairs(barnFood[name]) do add(name, tostring(barn), false) end
end
for _, name in ipairs(sortedKeys(chainInputs)) do
for _, root in ipairs(chainInputs[name]) do add(name, tostring(root), true, true) end
end
return result
end
-- Tændte produktioner der ikke er kædens (samme regel som ADChain.enabledKeys) -> {[nøgle] = true}
function ADDependency.enabledKeys(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
end
return result
end
-- Tændte produktioners kapacitet pr. vare (outputs × cyklusser pr. måned) -> {[vare] = mængde}
function ADDependency.capacity(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled then
for name, amount in pairs(recipe.outputs or {}) do
result[name] = (result[name] or 0) + (amount or 0) * (recipe.cyclesPerMonth or 0)
end
end
end
return result
end
-- Dækker andre tændte produktioner varen? Ukendt/intet forbrug: enhver kapacitet dækker.
local function isCovered(name, otherCapacity, demand)
local capacity = (otherCapacity or {})[name] or 0
return capacity > 0 and capacity >= ((demand or {})[name] or 0)
end
-- Rækker lageret længe nok? nil = ukendt (advar, uden dage).
local function stockLasts(name, stockDays)
local days = (stockDays or {})[name]
return days ~= nil and days > ADDependency.STOCK_DAYS_OK
end
-- {[bygning|vare] = true} for tændte opskrifter: spilleren har valgt hvilken opskrift i fabrikken der kører.
local function enabledInBuilding(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled then
for name, amount in pairs(recipe.outputs or {}) do
if (amount or 0) > 0 then result[tostring(recipe.building) .. "|" .. name] = true end
end
end
end
return result
end
-- Er behovet for varen reelt? Kun kæder: nej (Task 10 melder kæderne). Svagt behov (stald-foder, auto-flowets mål)
-- kræver kendt forbrug (lagerdage); manuel flow-vare og kæde-input gælder også uden.
local function isNeeded(need, name, stockDays)
if need == nil or need.onlyChain then return false end
return need.strong == true or (stockDays or {})[name] ~= nil
end
-- Første nødvendige, udækkede vare (efter navn) for en slukket produktion (ikke kædens) -> advarsel eller nil.
local function check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
if recipe.enabled or recipe.byChain then return nil end
for _, name in ipairs(sortedKeys(recipe.outputs)) do
local need = (needed or {})[name]
if isNeeded(need, name, stockDays) and (recipe.outputs[name] or 0) > 0
and not alternatives[tostring(recipe.building) .. "|" .. name]
and not isCovered(name, otherCapacity, demand) and not stockLasts(name, stockDays) then
local days = (stockDays or {})[name]
return {recipeKey = recipe.key, building = recipe.building, buildingName = recipe.buildingName, fillType = name,
users = need.users, onlyChain = need.onlyChain == true, days = days}
end
end
return nil
end
-- Alle slukkede, nødvendige produktioner (én pr. fabrik) — "Slukket men nødvendig" i Kræver handling.
function ADDependency.offNeeded(recipes, needed, stockDays, otherCapacity, demand)
local result, seen = {}, {}
local alternatives = enabledInBuilding(recipes)
for _, recipe in ipairs(recipes or {}) do
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
if warning ~= nil and not seen[tostring(warning.building)] then
seen[tostring(warning.building)] = true
table.insert(result, warning)
end
end
return result
end
-- Fælles regler (warnings og offNeeded): ingen advarsel når en anden opskrift i SAMME fabrik laver varen og er
-- tændt, når andre tændte produktioner dækker behovet, når lageret rækker > 60 dage, eller når kun kæder bruger varen.
-- Nye slukninger siden sidste evaluering: tændt i prevEnabled, slukket nu, nødvendig og ikke dækket.
-- prevEnabled = nil (første evaluering efter indlæsning) -> ingen. Én pr. fabrik og vare.
function ADDependency.warnings(prevEnabled, recipes, needed, stockDays, otherCapacity, demand)
local result, seen = {}, {}
if prevEnabled == nil then return result end
local alternatives = enabledInBuilding(recipes)
for _, recipe in ipairs(recipes or {}) do
if prevEnabled[recipe.key] then
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
local id = warning ~= nil and (tostring(warning.building) .. "|" .. warning.fillType) or nil
if warning ~= nil and not seen[id] then
seen[id] = true
table.insert(result, warning)
end
end
end
return result
end
-- "A, B +2"
function ADDependency.usersText(users)
local shown = {}
for index = 1, math.min(#(users or {}), ADDependency.MAX_USERS) do shown[index] = tostring(users[index]) end
if #shown == 0 then return "?" end
local text = table.concat(shown, ", ")
if #users > #shown then text = text .. " +" .. tostring(#users - #shown) end
return text
end
-- Beskedtekst (spu_dep_warn med dage, spu_dep_warnNoDays uden).
function ADDependency.notifyText(warning, textOf, titleOf)
local title = tostring(titleOf ~= nil and titleOf(warning.fillType) or warning.fillType)
local users = ADDependency.usersText(warning.users)
local building = tostring(warning.buildingName or warning.building or "?")
local days = warning.days
if days ~= nil and days < math.huge then
return string.format(textOf("spu_dep_warn"), building, title, users, math.max(0, math.floor(days)))
end
return string.format(textOf("spu_dep_warnNoDays"), building, title, users)
end
+4 -1
View File
@@ -146,7 +146,7 @@ end
local PLAN_VALUES = { local PLAN_VALUES = {
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true, targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true, fertilize = true, weed = true, straw = true, chainFree = true, soil = true, rigs = true, unloaders = true,
} }
function ADFieldGuide.choose(guide, id, value) function ADFieldGuide.choose(guide, id, value)
@@ -206,6 +206,7 @@ local function rowsCrop(guide)
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow)) table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
end end
table.insert(rows, toggle("chainFree", L("spu_field_chainFree"), plan.chainFree))
return rows return rows
end end
@@ -442,6 +443,8 @@ end
function ADFieldGuide.result(guide, start) function ADFieldGuide.result(guide, start)
local draft = guide.draft local draft = guide.draft
local plan = ADGuide.deepCopy(draft.plan) local plan = ADGuide.deepCopy(draft.plan)
-- kilden ejes af setTarget; kladdens gamle værdi må ikke overskrive den
plan.targetSource = nil
local t = plan.targetFruit or "" local t = plan.targetFruit or ""
if t ~= "" then if t ~= "" then
plan.mode = "auto" plan.mode = "auto"
+2 -2
View File
@@ -309,8 +309,8 @@ function ADFieldJobs.forceReady(id)
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id))) ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
end end
function ADFieldJobs.setTarget(id, fruitName) function ADFieldJobs.setTarget(id, fruitName, source)
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName)) ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName, source))
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen") ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
ADFieldJobs.save() ADFieldJobs.save()
end end
+17 -4
View File
@@ -43,6 +43,8 @@ local DEFAULT_PLAN = {
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false, mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop", fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
-- kæde: marken er frigivet til produktionskæden; hvem der valgte målafgrøden ("user"|"chain"|"")
chainFree = false, targetSource = "",
} }
local STRAW_VALUES = {chop = true, collect = true, bale = true} local STRAW_VALUES = {chop = true, collect = true, bale = true}
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges) -- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
@@ -158,9 +160,11 @@ end
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes -- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel. -- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
function ADFieldModel.setTarget(field, fruitName) function ADFieldModel.setTarget(field, fruitName, source)
local copy = copyField(field) local copy = copyField(field)
copy.plan.targetFruit = fruitName or "" copy.plan.targetFruit = fruitName or ""
-- kilden huskes kun mens der er et mål; standard er at spilleren valgte det
copy.plan.targetSource = copy.plan.targetFruit ~= "" and (source or "user") or ""
copy.held = nil copy.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
@@ -376,13 +380,18 @@ ADFieldModel.MIN_WORK_WIDTH = 1.5
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde -- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true} ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
-- Bredden kurset laves med: højst CP's maksimum.
function ADFieldModel.effectiveWidth(width)
if width == nil then return nil end
return math.min(width, ADFieldModel.CP_MAX_WIDTH)
end
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 if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
return false, "widthUnknown", {width} return false, "widthUnknown", {width}
end end
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then -- bredere end CP's 50 m (Unrealistic kan sættes til 120 m): bruges; kurset begrænses til 50 m (brugerønske 30/9)
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH} width = ADFieldModel.effectiveWidth(width)
end
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
return false, "tooNarrow", {width, requiredWidth} return false, "tooNarrow", {width, requiredWidth}
end end
@@ -463,6 +472,7 @@ function ADFieldModel.toRecord(field)
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll), soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
fertilize = flag(plan.fertilize), weed = flag(plan.weed), fertilize = flag(plan.fertilize), weed = flag(plan.weed),
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop", targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
chainFree = flag(plan.chainFree), targetSource = plan.targetSource or "",
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "), kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
mowWidth = field.mowWidth or 0, mowWidth = field.mowWidth or 0,
mowVehicle = field.mowVehicleId or "", mowVehicle = field.mowVehicleId or "",
@@ -503,6 +513,9 @@ function ADFieldModel.fromRecord(record)
plan.weed = readFlag(record.weed, true) plan.weed = readFlag(record.weed, true)
plan.targetFruit = record.targetFruit or "" plan.targetFruit = record.targetFruit or ""
plan.prepAfterMow = readFlag(record.prepAfterMow, false) plan.prepAfterMow = readFlag(record.prepAfterMow, false)
plan.chainFree = readFlag(record.chainFree, false)
-- gamle saves: et eksisterende mål er valgt af spilleren
plan.targetSource = record.targetSource or (plan.targetFruit ~= "" and "user" or "")
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop" plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
field.kind = readText(record.kind) field.kind = readText(record.kind)
field.lastFruit = readText(record.lastFruit) field.lastFruit = readText(record.lastFruit)
+1
View File
@@ -12,6 +12,7 @@ ADFieldStorage.FIELD_TYPES = {
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int", rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int", sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String", kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
chainFree = "Int", targetSource = "String",
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String", mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
dedicated = "String", dedicated = "String",
} }
+63 -5
View File
@@ -166,7 +166,7 @@ function ADFieldWork.getRigs(adEnv)
manual = (state.manual or {})[id] == true, 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 -- 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, roles = ADPool.rolesOf(role, specs.sprayRoles), 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
@@ -379,6 +379,10 @@ local function refillPlan(adEnv, fieldId, vehicle, role, x, z)
if fillType == nil then return nil end if fillType == nil then return nil end
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0 local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
if refillId ~= nil then
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refillId)
log(fieldId, "'%s' er tom: fyldes med %s ved %s", ADRuns.vehicleLabel(vehicle), fillTypeName(fillType), tostring(marker ~= nil and marker.name or refillId))
end
return {fillType = fillType, empty = empty, refillId = refillId} return {fillType = fillType, empty = empty, refillId = refillId}
end end
@@ -604,11 +608,65 @@ function ADFieldWork.buyingSupports(station, fillType)
return ok and result == true return ok and result == true
end end
-- AD-markøren ved stationen: AutoDrives eget opslag (30 m fra triggeren), ellers nærmeste ikke-Wait -- Parkering, kobling og ventepladser er aldrig en læssemarkør.
-- markør inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren). local function isServiceMarker(marker)
local name = string.lower(tostring(marker.name or ""))
local group = string.lower(tostring(marker.group or ""))
return string.find(name, "^p ") ~= nil or string.find(name, "^connect ") ~= nil
or string.find(name, "%f[%a]wait%f[%A]") ~= nil or string.find(group, "%f[%a]wait%f[%A]") ~= nil
end
-- markers = {{id, name, group, x, z}}, points = triggerpositioner {{x, z}} -> id på nærmeste læssemarkør inden for
-- maxDistance m af en af triggerne, ellers nil.
function ADFieldWork.nearestStationMarker(markers, points, maxDistance)
local best, bestDistance = nil, maxDistance * maxDistance
for _, marker in ipairs(markers or {}) do
if not isServiceMarker(marker) then
for _, point in ipairs(points or {}) do
local d = (marker.x - point.x) ^ 2 + (marker.z - point.z) ^ 2
if d < bestDistance then best, bestDistance = marker.id, d end
end
end
end
return best
end
ADFieldWork.STATION_MARKER_M = 30
-- Stationens læssetriggere i verden (buying-/loading-station) -> {{x, z}}.
local function loadTriggerPoints(station)
local points = {}
for _, trigger in pairs(station.loadTriggers or {}) do
local node = trigger.triggerNode or trigger.aiNode
if node ~= nil then
local ok, x, _, z = pcall(getWorldTranslation, node)
if ok and x ~= nil then table.insert(points, {x = x, z = z}) end
end
end
return points
end
local function markerList(adEnv)
local list = {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
if wayPoint ~= nil then
table.insert(list, {id = marker.id, name = marker.name, group = marker.group, x = wayPoint.x, z = wayPoint.z})
end
end
return list
end
-- AD-markøren ved stationen: den nærmeste markør ved stationens EGNE læssetriggere, ellers nærmeste ikke-Wait markør
-- inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren). AutoDrives
-- ADTriggerManager:getMarkerAtStation bruges IKKE: den vil have markøren togets længde + 3..13 m forbi triggeren (et
-- langt sæt fik en anden stations markør: Puma + Kinze 4900 ventede på frø ved "Market Price Liquid Fill Station"), og
-- finder den ingen, OPRETTER den en ny markør med stationens navn (2 ekstra "Buying Station Multifruit", in-game 30/9).
local function markerAtStation(adEnv, vehicle, station, placeable) local function markerAtStation(adEnv, vehicle, station, placeable)
local okAd, wayPointId = pcall(function() return adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 30) end) local okOwn, ownId = pcall(function()
if okAd and wayPointId ~= nil and wayPointId > 0 then return wayPointId end return ADFieldWork.nearestStationMarker(markerList(adEnv), loadTriggerPoints(station), ADFieldWork.STATION_MARKER_M)
end)
if okOwn and ownId ~= nil then return ownId end
local node = placeable ~= nil and placeable.rootNode or nil local node = placeable ~= nil and placeable.rootNode or nil
if node == nil then return nil end if node == nil then return nil end
local px, _, pz = getWorldTranslation(node) local px, _, pz = getWorldTranslation(node)
+31 -2
View File
@@ -10,6 +10,9 @@ ADFields.MARKER_MAX_DISTANCE = 400
ADFields.cache = {} ADFields.cache = {}
ADFields.order = {} ADFields.order = {}
ADFields.scanIndex = 0 ADFields.scanIndex = 0
-- {[markId] = true}: marker hvis scanning er afsluttet (med resultat) eller fejlede — til isScanComplete
ADFields.scanFailed = {}
ADFields.ownedDone = false
ADFields.mowable = nil ADFields.mowable = nil
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan) -- 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.OWNED_REFRESH_MS = 30000
@@ -104,7 +107,7 @@ end
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen -- 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. -- (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} ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar? -- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
function ADFields.isMowableFruit(fruitName, mowableEntry) function ADFields.isMowableFruit(fruitName, mowableEntry)
@@ -291,6 +294,17 @@ function ADFields.shapeSignature(verts)
roundTo(cx, 1), roundTo(cz, 1)) roundTo(cx, 1), roundTo(cz, 1))
end end
-- Markens areal i hektar (shoelace på {x,z}-polygonen, /10000); 0 uden en flade (under 3 hjørner).
function ADFields.polygonAreaHa(verts)
if type(verts) ~= "table" or #verts < 3 then return 0 end
local n, twiceArea = #verts, 0
for index = 1, n do
local a, b = verts[index], verts[index % n + 1]
twiceArea = twiceArea + a.x * b.z - b.x * a.z
end
return math.abs(twiceArea) / 2 / 10000
end
-- Hjørnernes gennemsnit (nil uden hjørner). -- Hjørnernes gennemsnit (nil uden hjørner).
function ADFields.polygonCenter(verts) function ADFields.polygonCenter(verts)
if verts == nil or #verts == 0 then return nil end if verts == nil or #verts == 0 then return nil end
@@ -555,9 +569,20 @@ local function finishOwnedScan(scan)
if not ids[id] then ADFields.cache[id] = nil end if not ids[id] then ADFields.cache[id] = nil end
end end
ADFields.order = scan.result ADFields.order = scan.result
ADFields.ownedDone = true
ADFields.listCache = nil ADFields.listCache = nil
end end
-- Har scanningen nået alle ejede marker mindst én gang (resultat i cachen, eller scanningen fejlede)?
-- Indtil da er marklisten ufuldstændig (efter indlæsning fyldes den ~én mark pr. SCAN_INTERVAL_MS).
function ADFields.isScanComplete()
if not ADFields.ownedDone then return false end
for _, entry in ipairs(ADFields.order) do
if ADFields.cache[entry.id] == nil and not ADFields.scanFailed[entry.id] then return false end
end
return true
end
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert -- 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. -- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
ADFields.SCAN_INTERVAL_MS = 2000 ADFields.SCAN_INTERVAL_MS = 2000
@@ -601,6 +626,7 @@ function ADFields.scanStep(adEnv, farmId, now)
local ok, err = pcall(task) local ok, err = pcall(task)
if not ok then if not ok then
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err)) Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
ADFields.activeScan = nil ADFields.activeScan = nil
return return
end end
@@ -617,7 +643,10 @@ function ADFields.scanStep(adEnv, farmId, now)
else else
ok, err = pcall(finishScan, adEnv, scan) ok, err = pcall(finishScan, adEnv, scan)
end end
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end if not ok then
Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err))
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
end
end end
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang). -- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
+8 -3
View File
@@ -69,7 +69,7 @@ end
local RUN_FIELDS = { local RUN_FIELDS = {
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products", "id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops", "ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons", "returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons", "chain",
} }
local function draftFrom(run, isNew) local function draftFrom(run, isNew)
@@ -339,7 +339,12 @@ local function rowsRules(guide)
if isOut(guide) then if isOut(guide) then
local sells, stores = ADFlowView.policyUse(productPolicies(draft)) local sells, stores = ADFlowView.policyUse(productPolicies(draft))
local rows = {} local rows = {}
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end if sells then
-- "Altid" (0) øverst: sælg uanset pris (in-game 30/9)
local options = {{label = L("spu_sellAt_always"), value = 0}}
for _, option in ipairs(percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5)) do table.insert(options, option) end
table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), options, draft.sellAtPercent))
end
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound)) table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
@@ -489,7 +494,7 @@ local SUMMARY = {
if isOut(guide) then if isOut(guide) then
local sells, stores = ADFlowView.policyUse(productPolicies(draft)) local sells, stores = ADFlowView.policyUse(productPolicies(draft))
local parts = {} local parts = {}
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end if sells then table.insert(parts, draft.sellAtPercent == 0 and L("spu_guide_flow_sumSellAlways") or L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
+42 -1
View File
@@ -52,20 +52,61 @@ end
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn. -- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
-- {{kind, runs}, ...}; tomme grupper udelades. -- {{kind, runs}, ...}; tomme grupper udelades.
-- Spillerens egne flows først (forsyning, udkørsel), så kædernes egne flows (run.chain) samlet til sidst
-- (in-game 30/9: "Kæde: …"-flows blandede sig alfabetisk ind mellem de flows spilleren selv har sat op).
function ADFlowView.boardGroups(runs) function ADFlowView.boardGroups(runs)
local byKind = {supply = {}, outbound = {}} local byKind = {supply = {}, outbound = {}}
local chainRuns = {}
for _, run in ipairs(runs or {}) do for _, run in ipairs(runs or {}) do
if run.chain ~= nil and run.chain ~= "" then
table.insert(chainRuns, run)
else
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run) table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
end end
end
local groups = {} local groups = {}
for _, kind in ipairs({"supply", "outbound"}) do for _, kind in ipairs({"supply", "outbound"}) do
if #byKind[kind] > 0 then if #byKind[kind] > 0 then
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")}) table.insert(groups, {kind = kind, chain = false, runs = sortedBy(byKind[kind], "name")})
end end
end end
if #chainRuns > 0 then table.insert(groups, {kind = "supply", chain = true, runs = sortedBy(chainRuns, "name")}) end
return groups return groups
end end
-- Kort maskinnavn til lister: " Unrealistic" og parenteser som "(+ Custom Crops)" fjernes (lange navne blev ombrudt
-- med traktorens nummer alene på næste linje, in-game 30/9). Bliver intet tilbage, bruges det fulde navn.
function ADFlowView.shortVehicleName(name)
local full = tostring(name or "")
local short = full:gsub("%s*%b()", ""):gsub("%s+[Uu]nrealistic", ""):gsub("^%s+", ""):gsub("%s+$", "")
if short == "" then return full end
return short
end
-- Kædeflow uden traktor (venter på en ledig traktor/vogn).
function ADFlowView.isIdleChain(run)
return run.chain ~= nil and run.chain ~= "" and #(run.vehicleIds or {}) == 0
end
-- Kædeflows uden traktor foldes sammen i listen (de venter på en ledig traktor/vogn). -> active, idle
function ADFlowView.splitIdle(runs)
local active, idle = {}, {}
for _, run in ipairs(runs or {}) do
table.insert(#(run.vehicleIds or {}) > 0 and active or idle, run)
end
return active, idle
end
-- Navn i listen: kædeflows står under overskriften "Kædernes egne flows", så "Kæde: " fjernes (ellers skjuler
-- afkortningen hvilken bygning det gælder).
function ADFlowView.listName(run)
local name = tostring(run.name or "")
if run.chain ~= nil and run.chain ~= "" then
name = name:gsub("^Kæde:%s*", ""):gsub("^Chain:%s*", "")
end
return name
end
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren). -- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
function ADFlowView.orderedRuns(runs) function ADFlowView.orderedRuns(runs)
local result = {} local result = {}
+22 -2
View File
@@ -23,6 +23,12 @@ function ADFlowWagons.remember(wagons, tractorId, wagonId)
return copy return copy
end end
-- Må redskabet bag traktoren huskes som flowets vogn? Kun noget der kan rumme last — ikke plov/kultivator (ingen
-- plads) og ikke såmaskine (frøtank). s = {capacity, isSowingMachine}. in-game 30/9: 'Sell Slurry 2' huskede ploven.
function ADFlowWagons.isRememberableWagon(s)
return (s.capacity or 0) > 0 and not s.isSowingMachine
end
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait" -- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
function ADFlowWagons.startAction(s) function ADFlowWagons.startAction(s)
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri -- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
@@ -453,11 +459,16 @@ end
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows). -- 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. -- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
local function isCargoWagon(object)
local _, capacity = ADPool.cargoOf(object)
return ADFlowWagons.isRememberableWagon({capacity = capacity, isSowingMachine = object.spec_sowingMachine ~= nil})
end
local function rememberWagonOf(vehicle) local function rememberWagonOf(vehicle)
local run = ADRunsController.getRunForVehicle(vehicle) local run = ADRunsController.getRunForVehicle(vehicle)
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {} local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
local tractorId = ADRunsController.getVehicleId(vehicle) local tractorId = ADRunsController.getVehicleId(vehicle)
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
if wagonId == nil or tractorId == nil then return end if wagonId == nil or tractorId == nil then return end
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId) local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
if updated ~= run.wagons then if updated ~= run.wagons then
@@ -551,6 +562,15 @@ function ADFlowWagons.isEmptying(vehicle)
return emptying[vehicle] ~= nil return emptying[vehicle] ~= nil
end end
-- Kun læsning (statusvinduet): hvad flowets vogn-logik har traktoren i gang med -> "fetching"|"waiting"|"emptying"|"homing"|nil
function ADFlowWagons.statusOf(vehicle)
if fetching[vehicle] ~= nil then return "fetching" end
if waiting[vehicle] ~= nil then return "waiting" end
if emptying[vehicle] ~= nil then return "emptying" end
if homing[vehicle] ~= nil then return "homing" end
return nil
end
function ADFlowWagons.sendHome(adEnv, run) function ADFlowWagons.sendHome(adEnv, run)
for _, vehicleId in ipairs(run.vehicleIds or {}) do for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
@@ -615,7 +635,7 @@ local function rememberAll(adEnv)
for _, vehicleId in ipairs(run.vehicleIds or {}) do for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {} local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
if wagonId ~= nil then if wagonId ~= nil then
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId) local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
if updated ~= run.wagons then if updated ~= run.wagons then
+132
View File
@@ -0,0 +1,132 @@
-- AD Load Diag
-- Diagnose når AutoDrive står ved et LOAD-mål uden at fylde (in-game 30/9: Fendt + MEGA-sprøjte ved "Market Price
-- Liquid Fill Station"). AutoDrive fylder kun, når en fyldenhed står i triggerens boks (trigger.fillableObjects) og
-- triggeren har varen. Efter REPORT_MS ved målet uden fremgang skrives én linje: markør, vare, og for hver vogn den
-- nærmeste AD-læssetrigger (afstand, har varen?, står vognen i boksen?). Kun når en trigger med varen er inden for
-- NEAR_M (holder ved Wait-punkter logges ikke). Ingen indgreb — kun log.
ADLoadDiag = {}
ADLoadDiag.LOG_PREFIX = "ADSmartPickup: pålæsning: "
ADLoadDiag.REPORT_MS = 30000
ADLoadDiag.NEAR_M = 20 -- m: trigger med varen herinden for = køretøjet står ved stationen (ellers Wait/mark: ingen log)
-- ---- ren logik ----
-- state (pr. opgave), dt, reached (ved målet), level (liter på vognene) -> true når der skal rapporteres (én gang pr.
-- ankomst). Fremgang eller kørsel nulstiller.
function ADLoadDiag.tick(state, dt, reached, level)
if not reached then
state.ms, state.reported, state.level = 0, false, nil
return false
end
if state.level == nil or level > state.level then
state.level, state.ms = level, 0
return false
end
state.ms = (state.ms or 0) + (dt or 0)
if state.reported or state.ms < ADLoadDiag.REPORT_MS then return false end
state.reported = true
return true
end
-- distances = afstande fra vognene til nærmeste AD-trigger med varen -> står ved stationen?
function ADLoadDiag.atStation(distances)
for _, d in ipairs(distances or {}) do
if d <= ADLoadDiag.NEAR_M then return true end
end
return false
end
-- ---- spil ----
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and fillType ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
local function inBox(trigger, trailer)
for key, entry in pairs(trigger.fillableObjects or {}) do
if key == trailer or (type(entry) == "table" and entry.object == trailer) then return true end
end
return false
end
local function describeTrailer(adEnv, trailer, fillType)
local node = trailer.components ~= nil and trailer.components[1] ~= nil and trailer.components[1].node or trailer.rootNode
if node == nil then return nil end
local tx, _, tz = getWorldTranslation(node)
local nearest, nearestOk = nil, nil
for _, trigger in pairs(adEnv.ADTriggerManager.getLoadTriggers() or {}) do
local x, _, z = adEnv.ADTriggerManager.getTriggerPos(trigger)
if x ~= nil and (x ~= 0 or z ~= 0) then
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
local ok = adEnv.ADTriggerManager.triggerSupportsFillType(trigger, fillType)
if nearest == nil or d < nearest.d then nearest = {d = d, ok = ok, trigger = trigger} end
if ok and (nearestOk == nil or d < nearestOk.d) then nearestOk = {d = d, trigger = trigger} end
end
end
local name = trailer.getName ~= nil and trailer:getName() or "?"
local parts = {}
if nearest ~= nil then
table.insert(parts, string.format("nærmeste AD-trigger %.1f m (%s varen, %s)", nearest.d, nearest.ok and "har" or "UDEN",
inBox(nearest.trigger, trailer) and "vognen i boksen" or "vognen IKKE i boksen"))
end
if nearestOk ~= nil and (nearest == nil or nearestOk.trigger ~= nearest.trigger) then
table.insert(parts, string.format("nærmeste med varen %.1f m (%s)", nearestOk.d,
inBox(nearestOk.trigger, trailer) and "i boksen" or "ikke i boksen"))
end
if nearestOk == nil then table.insert(parts, "INGEN AD-læssetrigger med varen") end
return string.format("'%s': %s", tostring(name), table.concat(parts, ", ")), nearestOk ~= nil and nearestOk.d or nil
end
local function report(adEnv, task)
local vehicle = task.vehicle
local fillType = vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
local marker = task.destinationID ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(task.destinationID) or nil
local lines, distances = {}, {}
for _, trailer in ipairs(task.trailers or {}) do
if trailer.getFillUnits ~= nil and #trailer:getFillUnits() > 0 then
local ok, line, distance = pcall(describeTrailer, adEnv, trailer, fillType)
if ok and line ~= nil then table.insert(lines, line) end
if ok and distance ~= nil then table.insert(distances, distance) end
end
end
-- holder ved Wait/mark (ingen trigger med varen tæt på): det er ikke en fejlet pålæsning
if not ADLoadDiag.atStation(distances) then return end
local maxDistance = adEnv.AutoDrive.getMaxTriggerDistance ~= nil and adEnv.AutoDrive.getMaxTriggerDistance(vehicle) or nil
Logging.warning(ADLoadDiag.LOG_PREFIX .. "'%s' står ved '%s' uden at fylde %s i %d s (AD-triggerafstand %s m): %s",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker ~= nil and marker.name or task.destinationID), fillTypeName(fillType),
math.floor(ADLoadDiag.REPORT_MS / 1000), tostring(maxDistance), #lines > 0 and table.concat(lines, "; ") or "ingen vogne med fyldenheder")
end
local states = setmetatable({}, {__mode = "k"})
function ADLoadDiag.after(adEnv, task, dt)
local vehicle = task.vehicle
if vehicle == nil or vehicle.ad == nil or vehicle.ad.drivePathModule == nil then return end
local state = states[task]
if state == nil then
state = {}
states[task] = state
end
local reached = vehicle.ad.drivePathModule:isTargetReached() and not vehicle.ad.trailerModule:isActiveAtTrigger()
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
if ADLoadDiag.tick(state, dt, reached, level) then report(adEnv, task) end
end
local isInstalled = false
function ADLoadDiag.install(adEnv)
local task = adEnv ~= nil and adEnv.LoadAtDestinationTask or nil
if isInstalled or task == nil or task.update == nil or adEnv.ADTriggerManager == nil then return end
isInstalled = true
local original = task.update
task.update = function(self, dt)
local result = original(self, dt)
local ok, err = pcall(ADLoadDiag.after, adEnv, self, dt)
if not ok and not self.spuDiagFailed then
self.spuDiagFailed = true
Logging.warning(ADLoadDiag.LOG_PREFIX .. "diagnose fejlede: %s", tostring(err))
end
return result
end
end
+9 -1
View File
@@ -660,6 +660,13 @@ end
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én -- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn. -- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
-- Palleskur: moden tager ét objekt ud ad gangen og afslutter selv pålæsningen (fuld / tomt lager / 30 s uden
-- udlevering). Imens holdes AD's egen pålæsning, ellers kører AD, så snart triggeren et øjeblik er tom (in-game 30/9:
-- Sell Egg kørte halvfuld). Først når AD har aktiveret UAL (det gør AD selv ved ankomst).
function ADOutbound.holdForSpawn(sourceKind, done, ualActive)
return sourceKind == "objectStorage" and not done and ualActive == true
end
local function updateSpawn(adEnv, state, task, source, dt) local function updateSpawn(adEnv, state, task, source, dt)
local vehicle = task.vehicle local vehicle = task.vehicle
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den. -- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
@@ -676,7 +683,7 @@ local function updateSpawn(adEnv, state, task, source, dt)
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
spawn.pending = nil spawn.pending = nil
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType))) warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift: lager %d -> %d, vogn %d -> %d objekter); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType), spawn.pending.stored, stored, spawn.pending.loaded, loaded))
state.done = true state.done = true
return return
else else
@@ -763,6 +770,7 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
task:finished() task:finished()
return true return true
end end
if ADOutbound.holdForSpawn(source.kind, state.done, task.activatedUALLoading) then return true end
end end
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0 local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
if state.lastFill == nil or level > state.lastFill then if state.lastFill == nil or level > state.lastFill then
+2 -1
View File
@@ -67,7 +67,8 @@ function ADOutboundPlanner.evaluate(product, input)
storagePercent = storagePercent, storagePercent = storagePercent,
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), -- sælg fra 0 % = "Altid": uanset pris (også uden prisdata)
priceOk = input.sellAtPercent == 0 or (percent ~= nil and percent >= (input.sellAtPercent or 90)),
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris -- "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, overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
} }
+8 -1
View File
@@ -18,6 +18,13 @@ function ADPool.roleOf(specs)
return role return role
end end
-- Ekstra roller for et redskab: sprøjtens roller (gødning/kalk/ukrudt) — men ikke for en såmaskine med
-- gødningstank (Kinze 4905), den er kun såmaskine (in-game 30/9: hentet til ren gødning).
function ADPool.rolesOf(role, sprayRoles)
if role ~= "seeder" and sprayRoles ~= nil then return sprayRoles end
return ADFieldFleet.rolesFor(role)
end
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil. -- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
function ADPool.numberOf(value) function ADPool.numberOf(value)
if type(value) == "number" then return value end if type(value) == "number" then return value end
@@ -402,7 +409,7 @@ function ADPool.collect(adEnv, isBusy)
local carries, capacity = ADPool.cargoOf(vehicle) local carries, capacity = ADPool.cargoOf(vehicle)
table.insert(implements, {carries = carries, capacity = capacity, table.insert(implements, {carries = carries, capacity = capacity,
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), 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), role = role, roles = ADPool.rolesOf(role, specs.sprayRoles), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id, neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
}) })
end end
+2
View File
@@ -110,6 +110,8 @@ function ADProfiler.install()
{ADSources, "getFarmInventory", "gårdens lagre"}, {ADSources, "getFarmInventory", "gårdens lagre"},
{ADWaitPool, "hasRouteTo", "rutetjek"}, {ADWaitPool, "hasRouteTo", "rutetjek"},
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"}, {ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
{ADChain, "evaluate", "kæde"},
{ADChainData, "collect", "kæde: læs gården"},
} }
for _, target in ipairs(targets) do for _, target in ipairs(targets) do
ADProfiler.wrap(target[1], target[2], target[3]) ADProfiler.wrap(target[1], target[2], target[3])
+8 -2
View File
@@ -97,6 +97,7 @@ local function copyRun(run)
vehicleIds = copyList(run.vehicleIds), vehicleIds = copyList(run.vehicleIds),
stopped = run.stopped == true, stopped = run.stopped == true,
wagons = ADRuns.copyWagons(run.wagons), wagons = ADRuns.copyWagons(run.wagons),
chain = run.chain or "",
} }
end end
@@ -145,6 +146,7 @@ function ADRuns.createRun(store, name, kind)
loops = 0, loops = 0,
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT, returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
vehicleIds = {}, vehicleIds = {},
chain = "",
} }
new.nextId = new.nextId + 1 new.nextId = new.nextId + 1
table.insert(new.runs, run) table.insert(new.runs, run)
@@ -200,10 +202,12 @@ function ADRuns.updateRun(store, runId, changes)
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end
if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end
if changes.products ~= nil then run.products = copyProducts(changes.products) end if changes.products ~= nil then run.products = copyProducts(changes.products) end
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end -- 0 = "Altid" (sælg uanset pris); ellers 50-100 %
if changes.sellAtPercent ~= nil then run.sellAtPercent = tonumber(changes.sellAtPercent) == 0 and 0 or clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end if changes.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.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
if changes.chain ~= nil then run.chain = tostring(changes.chain) end
return new return new
end end
@@ -440,6 +444,7 @@ function ADRuns.toRecords(store)
collectRound = run.collectRound and 1 or 0, collectRound = run.collectRound and 1 or 0,
stopped = run.stopped and 1 or 0, stopped = run.stopped and 1 or 0,
wagons = ADRuns.serializeWagons(run.wagons), wagons = ADRuns.serializeWagons(run.wagons),
chain = run.chain or "",
} }
end end
return records return records
@@ -480,7 +485,7 @@ function ADRuns.fromRecords(records)
targetBuildingIds = parseIds(record.targetBuildings), targetBuildingIds = parseIds(record.targetBuildings),
storeBuildingIds = parseIds(record.storeBuildings), storeBuildingIds = parseIds(record.storeBuildings),
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 = tonumber(record.sellAt) == 0 and 0 or clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT), 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), 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,
@@ -494,6 +499,7 @@ function ADRuns.fromRecords(records)
vehicleIds = copyList(record.vehicleIds), vehicleIds = copyList(record.vehicleIds),
stopped = tonumber(record.stopped) == 1, stopped = tonumber(record.stopped) == 1,
wagons = ADRuns.parseWagons(record.wagons), wagons = ADRuns.parseWagons(record.wagons),
chain = record.chain or "",
}) })
store.nextId = math.max(store.nextId, id + 1) store.nextId = math.max(store.nextId, id + 1)
end end
+5
View File
@@ -1476,6 +1476,9 @@ function ADRunsController.save()
pcall(ADFieldJobs.save) pcall(ADFieldJobs.save)
end end
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
-- kædens tilstand (tændte mellemled, pausede kæder, afviste forslag) — samme gem-regel som navnefilen
if ADChain ~= nil and ADChain.save ~= nil then pcall(ADChain.save) end
if ADSellBlock ~= nil and ADSellBlock.save ~= nil then pcall(ADSellBlock.save) end
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver -- 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) -- 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 if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
@@ -1495,6 +1498,8 @@ function ADRunsController.load(adEnv)
if ADNames ~= nil then pcall(ADNames.load) end 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 ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
if ADChain ~= nil and ADChain.load ~= nil then pcall(ADChain.load) end
if ADSellBlock ~= nil and ADSellBlock.load ~= nil then pcall(ADSellBlock.load) end
if ADCpResume ~= nil then pcall(ADCpResume.load) end 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
+2
View File
@@ -51,6 +51,7 @@ function ADRunsStorage.writeRecords(xmlFile, records)
-- maskinpulje: flowet stoppet + husket vogn pr. traktor -- maskinpulje: flowet stoppet + husket vogn pr. traktor
xmlFile:setInt(key .. "#stopped", record.stopped or 0) xmlFile:setInt(key .. "#stopped", record.stopped or 0)
xmlFile:setString(key .. "#wagons", record.wagons or "") xmlFile:setString(key .. "#wagons", record.wagons or "")
xmlFile:setString(key .. "#chain", record.chain or "")
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId) xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
end end
@@ -82,6 +83,7 @@ function ADRunsStorage.readRecords(xmlFile)
collectRound = xmlFile:getInt(key .. "#collectRound"), collectRound = xmlFile:getInt(key .. "#collectRound"),
stopped = xmlFile:getInt(key .. "#stopped"), stopped = xmlFile:getInt(key .. "#stopped"),
wagons = xmlFile:getString(key .. "#wagons"), wagons = xmlFile:getString(key .. "#wagons"),
chain = xmlFile:getString(key .. "#chain"),
vehicleIds = {}, vehicleIds = {},
} }
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey) xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
+154
View File
@@ -0,0 +1,154 @@
-- AD Sell Block
-- Salgssteder spilleren har slået fra (in-game 30/9: "Market Price Liquid Fill Station" fra FarmFillStations gik i stå
-- ved aflæsning af gylle). Konsol: spuSellBlock viser salgsstederne; spuSellBlock <del af navn> slår dem fra/til.
-- Udkørsel (ADSources.findSellMarkers) og priser (ADSources.getMarketQuote) springer udelukkede over. Gemmes pr.
-- savegame i ADSmartPickup_sellblock.xml (skrives fra ADRunsController.save, som de andre filer).
ADSellBlock = {}
ADSellBlock.FILE_NAME = "ADSmartPickup_sellblock.xml"
ADSellBlock.ROOT = "sellBlock"
ADSellBlock.blocked = {} -- {[stationId] = visningsnavn}
-- ---- ren logik ----
-- stations = {{id, names = {...}}}, query = del af et navn (små/store bogstaver ligegyldige) -> matchende stationer
function ADSellBlock.match(stations, query)
local wanted = string.lower(tostring(query or ""))
local hits = {}
if wanted == "" then return hits end
for _, station in ipairs(stations or {}) do
for _, name in ipairs(station.names or {}) do
if string.find(string.lower(tostring(name)), wanted, 1, true) ~= nil then
table.insert(hits, station)
break
end
end
end
return hits
end
-- Ny tabel: hver station skiftes (fra -> til, til -> fra). Første navn huskes til visning.
function ADSellBlock.toggle(blocked, stations)
local result = {}
for id, name in pairs(blocked or {}) do result[id] = name end
for _, station in ipairs(stations or {}) do
if result[station.id] ~= nil then
result[station.id] = nil
else
result[station.id] = tostring((station.names or {})[1] or station.id)
end
end
return result
end
-- ---- spil ----
local function stationId(station)
local placeable = station ~= nil and station.owningPlaceable or nil
if placeable ~= nil and placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
return nil
end
function ADSellBlock.isBlocked(station)
local id = stationId(station)
return id ~= nil and ADSellBlock.blocked[id] ~= nil
end
local function stationNames(station)
local names = {}
local placeable = station.owningPlaceable
if placeable ~= nil and placeable.getName ~= nil then
local ok, name = pcall(placeable.getName, placeable)
if ok and name ~= nil then table.insert(names, name) end
end
if station.getName ~= nil then
local ok, name = pcall(station.getName, station)
if ok and name ~= nil then table.insert(names, name) end
end
return names
end
-- Alle salgssteder med AD-markør -> {{id, names}} (markørens navn først: det spilleren ser i AutoDrive).
local function sellStations(adEnv)
local list, byId = {}, {}
local farmId = g_currentMission:getFarmId()
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
local id = station ~= nil and ADPrices.isSellingStation(station) and stationId(station) or nil
if id ~= nil then
if byId[id] == nil then
byId[id] = {id = id, names = {}}
table.insert(list, byId[id])
for _, name in ipairs(stationNames(station)) do table.insert(byId[id].names, name) end
end
table.insert(byId[id].names, 1, marker.name)
end
end
table.sort(list, function(a, b) return tostring(a.names[1]) < tostring(b.names[1]) end)
return list
end
function ADSellBlock:consoleSellBlock(...)
local query = table.concat({...}, " ")
local adEnv = FS25_AutoDrive
if adEnv == nil or adEnv.ADGraphManager == nil then return "AutoDrive ikke fundet" end
local stations = sellStations(adEnv)
if query == "" then
local lines = {"Salgssteder (spuSellBlock <del af navn> slår fra/til):"}
for _, station in ipairs(stations) do
table.insert(lines, string.format(" %s %s", ADSellBlock.blocked[station.id] ~= nil and "[FRA]" or "[til]", table.concat(station.names, " / ")))
end
return table.concat(lines, "\n")
end
local hits = ADSellBlock.match(stations, query)
if #hits == 0 then return string.format("Intet salgssted matcher '%s' (spuSellBlock uden navn viser listen)", query) end
ADSellBlock.blocked = ADSellBlock.toggle(ADSellBlock.blocked, hits)
local lines = {}
for _, station in ipairs(hits) do
local text = string.format("%s: %s", station.names[1], ADSellBlock.blocked[station.id] ~= nil and "slået FRA som salgssted" or "bruges igen")
table.insert(lines, text)
Logging.info("ADSmartPickup: salgssted " .. text)
end
if ADRunsController ~= nil and ADRunsController.save ~= nil then pcall(ADRunsController.save) end
return table.concat(lines, "\n")
end
local function getPath()
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
if path == nil then return nil end
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADSellBlock.FILE_NAME))
end
function ADSellBlock.save()
local path = getPath()
if path == nil then return false end
local xmlFile = XMLFile.create("ADSmartPickupSellBlock", path, ADSellBlock.ROOT)
if xmlFile == nil then return false end
local index = 0
for id, name in pairs(ADSellBlock.blocked) do
local key = string.format("%s.station(%d)", ADSellBlock.ROOT, index)
xmlFile:setString(key .. "#id", id)
xmlFile:setString(key .. "#name", name)
index = index + 1
end
xmlFile:save()
xmlFile:delete()
return true
end
function ADSellBlock.load()
ADSellBlock.blocked = {}
local path = getPath()
if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupSellBlock", path)
if xmlFile == nil then return end
xmlFile:iterate(ADSellBlock.ROOT .. ".station", function(_, key)
local id = xmlFile:getString(key .. "#id")
if id ~= nil then ADSellBlock.blocked[id] = xmlFile:getString(key .. "#name") or id end
end)
xmlFile:delete()
end
if addConsoleCommand ~= nil then
addConsoleCommand("spuSellBlock", "Salgssteder: uden navn = liste; spuSellBlock <del af navn> slår fra/til", "consoleSellBlock", ADSellBlock)
end
+20
View File
@@ -28,6 +28,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupChainTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua") source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
@@ -1151,6 +1152,9 @@ local function installHook(adEnv)
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
ADHitchCreep.install(adEnv) ADHitchCreep.install(adEnv)
end end
if ADLoadDiag ~= nil and ADLoadDiag.install ~= nil then
ADLoadDiag.install(adEnv)
end
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)") 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
@@ -1309,6 +1313,12 @@ function ADSmartPickup:update(dt)
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos)) Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
end end
end end
if ADChain ~= nil then
local okChain, errChain = pcall(ADChain.tick, getAutoDriveEnv(), dt)
if not okChain then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kæde fejlede: %s", tostring(errChain))
end
end
if ADFlowWagons ~= nil then if ADFlowWagons ~= nil then
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt) local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
@@ -1328,6 +1338,16 @@ function ADSmartPickup:update(dt)
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err)) Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
supplyState = setmetatable({}, {__mode = "k"}) supplyState = setmetatable({}, {__mode = "k"})
end end
if ADStatusHud ~= nil then pcall(ADStatusHud.update, dt, getAutoDriveEnv()) end
end
-- Statusvinduet på skærmen (adStatusHud.lua): tegner kun den cachede tekst.
function ADSmartPickup:draw()
if ADStatusHud ~= nil then pcall(ADStatusHud.draw) end
end
function ADSmartPickup:deleteMap()
if ADStatusHud ~= nil then pcall(ADStatusHud.delete) end
end end
addModEventListener(ADSmartPickup) addModEventListener(ADSmartPickup)
+43 -5
View File
@@ -585,9 +585,17 @@ function ADSources.getObjectInfoIndex(source, fillType)
return nil return nil
end end
-- Alle grupper med varen: lageret deler samme vare i flere objectInfos (fx paller med forskellig fyldning). Kun den
-- første talt -> tallet "voksede", når en lille gruppe blev tom, og spawn-ventningen gik i stå (Sell Egg 30/9).
function ADSources.countStored(source, fillType) function ADSources.countStored(source, fillType)
local _, count = ADSources.getObjectInfoIndex(source, fillType) local spec = source.placeable ~= nil and source.placeable.spec_objectStorage or nil
return count or 0 local count = 0
for _, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
if getObjectInfoContent(objectInfo) == fillType then
count = count + (objectInfo.numObjects or 0)
end
end
return count
end end
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så -- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
@@ -636,7 +644,12 @@ function ADSources.describeAccepted(station)
return table.concat(names, ", ") return table.concat(names, ", ")
end end
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }. -- Salgssted spilleren har slået fra (spuSellBlock, ADSellBlock).
function ADSources.isSellBlocked(station)
return ADSellBlock ~= nil and ADSellBlock.isBlocked ~= nil and ADSellBlock.isBlocked(station) == true
end
-- Markører hvis aflæsningsstation er et rigtigt salgssted (og ikke slået fra): { {marker, station} }.
function ADSources.getSellMarkers(adEnv, farmId) function ADSources.getSellMarkers(adEnv, farmId)
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end) return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
end end
@@ -645,7 +658,7 @@ 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)
if station ~= nil and ADPrices.isSellingStation(station) then if station ~= nil and ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) then
table.insert(result, {marker = marker, station = station}) table.insert(result, {marker = marker, station = station})
end end
end end
@@ -963,13 +976,38 @@ function ADSources.getFarmRates(adEnv, farmId, feedOnly)
return rates return rates
end end
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
function ADSources.getHusbandryConsumption(placeable)
local rates, result = {}, {}
if placeable == nil then return result end
pcall(addHusbandryRates, rates, placeable)
for fillType, rate in pairs(rates) do
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
end
return result
end
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
function ADSources.getHusbandryOutputs(placeable)
local rates, outputs = {}, {}
if placeable == nil then return outputs end
pcall(addHusbandryRates, rates, placeable) -- giver kun forbrug; med for fuldstændighedens skyld
pcall(addAnimalProduction, rates, placeable)
for fillType, rate in pairs(rates) do
if (rate.production or 0) > 0 then outputs[fillType] = rate.production end
end
return outputs
end
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet): -- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen. -- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
function ADSources.getMarketQuote(fillType) function ADSources.getMarketQuote(fillType)
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
local best = nil local best = nil
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then if ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) and ADSources.stationAccepts(station, fillType) then
local quote = ADPrices.getQuote(station, fillType) local quote = ADPrices.getQuote(station, fillType)
if quote ~= nil and (best == nil or quote.price > best.price) then if quote ~= nil and (best == nil or quote.price > best.price) then
best = {price = quote.price, top = quote.top} best = {price = quote.price, top = quote.top}
+473
View File
@@ -0,0 +1,473 @@
-- AD Status Hud
-- Statusvinduet på skærmen (i stil med Simple Inspector): slås til/fra med input-handlingen SPU_TOGGLE_STATUS
-- (standard numpad *). Øjebliksbilledet bygges af modens egne statusfunktioner (samme som fanerne) højst
-- hvert sekund i update() og kun mens vinduet er tændt; draw() tegner kun de cachede linjer og bredder.
-- Synligheden gemmes pr. bruger i modSettings/FS25_ADSmartPickup/status.xml (standard skjult).
-- Kører kun på en klient (ikke dedikeret server). Alle spilkald er beskyttet; fejl logges én gang pr. nøgle.
ADStatusHud = {}
ADStatusHud.LOG_PREFIX = "ADSmartPickup: status: "
ADStatusHud.REFRESH_MS = 1000
ADStatusHud.STUCK_WARN_MS = 120000 -- stået stille så længe (uden at Smart Pickup holder den) = advarsel
ADStatusHud.TEXT_SIZE_PX = 14
ADStatusHud.MARGIN_PX = 8
ADStatusHud.SCREEN_OFFSET_PX = 16 -- afstand til skærmens højre kant
ADStatusHud.TOP_Y = 1 - 0.12 -- øverste kant (normaliseret); under spillets info-boks
ADStatusHud.SETTINGS_DIR = "modSettings/FS25_ADSmartPickup/"
ADStatusHud.SETTINGS_FILE = "status.xml"
ADStatusHud.SETTINGS_ROOT = "adSmartPickupStatus"
ADStatusHud.HEADER_COLOR = {1, 1, 1, 1}
ADStatusHud.visible = false
ADStatusHud.lines = {} -- {{text, level, header, color, width}}
ADStatusHud.layout = nil -- {x, y, width, height, textSize, marginX, marginY, padding}
local timerMs = 0
local settingsLoaded = false
local overlays = nil
local loggedErrors = {}
local registerToggleActionRef -- sat nederst (tastens handler til krogen)
-- ---- hjælpere ----
local function logOnce(key, err)
if loggedErrors[key] then return end
loggedErrors[key] = true
if Logging ~= nil and Logging.warning ~= nil then
Logging.warning(ADStatusHud.LOG_PREFIX .. "%s fejlede: %s", tostring(key), tostring(err))
end
end
local function guarded(key, fn, ...)
local ok, result = pcall(fn, ...)
if not ok then
logOnce(key, result)
return nil
end
return result
end
local function textOf(key)
if g_i18n == nil then return key end
local ok, text = pcall(g_i18n.getText, g_i18n, key)
return ok and text or key
end
function ADStatusHud.isClient()
local mission = g_currentMission
if mission == nil or mission.getIsClient == nil then return false end
local ok, isClient = pcall(mission.getIsClient, mission)
return ok and isClient == true
end
-- Fanernes egne tekst-/statusfunktioner ligger som metoder på SmartPickupFrame og bruger kun self.i18n:
-- en let "frame" med spillets i18n genbruger dem uden at røre menuen. "Start afvist" (lastStartFailed, rødt flow)
-- huskes på den rigtige ESC-fane (ADSmartPickup.guiFrame): tabellen deles, så vinduet viser samme farve som
-- Flows-listen (flowLevel rydder en afvisning når flowet kører — det samme som fanen selv gør).
local function frameProxy()
if SmartPickupFrame == nil then return nil end
local real = ADSmartPickup ~= nil and ADSmartPickup.guiFrame or nil
local failed = {}
if type(real) == "table" then
if type(real.lastStartFailed) ~= "table" then real.lastStartFailed = {} end
failed = real.lastStartFailed
end
return setmetatable({i18n = g_i18n, lastStartFailed = failed}, {__index = SmartPickupFrame})
end
local function fillTitle(fillType)
if fillType == nil or g_fillTypeManager == nil then return "?" end
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType)
or g_fillTypeManager:getFillTypeNameByIndex(fillType)
return tostring(title or "?")
end
-- ---- øjebliksbillede ----
local WAGON_ACTIONS = {
fetching = {key = "spu_status_act_fetchWagon", level = "waiting"},
waiting = {key = "spu_status_act_waitWagon", level = "waiting"},
emptying = {key = "spu_status_act_emptying", level = "running"},
homing = {key = "spu_status_act_homing", level = "running"},
}
-- Traktorens handling som kort tekst + niveau (ADRunsController.getVehicleActivity som Flows-fanen).
local function vehicleAction(adEnv, vehicle)
local wagonState = ADFlowWagons ~= nil and ADFlowWagons.statusOf ~= nil and ADFlowWagons.statusOf(vehicle) or nil
if wagonState ~= nil and WAGON_ACTIONS[wagonState] ~= nil then
return textOf(WAGON_ACTIONS[wagonState].key), WAGON_ACTIONS[wagonState].level
end
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
local place = tostring(activity.place or "?")
if activity.kind == "stopped" then
return textOf("spu_activity_stopped"), "stopped"
elseif activity.kind == "waiting" then
return string.format(textOf("spu_status_act_waiting"), place), "waiting"
elseif activity.kind == "holding" then
return string.format(textOf("spu_status_act_holding"), place), "waiting"
elseif activity.kind == "empty" and activity.fillType ~= nil then
return string.format(textOf("spu_status_act_fetch"), fillTitle(activity.fillType), place), "running"
end
return string.format(textOf("spu_status_act_driving"), place), "running"
end
-- Fyld i % af hele vogntoget (AutoDrives egne opgørelser, som kildevalget bruger); nil uden kapacitet.
local function fillPercent(adEnv, vehicle)
local autoDrive = adEnv.AutoDrive
if autoDrive == nil or autoDrive.getAllUnits == nil or autoDrive.getAllFillLevels == nil then return nil end
local level, capacity = autoDrive.getAllFillLevels(autoDrive.getAllUnits(vehicle))
if type(level) ~= "number" or type(capacity) ~= "number" or capacity <= 0 then return nil end
return math.max(0, math.min(100, level / capacity * 100))
end
local function farmVehiclesById(adEnv)
local byId = {}
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle)
if id ~= nil then byId[id] = vehicle end
end
return byId
end
-- Én linje pr. traktor i et kørende flow (flows der kan stoppes: startet, eller holder vogn/henter).
function ADStatusHud.flowEntries(adEnv, proxy)
local entries = {}
if ADRunsController == nil or ADRunsController.store == nil or ADFlowView == nil then return entries end
local byId = farmVehiclesById(adEnv)
for _, run in ipairs(ADFlowView.orderedRuns(ADRunsController.store.runs or {})) do
local status = ADRunsController.getRunStatus(adEnv, run)
if status.canStop then
local runLevel = proxy ~= nil and proxy.flowLevel ~= nil and guarded("flowLevel", proxy.flowLevel, proxy, adEnv, run) or nil
for _, vehicleId in ipairs(run.vehicleIds or {}) do
local vehicle = byId[vehicleId]
if vehicle ~= nil then
local action, level = vehicleAction(adEnv, vehicle)
-- farven følger flowets niveau som Flows-listen (venter/fejl), ellers traktorens egen handling
if runLevel == "error" or runLevel == "waiting" then level = runLevel end
table.insert(entries, {vehicle = tostring(ADRuns.vehicleLabel(vehicle)), action = action,
fillPercent = guarded("fillPercent", fillPercent, adEnv, vehicle), level = level})
end
end
end
end
return entries
end
-- Én linje pr. mark med aktivt markarbejde (samme statustekst og farve som Marker-fanen).
function ADStatusHud.fieldEntries(proxy)
local entries = {}
if ADFields == nil or ADFieldJobs == nil or ADFieldModel == nil or proxy == nil then return entries end
for _, info in ipairs(ADFields.list()) do
local field = ADFieldJobs.getField(info.id)
if field ~= nil and (ADFieldModel.isWorking(field) or ADFieldJobs.awaitingPlayer(info.id) ~= nil) then
local names = {}
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
end
table.insert(entries, {
fieldId = info.id,
work = proxy:fieldStatusText(field, info, true),
vehicle = #names > 0 and ADFlowView.joinCounted(names, ", ", nil) or nil,
progress = SmartPickupFrame.monthCpProgress ~= nil and guarded("fieldProgress", SmartPickupFrame.monthCpProgress, info.id) or nil,
level = proxy:fieldLevel(info.id),
})
end
end
return entries
end
-- Kæder med manglende led + ventende forslag + slukkede nødvendige fabrikker (samme punkter og tekster som
-- "Kræver handling"; de to sidste står under Advarsler).
function ADStatusHud.chainEntries(proxy)
local chains, suggestions = {}, {}
local result = {chains = chains, suggestions = suggestions, dependencies = {}}
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
return result
end
for _, entry in ipairs(ADStorageStats.chainEntries(ADChain.last)) do
if entry.kind == "chainProblem" then
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
elseif entry.kind == "chainSuggest" then
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
elseif entry.kind == "dependencyOff" then
table.insert(result.dependencies, {text = proxy:alertText(entry), level = "error"})
end
end
return result
end
-- Solgt/slettet køretøj (ADStuckDiag kan holde det et øjeblik efter salget).
local function isGone(vehicle)
if vehicle == nil or vehicle.isDeleted then return true end
if vehicle.rootNode == nil then return true end
return entityExists ~= nil and not entityExists(vehicle.rootNode)
end
-- Gårdens køretøjer der har stået stille over grænsen, uden at Smart Pickup selv holder dem (Wait/udkørsel).
function ADStatusHud.stuckEntries()
local entries = {}
if ADStuckDiag == nil or ADStuckDiag.stillVehicles == nil then return entries end
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or nil
for _, still in ipairs(ADStuckDiag.stillVehicles(ADStatusHud.STUCK_WARN_MS)) do
local vehicle = still.vehicle
local owned = not isGone(vehicle) and (farmId == nil or (vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId))
local held = owned and ((ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) ~= nil)
or (ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) ~= nil))
if owned and not held then
table.insert(entries, {text = string.format(textOf("spu_status_stuck"), tostring(ADRuns.vehicleLabel(vehicle)),
math.floor(still.ms / 60000)), level = "error"})
end
end
return entries
end
function ADStatusHud.snapshot(adEnv)
local proxy = frameProxy()
local snapshot = {flows = {}, fields = {}, chains = {}, warnings = {}}
if adEnv ~= nil then
snapshot.flows = guarded("flows", ADStatusHud.flowEntries, adEnv, proxy) or {}
end
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
snapshot.chains = chainResult.chains or {}
for _, entry in ipairs(chainResult.dependencies or {}) do table.insert(snapshot.warnings, entry) end
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
return snapshot
end
-- ---- layout og cache (kun i update) ----
local function gameInfoDisplay()
local hud = g_currentMission ~= nil and g_currentMission.hud or nil
return hud ~= nil and hud.gameInfoDisplay or nil
end
-- Størrelser i skærmkoordinater, skaleret som spillets egen HUD (Simple Inspector-mønster).
local function measureUnits()
local display = gameInfoDisplay()
if display == nil or display.scalePixelToScreenHeight == nil or display.scalePixelToScreenVector == nil then return nil end
local textSize = display:scalePixelToScreenHeight(ADStatusHud.TEXT_SIZE_PX)
local marginX, marginY = display:scalePixelToScreenVector({ADStatusHud.MARGIN_PX, ADStatusHud.MARGIN_PX})
local offsetX = display:scalePixelToScreenVector({ADStatusHud.SCREEN_OFFSET_PX, 0})
return textSize, marginX, marginY, offsetX
end
-- Øverste kant: fast under info-boksen; ligger info-boksens position lavere, rykkes vinduet ned under den.
local function topY(marginY)
local top = ADStatusHud.TOP_Y
local display = gameInfoDisplay()
if display ~= nil and display.getPosition ~= nil then
local ok, _, y = pcall(display.getPosition, display)
if ok and type(y) == "number" and y > 0.5 and y < 1 then top = math.min(top, y - marginY) end
end
return top
end
function ADStatusHud.rebuild(adEnv)
local lines = ADStatusView.lines(ADStatusHud.snapshot(adEnv), textOf, ADStatusView.MAX_LINES)
local textSize, marginX, marginY, offsetX = measureUnits()
if textSize == nil then
-- spillets info-boks (skalering) findes ikke: vinduet kan ikke måles og tegnes
logOnce("measure", "gameInfoDisplay mangler eller kan ikke skalere")
ADStatusHud.lines, ADStatusHud.layout = {}, nil
return
end
local maxWidth = 0
for _, line in ipairs(lines) do
setTextBold(line.header == true)
line.width = getTextWidth(textSize, line.text)
line.color = line.header and ADStatusHud.HEADER_COLOR or (ADFlowView.LEVEL_COLORS[line.level] or ADStatusHud.HEADER_COLOR)
if line.width > maxWidth then maxWidth = line.width end
end
setTextBold(false)
local width = maxWidth + 2 * marginX
local height = #lines * textSize + 2 * marginY
local top = topY(marginY)
ADStatusHud.lines = lines
ADStatusHud.layout = {x = 1 - offsetX - width, y = top - height, top = top, width = width, height = height,
textSize = textSize, marginX = marginX, marginY = marginY, padding = marginX * 0.5}
end
-- ---- overlays ----
local function createOverlays()
if overlays ~= nil then return overlays end
if g_overlayManager == nil or HUD == nil or HUD.COLOR == nil or HUD.COLOR.BACKGROUND == nil then return nil end
local r, g, b, a = unpack(HUD.COLOR.BACKGROUND)
local created = {
middle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0),
left = g_overlayManager:createOverlay("gui.gameInfo_left", 0, 0, 0, 0),
right = g_overlayManager:createOverlay("gui.gameInfo_right", 0, 0, 0, 0),
}
for _, overlay in pairs(created) do overlay:setColor(r, g, b, a) end
overlays = created
return overlays
end
local function deleteOverlays()
if overlays == nil then return end
for _, overlay in pairs(overlays) do
if overlay.delete ~= nil then pcall(overlay.delete, overlay) end
end
overlays = nil
end
-- ---- indstillinger (pr. bruger, ikke i savegamet) ----
local function settingsPath()
if getUserProfileAppPath == nil then return nil end
return getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR .. ADStatusHud.SETTINGS_FILE
end
function ADStatusHud.loadSettings()
settingsLoaded = true
local path = settingsPath()
if path == nil or fileExists == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupStatus", path)
if xmlFile == nil then return end
ADStatusHud.visible = xmlFile:getBool(ADStatusHud.SETTINGS_ROOT .. "#visible", false) == true
xmlFile:delete()
end
function ADStatusHud.saveSettings()
local path = settingsPath()
if path == nil then return end
if createFolder ~= nil then
createFolder(getUserProfileAppPath() .. "modSettings/")
createFolder(getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR)
end
local xmlFile = XMLFile.create("ADSmartPickupStatus", path, ADStatusHud.SETTINGS_ROOT)
if xmlFile == nil then
logOnce("saveSettings", "kunne ikke oprette " .. path)
return
end
xmlFile:setBool(ADStatusHud.SETTINGS_ROOT .. "#visible", ADStatusHud.visible == true)
xmlFile:save()
xmlFile:delete()
end
-- ---- offentlige kald fra moden ----
function ADStatusHud.toggle()
ADStatusHud.visible = not ADStatusHud.visible
timerMs = ADStatusHud.REFRESH_MS -- byg straks ved næste update
if not ADStatusHud.visible then ADStatusHud.lines, ADStatusHud.layout = {}, nil end
guarded("saveSettings", ADStatusHud.saveSettings)
end
-- Action-event-callback (target = ADStatusHud).
function ADStatusHud.onToggleAction()
ADStatusHud.toggle()
end
function ADStatusHud.update(dt, adEnv)
if not ADStatusHud.isClient() then return end
if not settingsLoaded then
guarded("loadSettings", ADStatusHud.loadSettings)
timerMs = ADStatusHud.REFRESH_MS
end
if not ADStatusHud.visible then return end
timerMs = timerMs + (dt or 0)
if timerMs < ADStatusHud.REFRESH_MS then return end
timerMs = 0
guarded("rebuild", ADStatusHud.rebuild, adEnv)
end
-- Tegnes ikke ved åben menu/dialog, skjult HUD, søvn eller åben chat.
local function shouldSkipDraw()
if g_gui ~= nil and g_gui:getIsGuiVisible() then return true end
if g_noHudModeEnabled then return true end
local mission = g_currentMission
local hud = mission ~= nil and mission.hud or nil
if hud == nil or hud.isVisible == false then return true end
if g_sleepManager ~= nil and g_sleepManager.getIsSleeping ~= nil and g_sleepManager:getIsSleeping() then return true end
if hud.chatDisplay ~= nil and hud.chatDisplay.getVisible ~= nil and hud.chatDisplay:getVisible() then return true end
return false
end
local function render(layout, bg)
bg.middle:setDimension(layout.width, layout.height)
bg.middle:setPosition(layout.x, layout.y)
bg.middle:render()
bg.left:setDimension(layout.padding, layout.height)
bg.left:setPosition(layout.x - layout.padding, layout.y)
bg.left:render()
bg.right:setDimension(layout.padding, layout.height)
bg.right:setPosition(layout.x + layout.width, layout.y)
bg.right:render()
setTextAlignment(RenderText.ALIGN_LEFT)
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_TOP)
local textX, textY = layout.x + layout.marginX, layout.top - layout.marginY
for _, line in ipairs(ADStatusHud.lines) do
local color = line.color
setTextBold(line.header == true)
setTextColor(color[1], color[2], color[3], color[4])
renderText(textX, textY, layout.textSize, line.text)
textY = textY - layout.textSize
end
end
function ADStatusHud.draw()
if not ADStatusHud.visible or ADStatusHud.layout == nil or #ADStatusHud.lines == 0 then return end
if shouldSkipDraw() then return end
local ok0, bg = pcall(createOverlays)
if not ok0 or bg == nil then
-- uden baggrund tegnes intet (g_overlayManager/HUD.COLOR mangler eller createOverlay fejlede)
logOnce("overlays", ok0 and "g_overlayManager eller HUD.COLOR.BACKGROUND mangler" or bg)
return
end
local ok, err = pcall(render, ADStatusHud.layout, bg)
-- tekststate nulstilles altid, også efter en fejl (andre mods/spillet tegner videre med den)
setTextColor(1, 1, 1, 1)
setTextAlignment(RenderText.ALIGN_LEFT)
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)
setTextBold(false)
if not ok then logOnce("draw", err) end
end
function ADStatusHud.delete()
deleteOverlays()
if PlayerInputComponent ~= nil and PlayerInputComponent.spuStatusToggleHandler == registerToggleActionRef then
PlayerInputComponent.spuStatusToggleHandler = nil
end
ADStatusHud.lines, ADStatusHud.layout = {}, nil
settingsLoaded = false
timerMs = 0
end
-- ---- tast: global spiller-handling (til fods og i køretøjer) ----
-- PlayerInputComponent.registerGlobalPlayerActionEvents kaldes ved hvert kontekstskift (til fods / i køretøj);
-- samme mønster som FS25_Courseplay (CP_OPEN_INGAME_MENU) og FS25_EasyDevControls. Hjælpeteksten skjules i F1.
local function registerToggleAction(component)
if not ADStatusHud.isClient() then return end
local player = component ~= nil and component.player or nil
if player ~= nil and player.isOwner == false then return end
if g_inputBinding == nil or InputAction == nil or InputAction.SPU_TOGGLE_STATUS == nil then return end
local _, eventId = g_inputBinding:registerActionEvent(InputAction.SPU_TOGGLE_STATUS, ADStatusHud, ADStatusHud.onToggleAction,
false, true, false, true)
if eventId ~= nil then g_inputBinding:setActionEventTextVisibility(eventId, false) end
end
registerToggleActionRef = function(component) guarded("registerAction", registerToggleAction, component) end
-- Spillets klasse overlever en genindlæsning af savegamet, moden gør ikke: krogen lægges kun én gang (markør på
-- klassen) og kalder den aktuelle indlæsnings handler, så en gammel ADStatusHud aldrig registrerer tasten igen.
function ADStatusHud.installInputHook()
if PlayerInputComponent == nil or PlayerInputComponent.registerGlobalPlayerActionEvents == nil or Utils == nil then return false end
PlayerInputComponent.spuStatusToggleHandler = registerToggleActionRef
if PlayerInputComponent.spuStatusToggleHooked then return true end
PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.appendedFunction(PlayerInputComponent.registerGlobalPlayerActionEvents,
function(component, ...)
local handler = PlayerInputComponent.spuStatusToggleHandler
if handler ~= nil then handler(component) end
end)
PlayerInputComponent.spuStatusToggleHooked = true
return true
end
if PlayerInputComponent ~= nil and Utils ~= nil then
ADStatusHud.installInputHook()
end
+143
View File
@@ -0,0 +1,143 @@
-- AD Status View
-- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet
-- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med
-- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, "..").
--
-- snapshot = {
-- flows = {{vehicle, action, fillPercent (0-100|nil), level}},
-- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}},
-- chains = {{text, level}}, -- standard rød
-- warnings = {{text, level}}, -- standard gul
-- }
-- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header".
ADStatusView = {}
ADStatusView.MAX_LINES = 18
ADStatusView.MAX_CHARS = 48
ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen
ADStatusView.MIN_FLEX_CHARS = 6
ADStatusView.SEPARATOR = " · "
ADStatusView.ELLIPSIS = ".."
local LEVELS = {running = true, waiting = true, error = true, stopped = true}
local SECTIONS = {
{key = "flows", header = "spu_status_flows", defaultLevel = "running"},
{key = "fields", header = "spu_status_fields", defaultLevel = "running"},
{key = "chains", header = "spu_status_chains", defaultLevel = "error"},
{key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"},
}
-- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ----
function ADStatusView.textLength(text)
local count = 0
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
return count
end
local function utf8Prefix(text, chars)
local result, count = {}, 0
for char in text:gmatch("[^\128-\191][\128-\191]*") do
if count >= chars then break end
table.insert(result, char)
count = count + 1
end
return table.concat(result)
end
function ADStatusView.fit(text, chars)
text = tostring(text or "")
if ADStatusView.textLength(text) <= chars then return text end
local ellipsis = ADStatusView.ELLIPSIS
return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis
end
-- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares.
local function joinFitted(parts, flexIndex)
local sep = ADStatusView.SEPARATOR
local fixed = ADStatusView.textLength(sep) * (#parts - 1)
for index, part in ipairs(parts) do
if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end
end
local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed)
local fitted = {}
for index, part in ipairs(parts) do
fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part
end
return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS)
end
local function percentText(value)
return string.format("%d %%", math.floor(value + 0.5))
end
-- ---- linjer pr. sektion ----
local function flowText(entry)
local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")}
if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end
return joinFitted(parts, 2)
end
local function fieldText(entry)
local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))}
if entry.vehicle ~= nil and entry.vehicle ~= "" then
table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS))
end
if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end
return joinFitted(parts, 1)
end
local function plainText(entry)
return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS)
end
local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText}
local function levelOf(entry, default)
return LEVELS[entry.level] and entry.level or default
end
-- Alle linjer uden loft.
local function allLines(snapshot, textOf)
local lines = {}
for _, section in ipairs(SECTIONS) do
local entries = snapshot[section.key] or {}
if #entries > 0 then
table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true})
for _, entry in ipairs(entries) do
table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false})
end
end
end
return lines
end
-- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden
-- indhold lige før afkortningen droppes.
local function capLines(lines, maxLines, textOf)
if #lines <= maxLines then return lines end
local kept = {}
for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end
while #kept > 0 and kept[#kept].header do table.remove(kept) end
local hidden = 0
for index = #kept + 1, #lines do
if not lines[index].header then hidden = hidden + 1 end
end
local ok, text = pcall(string.format, textOf("spu_status_more"), hidden)
table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false})
return kept
end
function ADStatusView.lines(snapshot, textOf, maxLines)
snapshot = snapshot or {}
textOf = textOf or function(key) return key end
maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES))
local lines = allLines(snapshot, textOf)
if #lines == 0 then
return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}}
end
return capLines(lines, maxLines, textOf)
end
+100 -1
View File
@@ -258,15 +258,114 @@ function ADStorageStats.attentionList(stats, max)
for _, stat in ipairs(stats or {}) do for _, stat in ipairs(stats or {}) do
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
end end
return ADStorageStats.rankAttention(list, max)
end
-- Kæde-forslag ligger ikke på en vare (ingen stat): navnet til den stabile rækkefølge tages fra teksten.
local function entryName(entry)
return tostring(entry.stat ~= nil and entry.stat.title or entry.text)
end
function ADStorageStats.rankAttention(list, max)
table.sort(list, function(a, b) table.sort(list, function(a, b)
if a.score ~= b.score then return a.score > b.score end if a.score ~= b.score then return a.score > b.score end
return tostring(a.stat.title) < tostring(b.stat.title) return entryName(a) < entryName(b)
end) end)
local result = {} local result = {}
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
return result return result
end end
-- "Kræver handling" fra kæden (ADChain.last): ÉT gult punkt "Kæde: N forslag" (klik -> fanen Kæder), så
-- forslagene ikke fylder listen. Rangeres under alle røde punkter (>= 3000), øverst blandt de gule, over "sælg nu".
ADStorageStats.CHAIN_SCORE = 2000
-- Manglende led: ALLE kædeproblemer i ÉN rød post ("Kæder: N manglende led", klik -> fanen Kæder med kæderne
-- foldet ud), så en gård med mange kæder (fx fermenteringssiloer) ikke fylder "Kræver handling". N = unikke
-- (kæde, vare). Score CHAIN_PROBLEM_SCORE + alvor (0-99): under "rød fuld" (3000-3100), så lagerets røde poster
-- altid vises først, og over de gule (<= 2000). Alvor = manglende andel af behovet; kredsløb = 99.
ADStorageStats.CHAIN_PROBLEM_SCORE = 2900
local function severity(problems)
local missing, need = 0, 0
for _, problem in ipairs(problems) do
if problem.kind == "cycle" then return 99 end
missing, need = missing + (problem.detail or 0), need + (problem.need or 0)
end
if need <= 0 then return 99 end
return math.floor(99 * math.min(1, missing / need))
end
local function chainProblemEntries(last)
local known = {}
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do known[root.key] = true end
local problems, seen, rootKeys, rootSeen = {}, {}, {}, {}
for _, problem in ipairs(last.chain ~= nil and last.chain.problems or {}) do
if problem.rootKey ~= nil and known[problem.rootKey] then
table.insert(problems, problem)
seen[tostring(problem.rootKey) .. "|" .. tostring(problem.fillType)] = true
if not rootSeen[problem.rootKey] then
rootSeen[problem.rootKey] = true
table.insert(rootKeys, problem.rootKey)
end
end
end
if #problems == 0 then return {} end
local count = 0
for _ in pairs(seen) do count = count + 1 end
table.sort(rootKeys, function(a, b) return tostring(a) < tostring(b) end)
return {{kind = "chainProblem", level = "red", score = ADStorageStats.CHAIN_PROBLEM_SCORE + severity(problems), count = count,
rootKeys = rootKeys, text = string.format("Kæder: %d", count)}}
end
-- "Slukket men nødvendig" (ADChain.last.dependency.offNeeded): højst MAX_DEPENDENCY_ENTRIES post (den mest hastende
-- fabrik; more = antal skjulte, vist som "+k"). Score DEPENDENCY_SCORE + hast (0-99, færre lagerdage = højere;
-- ukendt = 50): over "rød fuld" (3000-3100), under "rød tom" (3900-4000).
ADStorageStats.DEPENDENCY_SCORE = 3700
ADStorageStats.MAX_DEPENDENCY_ENTRIES = 1
local function urgency(days)
if days == nil then return 50 end
if days == math.huge then return 0 end
return math.max(0, 99 - math.min(99, math.floor(days)))
end
function ADStorageStats.dependencyEntries(last)
local entries = {}
for _, item in ipairs(last ~= nil and last.dependency ~= nil and last.dependency.offNeeded or {}) do
local name = tostring(item.buildingName or item.building or "?")
table.insert(entries, {kind = "dependencyOff", level = "red", score = ADStorageStats.DEPENDENCY_SCORE + urgency(item.days),
building = item.building, buildingName = name, fillType = item.fillType, onlyChain = item.onlyChain == true, text = name})
end
entries = ADStorageStats.rankAttention(entries)
local result = {}
for index = 1, math.min(#entries, ADStorageStats.MAX_DEPENDENCY_ENTRIES) do result[index] = entries[index] end
if #entries > #result then result[#result].more = #entries - #result end
return result
end
function ADStorageStats.chainEntries(last)
if last == nil then return {} end
local result = chainProblemEntries(last)
for _, entry in ipairs(ADStorageStats.dependencyEntries(last)) do table.insert(result, entry) end
local count = 0
for _, action in ipairs(last.actions or {}) do
if action.mode == "suggest" then count = count + 1 end
end
if count > 0 then
table.insert(result, {kind = "chainSuggest", level = "yellow", score = ADStorageStats.CHAIN_SCORE, count = count,
text = string.format("Kæde: %d", count)})
end
return result
end
-- Varepunkterne (allerede rangeret) + kædens punkter, samme loft.
function ADStorageStats.mergeAttention(attention, chainEntries, max)
local list = {}
for _, entry in ipairs(attention or {}) do table.insert(list, entry) end
for _, entry in ipairs(chainEntries or {}) do table.insert(list, entry) end
return ADStorageStats.rankAttention(list, max)
end
function ADStorageStats.kpis(stats) function ADStorageStats.kpis(stats)
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0} local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
for _, stat in ipairs(stats or {}) do for _, stat in ipairs(stats or {}) do
+10
View File
@@ -53,6 +53,16 @@ function ADStuckDiag.describe(vehicle)
return table.concat(parts, " ") return table.concat(parts, " ")
end end
-- Kun læsning (statusvinduet): køretøjer der har holdt stille mindst minMs -> {{vehicle, ms}}, længst først.
function ADStuckDiag.stillVehicles(minMs)
local list = {}
for vehicle, ms in pairs(stillMs) do
if ms >= (minMs or ADStuckDiag.STILL_MS) then table.insert(list, {vehicle = vehicle, ms = ms}) end
end
table.sort(list, function(a, b) return a.ms > b.ms end)
return list
end
function ADStuckDiag.tick(dt) function ADStuckDiag.tick(dt)
timerMs = timerMs + (dt or 0) timerMs = timerMs + (dt or 0)
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
+58
View File
@@ -0,0 +1,58 @@
-- AD Tip Side
-- Tipvogne med flere tip-sider (fx Farmtech EDK 650 Unrealistic: bag / kornluge / venstre / højre) tillader kun
-- AI-aflæsning (allowAIDischarge) på nogle af udløbene. Står vognen på en side uden, åbner AD aflæsningen ved
-- målet, men intet løber ud — vognen holder med fuldt læs (in-game 29/9: Chicken Food 2 med 786.712 l hvede på
-- højre side ved en Farma med 10 mio. l fri; Disponibel Vogn 3 fuld af løg). Når AD kører en vogn, skiftes den til
-- en side, AI må aflæsse fra.
ADTipSide = {}
ADTipSide.LOG_PREFIX = "ADSmartPickup: tip-side: "
-- ---- ren logik ----
-- sides = {{node = udløbsindeks}}, allowAI = {[udløbsindeks] = true}, current = nuværende side
-- -> sideindeks der skal vælges, eller nil (nuværende er fin / ingen side tilladt / ukendt)
function ADTipSide.choose(sides, allowAI, current)
local currentSide = (sides or {})[current]
if currentSide ~= nil and (allowAI or {})[currentSide.node] then return nil end
for index, side in ipairs(sides or {}) do
if (allowAI or {})[side.node] then return index end
end
return nil
end
-- ---- spil ----
local warned = setmetatable({}, {__mode = "k"})
local function sidesOf(object)
local spec = object.spec_trailer
local dis = object.spec_dischargeable
if spec == nil or dis == nil or spec.tipSides == nil or #spec.tipSides < 2 then return nil end
local sides, allowAI = {}, {}
for index, side in ipairs(spec.tipSides) do sides[index] = {node = side.dischargeNodeIndex} end
for index, node in ipairs(dis.dischargeNodes or {}) do
if node.allowAIDischarge then allowAI[index] = true end
end
return sides, allowAI, spec.preferedTipSideIndex
end
-- Alle tipvogne bag traktoren: skift til en AI-tilladt side (kun mens vognen ikke tipper).
function ADTipSide.ensure(vehicle)
local children = vehicle.getChildVehicles ~= nil and vehicle:getChildVehicles() or {}
for _, object in pairs(children) do
if object ~= vehicle and object.setPreferedTipSide ~= nil then
local sides, allowAI, current = sidesOf(object)
local tipping = Trailer ~= nil and object.getTipState ~= nil and object:getTipState() ~= Trailer.TIPSTATE_CLOSED
local index = sides ~= nil and not tipping and ADTipSide.choose(sides, allowAI, current) or nil
if index ~= nil then
object:setPreferedTipSide(index)
if not warned[object] then
warned[object] = true
Logging.info(ADTipSide.LOG_PREFIX .. "'%s': tip-side %s -> %d (AutoDrive må kun aflæse fra den side)",
tostring(object.getName ~= nil and object:getName() or "?"), tostring(current), index)
end
end
end
end
end
+12
View File
@@ -426,6 +426,9 @@ function ADUnloadWait.getClaims(adEnv)
return ADWaitPool.getClaims(adEnv) return ADWaitPool.getClaims(adEnv)
end end
ADUnloadWait.TIP_CHECK_MS = 2000
local tipChecked = setmetatable({}, {__mode = "k"})
function ADUnloadWait.install(adEnv) function ADUnloadWait.install(adEnv)
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
@@ -438,6 +441,15 @@ function ADUnloadWait.install(adEnv)
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
task.update = function(self, dt) task.update = function(self, dt)
-- alle aflæsningsture (også udkørsel og tømning): tipvognen på en side AD må aflæsse fra (EDK 650, 29/9)
if ADTipSide ~= nil and self.vehicle ~= nil then
local now = g_time or 0
if (tipChecked[self.vehicle] or -math.huge) + ADUnloadWait.TIP_CHECK_MS <= now then
tipChecked[self.vehicle] = now
local okTip, errTip = pcall(ADTipSide.ensure, self.vehicle)
if not okTip then Logging.warning(ADUnloadWait.LOG_PREFIX .. "tip-side: %s", tostring(errTip)) end
end
end
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt) local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
if not ok then if not ok then
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err)) Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
@@ -4,6 +4,14 @@
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet). -- 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). -- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
-- Lagerfanen for en bygningstype (TAB_KINDS), fx "factory" -> fanen Fabrikker.
function SmartPickupFrame.tabOfKind(kind)
for tab, tabKind in pairs(SmartPickupFrame.TAB_KINDS or {}) do
if tabKind == kind then return tab end
end
return SmartPickupFrame.TAB_OVERVIEW
end
local function fillTypeName(fillType) local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType) return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end end
@@ -62,6 +70,10 @@ end
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info -- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
function SmartPickupFrame:attentionAdvice(entry) function SmartPickupFrame:attentionAdvice(entry)
-- kæde-forslag og manglende led har ingen vare (stat): klik fører til fanen Kæder
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then return "chain", nil end
-- slukket men nødvendig: fanen Fabrikker (Kæder når kun en kæde bruger varen)
if entry ~= nil and entry.kind == "dependencyOff" then return entry.onlyChain and "chain" or "factories", nil end
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end if entry ~= nil 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 if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
local stat = entry.stat local stat = entry.stat
@@ -83,6 +95,8 @@ function SmartPickupFrame:attentionAdvice(entry)
end end
function SmartPickupFrame:attentionHint(advice, info) function SmartPickupFrame:attentionHint(advice, info)
if advice == "chain" then return self.i18n:getText("spu_attention_hintChain") end
if advice == "factories" then return self.i18n:getText("spu_attention_hintFactories") end
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end if advice == "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 == "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 if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
@@ -91,6 +105,19 @@ end
function SmartPickupFrame:onAttentionClick(entry) function SmartPickupFrame:onAttentionClick(entry)
self:safe(function(frame) self:safe(function(frame)
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then
-- manglende led: kæderne med problemer foldes ud, så problemteksten står under den røde linje
if entry.kind == "chainProblem" then
frame.chainExpanded = frame.chainExpanded or {}
for _, rootKey in ipairs(entry.rootKeys or {}) do frame.chainExpanded[rootKey] = true end
end
frame:switchTab(SmartPickupFrame.TAB_CHAINS)
return
end
if entry ~= nil and entry.kind == "dependencyOff" then
frame:switchTab(entry.onlyChain and SmartPickupFrame.TAB_CHAINS or SmartPickupFrame.tabOfKind("factory"))
return
end
local adEnv = frame.getAdEnv() local adEnv = frame.getAdEnv()
local advice, info = frame:attentionAdvice(entry) local advice, info = frame:attentionAdvice(entry)
if adEnv == nil or advice == "none" then return end if adEnv == nil or advice == "none" then return end
@@ -0,0 +1,142 @@
-- Smart Pickup — fanen Kæder
-- Produktionskæderne som træ: rod (produktion spilleren har tændt) -> varer -> råvarer, med kilde, behov pr.
-- måned og flowstatus; afgrødeforslag kan godkendes/afvises og en rod sættes på pause. Rækkerne bygges af
-- ADChainView (testet); her kun kloning, farve og klik. Et resultat yngre end CHAIN_MAX_AGE_MS (fra kædens eget
-- tick eller en tidligere visning) genbruges; ellers evalueres (visning, handler ikke). Klik evaluerer altid.
SmartPickupFrame.CHAIN_MAX_AGE_MS = 30000
function SmartPickupFrame:chainLast(adEnv, force)
local age = ADChain.lastAge ~= nil and ADChain.lastAge() or math.huge
if force or ADChain.last == nil or age >= SmartPickupFrame.CHAIN_MAX_AGE_MS then
ADChain.evaluate(adEnv)
end
return ADChain.last
end
function SmartPickupFrame:chainTitle(last, name)
local title = last ~= nil and last.titles ~= nil and last.titles[name] or nil
return title or self:fruitTitle(name)
end
-- Klik på en kæde-knap: udfør, evaluér straks (ny visning) og genopbyg. Et forslag der ikke længere findes
-- (approve/dismiss -> false) meldes i statuslinjen som i "Kræver handling".
function SmartPickupFrame:onChainAction(action)
local ok = true
if action.kind == "approve" then
ok = ADChain.approve(action.fieldId)
elseif action.kind == "dismiss" then
ok = ADChain.dismiss(action.fieldId)
elseif action.kind == "optIn" or action.kind == "optOut" then
ADChain.setOptIn(action.rootKey, action.kind == "optIn")
elseif action.kind == "finalMode" then
ADChain.setFinalKeep(action.rootKey, action.keep)
elseif action.kind == "pause" or action.kind == "resume" then
ADChain.setPaused(action.rootKey, action.kind == "pause")
end
if ok == false then
local text = self.i18n:getText("spu_chain_gone")
self.statusOverride, self.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
end
local adEnv = self.getAdEnv()
if adEnv ~= nil then self:chainLast(adEnv, true) end
self:requestRebuild()
end
-- Klik på en varelinje: åbn flowet der kører varen til modtageren (kædens eget flow til bygningen, ellers et flow
-- der dækker varen) i Flows-fanens redigeringsvindue; uden flow en besked.
function SmartPickupFrame:openChainFlow(consumer, fillType)
local runs = ADChain.runsView(ADRunsController.store.runs)
local runId = ADChainActions.chainRunFor(runs, consumer)
if runId == nil then
for _, run in ipairs(runs) do
if runId == nil and ADChainActions.isCovered({consumer = consumer, fillType = fillType}, {run}) then runId = run.id end
end
end
if runId == nil then
self.statusOverride = self.i18n:getText("spu_chain_noFlow")
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self:requestRebuild()
return
end
self.selectedRunId = runId
self:switchTab(SmartPickupFrame.TAB_FLOWS)
self:openEditPanel("flow", runId)
end
-- Klik på rodens linje uden for knappen: fold ud/sammen (huskes kun mens spillet kører).
function SmartPickupFrame:toggleChainRoot(rootKey)
self.chainExpanded = self.chainExpanded or {}
self.chainExpanded[rootKey] = not self.chainExpanded[rootKey] or nil
self:requestRebuild()
end
-- Hvilken celle blev der klikket i? Højeste celle hvis venstre kant ligger til venstre for klikket.
local function clickedCell(frame, cells)
if frame.lastClickX == nil then return nil end
for index = #cells, 1, -1 do
local cell = cells[index]
if cell ~= nil and cell.absPosition ~= nil and frame.lastClickX >= cell.absPosition[1] then return index end
end
return nil
end
-- Række med prik + c1..c5 (flowListPrefab). Knapper er celler (entry.actions[celle], som [Start]/[Stop] i
-- Flows-listen); et klik andetsteds på en rodlinje folder den ud/sammen.
function SmartPickupFrame:addChainRow(entry)
local unpackFn = table.unpack or unpack
local row = self:addRow(self.flowListPrefab, {kind = "chainRow"})
local cells = {}
for index = 1, 5 do cells[index] = row:getDescendantByName("c" .. index) end
SmartPickupFrame.setCells(cells, entry.cells)
local dot = row:getDescendantByName("dot")
local color = entry.level ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
if dot ~= nil and dot.setImageColor ~= nil and color ~= nil then dot:setImageColor(nil, unpackFn(color)) end
-- kun rod og varer har en status-prik (overskrift, forslag og auto-skift har ingen)
if dot ~= nil and dot.setVisible ~= nil and entry.kind ~= "root" and entry.kind ~= "node" then dot:setVisible(false) end
if cells[4] ~= nil and cells[4].setTextColor ~= nil and color ~= nil and entry.level ~= "running" and entry.kind ~= "suggest" then
cells[4]:setTextColor(unpackFn(color))
end
local actions, toggle = entry.actions or {}, entry.toggle
if next(actions) == nil and toggle == nil and entry.openConsumer ~= nil then
self:makeRowClickable(row, function(frame) frame:openChainFlow(entry.openConsumer, entry.openFillType) end)
elseif next(actions) ~= nil or toggle ~= nil then
self:makeRowClickable(row, function(frame)
local action = actions[clickedCell(frame, cells) or 0]
if action ~= nil then
frame:onChainAction(action)
elseif toggle ~= nil then
frame:toggleChainRoot(toggle)
end
end)
end
return row
end
-- Manglende led: problemteksten som (ombrudt) tekstrække i rød under varelinjen.
function SmartPickupFrame:addChainProblemRow(entry)
local unpackFn = table.unpack or unpack
local color = ADFlowView.LEVEL_COLORS ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
for _, line in ipairs(ADFlowView.wrapText(entry.text, SmartPickupFrame.TEXT_ROW_CHARS)) do
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
title:setText(line)
if color ~= nil and title.setTextColor ~= nil then title:setTextColor(unpackFn(color)) end
end
end
function SmartPickupFrame:fillChainRows(adEnv)
local i18n = self.i18n
local last = self:chainLast(adEnv, false)
local rows = ADChainView.rows(last, function(name) return self:chainTitle(last, name) end,
SmartPickupFrame.formatCompact, function(key) return i18n:getText(key) end, self.chainExpanded or {})
for _, entry in ipairs(rows) do
if entry.kind == "info" or entry.kind == "empty" then
self:addTextRow(entry.text)
elseif entry.kind == "problem" then
self:addChainProblemRow(entry)
else
self:addChainRow(entry)
end
end
end
@@ -307,16 +307,16 @@ function SmartPickupFrame:fillPoolRows(adEnv)
self:addTextRow(i18n:getText("spu_pool_section_tractors")) self:addTextRow(i18n:getText("spu_pool_section_tractors"))
for _, tractor in ipairs(tractors) do for _, tractor in ipairs(tractors) do
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id}) 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 status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), ADFlowView.shortVehicleName(tractor.arg))
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or "" 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("title"):setText(string.format(" %s%s — %s", ADFlowView.shortVehicleName(tractor.label), hp, status))
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false) row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
end end
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
self:addTextRow(i18n:getText("spu_pool_section_implements")) self:addTextRow(i18n:getText("spu_pool_section_implements"))
for _, implement in ipairs(implements) do for _, implement in ipairs(implements) do
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg)) local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), ADFlowView.shortVehicleName(implement.arg))
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status)) self:addTextRow(string.format(" %s — %s: %s", ADFlowView.shortVehicleName(implement.label), self:roleText(implement.role), status))
end end
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
end end
@@ -232,9 +232,7 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
return return
end end
self:addTextRow(i18n:getText("spu_flowList_hint")) self:addTextRow(i18n:getText("spu_flowList_hint"))
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do local function addFlowRow(run)
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 row = self:addRow(self.flowListPrefab, {kind = "flowList"})
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"), local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"), cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
@@ -253,6 +251,22 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
end) end)
table.insert(self.flowListRows, entry) table.insert(self.flowListRows, entry)
end end
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
local runs, idle = group.runs, {}
-- kædeflows uden traktor (venter på en ledig traktor/vogn) foldes sammen (in-game 30/9)
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
for _, run in ipairs(runs) do addFlowRow(run) end
if #idle > 0 then
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
frame.showIdleChainFlows = not frame.showIdleChainFlows
frame:requestRebuild()
end)
if self.showIdleChainFlows then
for _, run in ipairs(idle) do addFlowRow(run) end
end
end
end end
self:refreshFlowListRows(adEnv) self:refreshFlowListRows(adEnv)
end end
@@ -266,7 +280,7 @@ function SmartPickupFrame:refreshFlowListRows(adEnv)
local cells = self:flowCells(adEnv, run) local cells = self:flowCells(adEnv, run)
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)] local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
entry.cells[1]:setText(tostring(run.name)) entry.cells[1]:setText(ADFlowView.listName(run))
entry.cells[2]:setText(tostring(cells[2])) entry.cells[2]:setText(tostring(cells[2]))
entry.cells[3]:setText(tostring(cells[5])) entry.cells[3]:setText(tostring(cells[5]))
entry.cells[4]:setText(tostring(cells[6])) entry.cells[4]:setText(tostring(cells[6]))
+21 -5
View File
@@ -51,7 +51,7 @@ function SmartPickupFrame:flowCells(adEnv, run)
if row.kind == "outbound" then if row.kind == "outbound" then
local sells, stores = ADFlowView.policyUse(row.policies) local sells, stores = ADFlowView.policyUse(row.policies)
local parts = {} local parts = {}
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end if sells then table.insert(parts, row.sellAtPercent == 0 and i18n:getText("spu_flow_ruleSellAlways") or string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
rule = table.concat(parts, ", ") rule = table.concat(parts, ", ")
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
@@ -64,7 +64,9 @@ function SmartPickupFrame:flowCells(adEnv, run)
elseif row.reason ~= nil then elseif row.reason ~= nil then
status = row.reason status = row.reason
end end
local name = (run.id == self.selectedRunId and "> " or "") .. tostring(run.name) -- kædeflow uden traktor: sig hvorfor det står stille i stedet for flowets regel
if ADFlowView.isIdleChain(run) then status = i18n:getText("spu_flow_chainIdle") end
local name = (run.id == self.selectedRunId and "> " or "") .. ADFlowView.listName(run)
return {name, products, from, to, string.format("%d/%d", row.running, row.total), status} return {name, products, from, to, string.format("%d/%d", row.running, row.total), status}
end end
@@ -103,9 +105,7 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
local i18n = self.i18n local i18n = self.i18n
self:addTextRow(i18n:getText("spu_overview_hint")) self:addTextRow(i18n:getText("spu_overview_hint"))
self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")}) self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")})
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do local function addBoardRow(run)
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
for _, run in ipairs(group.runs) do
local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run)) local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
if cells[1] ~= nil and cells[1].parent ~= nil then if cells[1] ~= nil and cells[1].parent ~= nil then
local runId = run.id local runId = run.id
@@ -113,6 +113,22 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
end end
table.insert(self.overviewRows, {runId = run.id, cells = cells}) table.insert(self.overviewRows, {runId = run.id, cells = cells})
end end
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
local runs, idle = group.runs, {}
-- kædeflows uden traktor foldes sammen som i Flows-listen (in-game 30/9)
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
for _, run in ipairs(runs) do addBoardRow(run) end
if #idle > 0 then
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
frame.showIdleChainFlows = not frame.showIdleChainFlows
frame:requestRebuild()
end)
if self.showIdleChainFlows then
for _, run in ipairs(idle) do addBoardRow(run) end
end
end
end end
if #ADRunsController.store.runs == 0 then if #ADRunsController.store.runs == 0 then
self:addTextRow(i18n:getText("spu_noRuns")) self:addTextRow(i18n:getText("spu_noRuns"))
+41 -8
View File
@@ -16,8 +16,19 @@ SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_
SmartPickupFrame.MONTH_REBUILD_TICKS = 5 SmartPickupFrame.MONTH_REBUILD_TICKS = 5
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg -- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30 SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
-- Kæder-fanen (gui/SmartPickupChainTab.lua): klikbare forslag/pause, så genopbygning sjældent (som Marksiloer)
SmartPickupFrame.TAB_CHAINS = 10
SmartPickupFrame.CHAIN_REBUILD_TICKS = 30
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory) -- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"} SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds", "spu_tab_chains"}
-- Visningsrækkefølge (fane-id'erne ovenfor er faste og bruges overalt): Kæder står efter Marker (in-game 30/9).
SmartPickupFrame.TAB_DISPLAY = {1, 2, 3, 10, 4, 5, 6, 7, 8, 9}
function SmartPickupFrame.tabPosition(tabId)
for position, id in ipairs(SmartPickupFrame.TAB_DISPLAY) do
if id == tabId then return position end
end
return tabId
end
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"} SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
function SmartPickupFrame.getTabKind(tab) function SmartPickupFrame.getTabKind(tab)
@@ -96,8 +107,9 @@ function SmartPickupFrame:initialize()
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
self.selectorPrefab:unlinkElement() self.selectorPrefab:unlinkElement()
FocusManager:removeElement(self.selectorPrefab) FocusManager:removeElement(self.selectorPrefab)
for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do for position, index in ipairs(SmartPickupFrame.TAB_DISPLAY) do
self.subCategoryPaging:addText(tostring(index)) local key = SmartPickupFrame.TAB_NAMES[index]
self.subCategoryPaging:addText(tostring(position))
local tab = self.selectorPrefab:clone(self.subCategoryBox) local tab = self.selectorPrefab:clone(self.subCategoryBox)
FocusManager:loadElementFromCustomValues(tab) FocusManager:loadElementFromCustomValues(tab)
tab:setText(self.i18n:getText(key)) tab:setText(self.i18n:getText(key))
@@ -267,11 +279,11 @@ function SmartPickupFrame:switchTab(tabIndex)
end end
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() 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 pairs(self.subCategoryTabs) do
tab:setSelected(index == tabIndex) tab:setSelected(index == tabIndex)
end end
if self.subCategoryPaging ~= nil then if self.subCategoryPaging ~= nil then
self.subCategoryPaging:setState(tabIndex, false) self.subCategoryPaging:setState(SmartPickupFrame.tabPosition(tabIndex), false)
end end
self:requestRebuild() self:requestRebuild()
self:safe(function(frame) self:safe(function(frame)
@@ -279,8 +291,9 @@ function SmartPickupFrame:switchTab(tabIndex)
end) end)
end end
-- pilene/paging giver en position i visningsrækkefølgen
function SmartPickupFrame:onClickTab(state) function SmartPickupFrame:onClickTab(state)
self:switchTab(state) self:switchTab(SmartPickupFrame.TAB_DISPLAY[state] or state)
end 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/
@@ -466,6 +479,18 @@ function SmartPickupFrame:rebuild()
self:restoreFocusIfLost() self:restoreFocusIfLost()
return return
end end
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
if adEnv ~= nil then
self:fillChainRows(adEnv)
end
self.statusRow = self:addTextRow(self.statusOverride or "")
self.monthRebuildTicks = SmartPickupFrame.CHAIN_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_MONTH then if self.currentTab == SmartPickupFrame.TAB_MONTH then
if adEnv ~= nil then if adEnv ~= nil then
self:fillMonthRows(adEnv) self:fillMonthRows(adEnv)
@@ -611,7 +636,8 @@ function SmartPickupFrame:refreshButtons()
wanted = self.overviewButtons wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
wanted = self:getFieldButtons() wanted = self:getFieldButtons()
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
wanted = self.monthButtons wanted = self.monthButtons
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
wanted = self.storageButtons wanted = self.storageButtons
@@ -636,7 +662,14 @@ 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 if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
-- "forslaget gælder ikke længere" står STATUS_OVERRIDE_TICKS sekunder
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
self:setStatusText(self.statusOverride or "")
end
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1 self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
if self.monthRebuildTicks <= 0 then self:requestRebuild() end if self.monthRebuildTicks <= 0 then self:requestRebuild() end
self:refreshButtons() self:refreshButtons()
@@ -183,8 +183,33 @@ function SmartPickupFrame:addKpiRow(stats)
return row return row
end end
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet
-- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS,
-- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks.
function SmartPickupFrame:attentionFor(stats)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then
local okEnv, adEnv = pcall(self.getAdEnv)
if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end
end
if ADChain == nil or ADChain.last == nil then return attention end
return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION)
end
function SmartPickupFrame:alertText(entry) function SmartPickupFrame:alertText(entry)
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
if entry.kind == "chainSuggest" then
return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0)
end
if entry.kind == "chainProblem" then
return string.format(i18n:getText("spu_attention_chainProblems"), entry.count or 0)
end
if entry.kind == "dependencyOff" then
if (entry.more or 0) > 0 then
return string.format(i18n:getText("spu_attention_depOffMore"), tostring(entry.buildingName), entry.more)
end
return string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName))
end
if entry.kind == "empty" then 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))) 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 elseif entry.kind == "full" then
@@ -263,7 +288,7 @@ end
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory) function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
local i18n = self.i18n local i18n = self.i18n
local stats = self:computeItemStats(adEnv, inventory, fullInventory) local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION) local attention = self:attentionFor(stats)
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)} self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
self:addTextRow(i18n:getText("spu_attention")) self:addTextRow(i18n:getText("spu_attention"))
@@ -307,7 +332,7 @@ function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
local view = self.itemView local view = self.itemView
if view == nil then return end if view == nil then return end
local stats = self:computeItemStats(adEnv, inventory, fullInventory) local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION) local attention = self:attentionFor(stats)
if self:itemStructureKey(stats, attention) ~= view.key then if self:itemStructureKey(stats, attention) ~= view.key then
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1 self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
if self.itemRebuildTicks <= 0 then self:requestRebuild() end if self.itemRebuildTicks <= 0 then self:requestRebuild() end
+84 -1
View File
@@ -151,7 +151,7 @@
<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, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/> <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_rig_working" text="i gang på F%s"/>
<text name="spu_field_tooWideShort" text="!! over 50 m"/> <text name="spu_field_tooWideShort" text="kursus 50 m"/>
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/> <text name="spu_field_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"/>
@@ -167,6 +167,7 @@
<text name="spu_field_careNeed" text="trænger til %s"/> <text name="spu_field_careNeed" text="trænger til %s"/>
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/> <text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
<text name="spu_field_lime" text="Kalk når jorden trænger"/> <text name="spu_field_lime" text="Kalk når jorden trænger"/>
<text name="spu_field_chainFree" text="Fri til kæden (moden må skifte afgrøde)"/>
<text name="spu_field_mulch" text="Stubbehakning efter høst"/> <text name="spu_field_mulch" text="Stubbehakning efter høst"/>
<text name="spu_field_need_fert" text="gødning"/> <text name="spu_field_need_fert" text="gødning"/>
<text name="spu_field_need_lime" text="kalk"/> <text name="spu_field_need_lime" text="kalk"/>
@@ -309,6 +310,7 @@
<text name="spu_policy_sell" text="Salg"/> <text name="spu_policy_sell" text="Salg"/>
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/> <text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/> <text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
<text name="spu_sellAt_always" text="Altid (uanset pris)"/>
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/> <text name="spu_sellAt" 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"/>
@@ -349,6 +351,8 @@
<text name="spu_flow_noSource" text="(ingen kilde)"/> <text name="spu_flow_noSource" text="(ingen kilde)"/>
<text name="spu_flow_noTarget" text="(intet mål)"/> <text name="spu_flow_noTarget" text="(intet mål)"/>
<text name="spu_flow_noVehicle" text="(ingen traktor)"/> <text name="spu_flow_noVehicle" text="(ingen traktor)"/>
<text name="spu_flow_ruleSellAlways" text="sælg altid"/>
<text name="spu_flow_chainIdle" text="mangler traktor/vogn"/>
<text name="spu_flow_ruleSell" text="pris >= %d %%"/> <text name="spu_flow_ruleSell" text="pris >= %d %%"/>
<text name="spu_flow_ruleStore" text="lager > %d %%"/> <text name="spu_flow_ruleStore" text="lager > %d %%"/>
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/> <text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
@@ -376,6 +380,9 @@
<text name="spu_col_to" text="Til"/> <text name="spu_col_to" text="Til"/>
<text name="spu_col_tractors" text="Kører"/> <text name="spu_col_tractors" text="Kører"/>
<text name="spu_col_status" text="Status"/> <text name="spu_col_status" text="Status"/>
<text name="spu_board_chainIdleShow" text="%d kædeflows venter på en ledig traktor/vogn — klik for at vise"/>
<text name="spu_board_chainIdleHide" text="%d kædeflows uden traktor — klik for at skjule"/>
<text name="spu_board_chain" text="· Kædernes egne flows (oprettes af fanen Kæder) ·"/>
<text name="spu_board_supply" text="· Ind til bygning ·"/> <text name="spu_board_supply" text="· Ind til bygning ·"/>
<text name="spu_board_outbound" text="· Ud fra bygning ·"/> <text name="spu_board_outbound" text="· Ud fra bygning ·"/>
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/> <text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
@@ -513,6 +520,7 @@
<text name="spu_guide_flow_none" text="ingen valgt"/> <text name="spu_guide_flow_none" text="ingen valgt"/>
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/> <text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
<text name="spu_guide_flow_sumGoods" text="%d varer"/> <text name="spu_guide_flow_sumGoods" text="%d varer"/>
<text name="spu_guide_flow_sumSellAlways" text="sælg altid"/>
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/> <text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
<text name="spu_guide_flow_sumStoreAbove" text="lager over %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_sumSellAbove" text="sælg altid over %d %%"/>
@@ -574,5 +582,80 @@
<text name="spu_item_fullIn" text="fuld om %s"/> <text name="spu_item_fullIn" text="fuld om %s"/>
<text name="spu_item_balanced" text="i balance"/> <text name="spu_item_balanced" text="i balance"/>
<text name="spu_item_share" text="%d %% af varen"/> <text name="spu_item_share" text="%d %% af varen"/>
<text name="spu_tab_chains" text="Kæder"/>
<text name="spu_chain_noFlow" text="Intet flow kører den vare endnu — kæden opretter det, når der er en ledig traktor/vogn"/>
<text name="spu_chain_optIn" text="[Kæde til]"/>
<text name="spu_chain_optOut" text="[Fjern]"/>
<text name="spu_chain_notChosen" text="ingen kæde"/>
<text name="spu_chain_candidatesShow" text="%d tændte produktioner uden kæde — klik for at vise"/>
<text name="spu_chain_candidatesHide" text="%d tændte produktioner uden kæde — klik for at skjule"/>
<text name="spu_chain_finalFarm" text="Gård"/>
<text name="spu_chain_finalSell" text="Sælg"/>
<text name="spu_chain_finalKeep" text="Behold"/>
<text name="spu_chain_suggestHeader" text="Forslag til ny afgrøde"/>
<text name="spu_chain_empty" text="Ingen kæder: slå et produkt til i en fabrik"/>
<text name="spu_chain_col_item" text="Vare"/>
<text name="spu_chain_col_source" text="Kilde"/>
<text name="spu_chain_col_perMonth" text="l/md"/>
<text name="spu_chain_col_status" text="Status"/>
<text name="spu_chain_col_stock" text="Lager"/>
<text name="spu_chain_approve" text="Forslag [Godkend]"/>
<text name="spu_chain_dismiss" text="[Afvis]"/>
<text name="spu_chain_auto" text="skiftet (fri mark)"/>
<text name="spu_chain_gone" text="Forslaget gælder ikke længere"/>
<text name="spu_chain_flowOk" text="flow kører"/>
<text name="spu_chain_flowMissing" text="mangler traktor/vogn"/>
<text name="spu_chain_flowNone" text="intet flow"/>
<text name="spu_chain_flowAuto" text="fordeles af spillet"/>
<text name="spu_chain_flowPallet" text="palle: vælg UAL-vogn"/>
<text name="spu_chain_flowStopped" text="flow stoppet"/>
<text name="spu_chain_bottleneck" text="flaskehals: -%s/md"/>
<text name="spu_chain_buy" text="skal købes: %s/md"/>
<text name="spu_chain_cycle" text="kredsløb"/>
<text name="spu_chain_fieldShort" text="mangler %s ha"/>
<text name="spu_chain_tight" text="lager &lt; 3 md"/>
<text name="spu_chain_noField" text="ingen mark"/>
<text name="spu_chain_buySource" text="køb"/>
<text name="spu_chain_fromFields" text="fra marker"/>
<text name="spu_chain_notifyMore" text="+%d manglende led — se fanen Kæder"/>
<text name="spu_chain_pause" text="[Pause]"/>
<text name="spu_chain_resume" text="[Start]"/>
<text name="spu_chain_rootOk" text="dækket"/>
<text name="spu_chain_rootError" text="mangel"/>
<text name="spu_chain_rootWaiting" text="venter"/>
<text name="spu_chain_rootPaused" text="på pause"/>
<text name="spu_chain_rootSummary" text="%s · %d forslag"/>
<text name="spu_chain_free" text="Marker fri til kæden: %s"/>
<text name="spu_chain_switches" text="Automatiske skift siden indlæsning: %d"/>
<text name="spu_chain_scanning" text="Marker scannes endnu – kæden skifter ingen marker før alle er set"/>
<text name="spu_attention_chainSuggests" text="Kæde: %d forslag"/>
<text name="spu_attention_hintChain" text=" → klik: se Kæder"/>
<text name="spu_chain_problem_buy" text="Ingen af dine fabrikker laver %s — byg en eller køb"/>
<text name="spu_chain_problem_barn" text="Dine stalde leverer ikke nok %s — køb resten eller flere dyr"/>
<text name="spu_chain_problem_bottleneck" text="%s kan kun lave %s af %s/md"/>
<text name="spu_chain_problem_off" text="%s (%s) er slukket af dig — tænd den eller sæt kæden på pause"/>
<text name="spu_chain_problem_cycle" text="Kredsløb omkring %s — kæden kan ikke beregnes"/>
<text name="spu_attention_chainProblems" text="Kæder: %d manglende led"/>
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: statusvindue"/>
<text name="spu_status_flows" text="Flows"/>
<text name="spu_status_fields" text="Marker"/>
<text name="spu_status_chains" text="Kæder"/>
<text name="spu_status_warnings" text="Advarsler"/>
<text name="spu_status_more" text="+%d mere"/>
<text name="spu_status_idle" text="Smart Pickup: intet kører lige nu"/>
<text name="spu_status_act_fetch" text="henter %s ved %s"/>
<text name="spu_status_act_driving" text="kører til %s"/>
<text name="spu_status_act_waiting" text="venter ved %s"/>
<text name="spu_status_act_holding" text="holder ved %s"/>
<text name="spu_status_act_fetchWagon" text="henter vogn"/>
<text name="spu_status_act_waitWagon" text="venter på vogn"/>
<text name="spu_status_act_emptying" text="tømmer vognen"/>
<text name="spu_status_act_homing" text="på vej hjem"/>
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
<text name="spu_dep_warn" text="Advarsel: du slukkede %s (%s) — %s bruger det. Lageret rækker ca. %d dage."/>
<text name="spu_dep_warnNoDays" text="Advarsel: du slukkede %s (%s) — %s bruger det."/>
<text name="spu_attention_depOff" text="Slukket men nødvendig: %s"/>
<text name="spu_attention_depOffMore" text="Slukket men nødvendig: %s +%d"/>
<text name="spu_attention_hintFactories" text=" → klik: se Fabrikker"/>
</texts> </texts>
</l10n> </l10n>
+84 -1
View File
@@ -151,7 +151,7 @@
<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, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/> <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_rig_working" text="working on F%s"/>
<text name="spu_field_tooWideShort" text="!! over 50 m"/> <text name="spu_field_tooWideShort" text="course 50 m"/>
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/> <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"/>
@@ -167,6 +167,7 @@
<text name="spu_field_careNeed" text="needs %s"/> <text name="spu_field_careNeed" text="needs %s"/>
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/> <text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
<text name="spu_field_lime" text="Lime when the soil needs it"/> <text name="spu_field_lime" text="Lime when the soil needs it"/>
<text name="spu_field_chainFree" text="Free for chains (mod may change the crop)"/>
<text name="spu_field_mulch" text="Mulch stubble after harvest"/> <text name="spu_field_mulch" text="Mulch stubble after harvest"/>
<text name="spu_field_need_fert" text="fertilizer"/> <text name="spu_field_need_fert" text="fertilizer"/>
<text name="spu_field_need_lime" text="lime"/> <text name="spu_field_need_lime" text="lime"/>
@@ -309,6 +310,7 @@
<text name="spu_policy_sell" text="Sell"/> <text name="spu_policy_sell" text="Sell"/>
<text name="spu_policy_sellElseStore" text="Sell, else store"/> <text name="spu_policy_sellElseStore" text="Sell, else store"/>
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/> <text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
<text name="spu_sellAt_always" text="Always (any price)"/>
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/> <text name="spu_sellAt" 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"/>
@@ -349,6 +351,8 @@
<text name="spu_flow_noSource" text="(no source)"/> <text name="spu_flow_noSource" text="(no source)"/>
<text name="spu_flow_noTarget" text="(no target)"/> <text name="spu_flow_noTarget" text="(no target)"/>
<text name="spu_flow_noVehicle" text="(no tractor)"/> <text name="spu_flow_noVehicle" text="(no tractor)"/>
<text name="spu_flow_ruleSellAlways" text="always sell"/>
<text name="spu_flow_chainIdle" text="needs a tractor/trailer"/>
<text name="spu_flow_ruleSell" text="price >= %d %%"/> <text name="spu_flow_ruleSell" text="price >= %d %%"/>
<text name="spu_flow_ruleStore" text="storage > %d %%"/> <text name="spu_flow_ruleStore" text="storage > %d %%"/>
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/> <text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
@@ -376,6 +380,9 @@
<text name="spu_col_to" text="To"/> <text name="spu_col_to" text="To"/>
<text name="spu_col_tractors" text="Running"/> <text name="spu_col_tractors" text="Running"/>
<text name="spu_col_status" text="Status"/> <text name="spu_col_status" text="Status"/>
<text name="spu_board_chainIdleShow" text="%d chain flows are waiting for a free tractor/trailer — click to show"/>
<text name="spu_board_chainIdleHide" text="%d chain flows without a tractor — click to hide"/>
<text name="spu_board_chain" text="· Flows created by chains (Chains tab) ·"/>
<text name="spu_board_supply" text="· Into building ·"/> <text name="spu_board_supply" text="· Into building ·"/>
<text name="spu_board_outbound" text="· Out of building ·"/> <text name="spu_board_outbound" text="· Out of building ·"/>
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/> <text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
@@ -513,6 +520,7 @@
<text name="spu_guide_flow_none" text="none chosen"/> <text name="spu_guide_flow_none" text="none chosen"/>
<text name="spu_guide_flow_goodsAuto" text="Automatic"/> <text name="spu_guide_flow_goodsAuto" text="Automatic"/>
<text name="spu_guide_flow_sumGoods" text="%d goods"/> <text name="spu_guide_flow_sumGoods" text="%d goods"/>
<text name="spu_guide_flow_sumSellAlways" text="always sell"/>
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/> <text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
<text name="spu_guide_flow_sumStoreAbove" text="store above %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_sumSellAbove" text="always sell above %d %%"/>
@@ -574,5 +582,80 @@
<text name="spu_item_fullIn" text="full in %s"/> <text name="spu_item_fullIn" text="full in %s"/>
<text name="spu_item_balanced" text="balanced"/> <text name="spu_item_balanced" text="balanced"/>
<text name="spu_item_share" text="%d %% of the good"/> <text name="spu_item_share" text="%d %% of the good"/>
<text name="spu_tab_chains" text="Chains"/>
<text name="spu_chain_noFlow" text="No flow carries that good yet — the chain creates one when a tractor/trailer is free"/>
<text name="spu_chain_optIn" text="[Chain on]"/>
<text name="spu_chain_optOut" text="[Remove]"/>
<text name="spu_chain_notChosen" text="no chain"/>
<text name="spu_chain_candidatesShow" text="%d productions on without a chain — click to show"/>
<text name="spu_chain_candidatesHide" text="%d productions on without a chain — click to hide"/>
<text name="spu_chain_finalFarm" text="Farm"/>
<text name="spu_chain_finalSell" text="Sell"/>
<text name="spu_chain_finalKeep" text="Keep"/>
<text name="spu_chain_suggestHeader" text="Crop change suggestions"/>
<text name="spu_chain_empty" text="No chains: enable a product in a factory"/>
<text name="spu_chain_col_item" text="Goods"/>
<text name="spu_chain_col_source" text="Source"/>
<text name="spu_chain_col_perMonth" text="l/mo"/>
<text name="spu_chain_col_status" text="Status"/>
<text name="spu_chain_col_stock" text="Stock"/>
<text name="spu_chain_approve" text="Suggest [Approve]"/>
<text name="spu_chain_dismiss" text="[Reject]"/>
<text name="spu_chain_auto" text="switched (free field)"/>
<text name="spu_chain_gone" text="The suggestion no longer applies"/>
<text name="spu_chain_flowOk" text="flow running"/>
<text name="spu_chain_flowMissing" text="no tractor/trailer"/>
<text name="spu_chain_flowNone" text="no flow"/>
<text name="spu_chain_flowAuto" text="distributed by game"/>
<text name="spu_chain_flowPallet" text="pallet: UAL trailer"/>
<text name="spu_chain_flowStopped" text="flow stopped"/>
<text name="spu_chain_bottleneck" text="bottleneck: -%s/mo"/>
<text name="spu_chain_buy" text="must buy: %s/mo"/>
<text name="spu_chain_cycle" text="loop"/>
<text name="spu_chain_fieldShort" text="short %s ha"/>
<text name="spu_chain_tight" text="stock &lt; 3 mo"/>
<text name="spu_chain_noField" text="no field"/>
<text name="spu_chain_buySource" text="buy"/>
<text name="spu_chain_fromFields" text="from fields"/>
<text name="spu_chain_notifyMore" text="+%d more missing links — see the Chains tab"/>
<text name="spu_chain_pause" text="[Pause]"/>
<text name="spu_chain_resume" text="[Resume]"/>
<text name="spu_chain_rootOk" text="covered"/>
<text name="spu_chain_rootError" text="short"/>
<text name="spu_chain_rootWaiting" text="waiting"/>
<text name="spu_chain_rootPaused" text="paused"/>
<text name="spu_chain_rootSummary" text="%s · %d sugg."/>
<text name="spu_chain_free" text="Fields free for the chain: %s"/>
<text name="spu_chain_switches" text="Automatic switches since load: %d"/>
<text name="spu_chain_scanning" text="Fields still being scanned – no field changes until all are seen"/>
<text name="spu_attention_chainSuggests" text="Chain: %d suggestions"/>
<text name="spu_attention_hintChain" text=" → click: see Chains"/>
<text name="spu_chain_problem_buy" text="None of your factories makes %s — build one or buy it"/>
<text name="spu_chain_problem_barn" text="Barns make too little %s — buy the rest or more animals"/>
<text name="spu_chain_problem_bottleneck" text="%s can only make %s of %s/mo"/>
<text name="spu_chain_problem_off" text="%s (%s) was turned off by you — turn it on or pause the chain"/>
<text name="spu_chain_problem_cycle" text="Loop around %s — the chain cannot be calculated"/>
<text name="spu_attention_chainProblems" text="Chains: %d missing links"/>
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: status window"/>
<text name="spu_status_flows" text="Flows"/>
<text name="spu_status_fields" text="Fields"/>
<text name="spu_status_chains" text="Chains"/>
<text name="spu_status_warnings" text="Warnings"/>
<text name="spu_status_more" text="+%d more"/>
<text name="spu_status_idle" text="Smart Pickup: nothing running right now"/>
<text name="spu_status_act_fetch" text="fetching %s at %s"/>
<text name="spu_status_act_driving" text="driving to %s"/>
<text name="spu_status_act_waiting" text="waiting at %s"/>
<text name="spu_status_act_holding" text="holding at %s"/>
<text name="spu_status_act_fetchWagon" text="fetching wagon"/>
<text name="spu_status_act_waitWagon" text="waiting for wagon"/>
<text name="spu_status_act_emptying" text="emptying the wagon"/>
<text name="spu_status_act_homing" text="heading home"/>
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
<text name="spu_dep_warn" text="Warning: you switched off %s (%s) — %s uses it. Stock lasts about %d days."/>
<text name="spu_dep_warnNoDays" text="Warning: you switched off %s (%s) — %s uses it."/>
<text name="spu_attention_depOff" text="Switched off but needed: %s"/>
<text name="spu_attention_depOffMore" text="Switched off but needed: %s +%d"/>
<text name="spu_attention_hintFactories" text=" → click: see Factories"/>
</texts> </texts>
</l10n> </l10n>
+23 -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.60.14.0</version> <version>1.62.7.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
@@ -31,6 +31,9 @@
<sourceFile filename="adSources.lua"/> <sourceFile filename="adSources.lua"/>
<sourceFile filename="adBuildings.lua"/> <sourceFile filename="adBuildings.lua"/>
<sourceFile filename="adOutbound.lua"/> <sourceFile filename="adOutbound.lua"/>
<sourceFile filename="adTipSide.lua"/>
<sourceFile filename="adLoadDiag.lua"/>
<sourceFile filename="adSellBlock.lua"/>
<sourceFile filename="adUnloadWait.lua"/> <sourceFile filename="adUnloadWait.lua"/>
<sourceFile filename="adFieldModel.lua"/> <sourceFile filename="adFieldModel.lua"/>
<sourceFile filename="adFieldFleet.lua"/> <sourceFile filename="adFieldFleet.lua"/>
@@ -54,6 +57,25 @@
<sourceFile filename="adFieldJobs.lua"/> <sourceFile filename="adFieldJobs.lua"/>
<sourceFile filename="adMonthView.lua"/> <sourceFile filename="adMonthView.lua"/>
<sourceFile filename="adProfiler.lua"/> <sourceFile filename="adProfiler.lua"/>
<sourceFile filename="adChainModel.lua"/>
<sourceFile filename="adCropPlan.lua"/>
<sourceFile filename="adChainActions.lua"/>
<sourceFile filename="adDependency.lua"/>
<sourceFile filename="adChainData.lua"/>
<sourceFile filename="adChain.lua"/>
<sourceFile filename="adChainRun.lua"/>
<sourceFile filename="adChainView.lua"/>
<sourceFile filename="adStatusView.lua"/>
<sourceFile filename="adStatusHud.lua"/>
<sourceFile filename="adSmartPickup.lua"/> <sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles> </extraSourceFiles>
<actions>
<action name="SPU_TOGGLE_STATUS"/>
</actions>
<inputBinding>
<actionBinding action="SPU_TOGGLE_STATUS">
<!-- numpad * (Insert bruges af FS25_UniversalAutoload) -->
<binding device="KB_MOUSE_DEFAULT" input="KEY_KP_multiply"/>
</actionBinding>
</inputBinding>
</modDesc> </modDesc>
+15 -1
View File
@@ -16,11 +16,16 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
startes eller stoppes samlet fra en farvet liste. startes eller stoppes samlet fra en farvet liste.
- **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og - **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og
sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje. sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje.
- **Produktionskæder:** slå et produkt til i en fabrik, og moden holder det kørende tilbage til marken. Fanen Kæder viser træet, marker markeret "Fri til kæden" skifter afgrøde automatisk, og andre marker får forslag, du godkender eller afviser. Mellemled og flows oprettes efter behov.
- **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen, - **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen,
og Courseplay arbejder på marken. og Courseplay arbejder på marken.
- **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre. - **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre.
- **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter - **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter
det flow, der løser problemet. det flow, der løser problemet.
- **Statusvindue:** numpad \* viser et lille vindue øverst til højre med flow-traktorernes handling og fyld,
markarbejde i gang, kæder med manglende led og advarsler.
- **Advarsel når en nødvendig fabrik slukkes:** slukker du en produktion, som et flow, en stald eller en kæde
bruger, advarer moden og viser hvor længe lageret rækker.
- **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes - **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes
med savegamet. med savegamet.
@@ -70,17 +75,23 @@ is set up in the **Smart Pickup** menu in the ESC menu.
start or stop them from a colour-coded list. start or stop them from a colour-coded list.
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect - **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
marker and park it again. Detached implements and free tractors form a shared pool. marker and park it again. Detached implements and free tractors form a shared pool.
- **Production chains:** switch on a product in a factory and the mod keeps it running back to the field. The Chains tab shows the tree, fields marked "Free for chains" change crop automatically, and other fields get suggestions you approve or reject. Intermediate goods and flows are set up as needed.
- **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive - **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive
drives the roads and Courseplay works the field. drives the roads and Courseplay works the field.
- **Field silos:** goods in field silos are brought home to the farm's storages automatically. - **Field silos:** goods in field silos are brought home to the farm's storages automatically.
- **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list - **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list
creates the flow that fixes the problem. creates the flow that fixes the problem.
- **Status window:** numpad \* shows a small window in the top right with the flow tractors' action and
fill, field work in progress, chains with missing links and warnings.
- **Warning when a needed factory is switched off:** switch off a production that a flow, a barn or a chain
uses, and the mod warns you and shows how long the stock lasts.
- **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved - **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved
with the savegame. with the savegame.
**Install:** download `FS25_ADSmartPickup.zip` from **Install:** download `FS25_ADSmartPickup.zip` from
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked, [Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
in `Documents/My Games/FarmingSimulator2025/mods/`. in `Documents/My Games/FarmingSimulator2025/mods/`. Enable the mod together with AutoDrive when you pick
the savegame. The **Smart Pickup** menu is in the ESC menu, just above Settings.
**Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is **Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is
needed for pallets. needed for pallets.
@@ -90,3 +101,6 @@ needed for pallets.
**Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with **Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with
`ADSmartPickup`. `ADSmartPickup`.
**Development:** the mod code is in `FS25_ADSmartPickup/`. Build the zip with
`cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
+187 -5
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — brugermanual # AD Smart Pickup — brugermanual
Version 1.60.11 · Farming Simulator 25 · tillæg til AutoDrive Version 1.61.0 · Farming Simulator 25 · tillæg til AutoDrive
*English version: [USER_MANUAL.md](USER_MANUAL.md)* *English version: [USER_MANUAL.md](USER_MANUAL.md)*
@@ -93,8 +93,10 @@ til tom*: den vare, der slipper op først, hentes først.
**Reserve.** Det, gårdens egne stalde og fabrikker skal bruge det næste døgn. Reserven sælges aldrig. **Reserve.** Det, gårdens egne stalde og fabrikker skal bruge det næste døgn. Reserven sælges aldrig.
**Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et **fast sæt** er et **Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et redskab eller en
redskab, der ikke har en Connect-markør. Det bliver siddende og udlånes aldrig. vogn er **bundet** og udlånes aldrig, når det hører til et flow (flowets traktorer og vogne, også når flowet
er stoppet), eller når det ikke har en Connect-markør (et **fast sæt**, fx en vintraktor med sit
vinredskab).
**Maskinpulje.** Frakoblede redskaber og vogne med parkering og Connect-markør samt ledige traktorer uden **Maskinpulje.** Frakoblede redskaber og vogne med parkering og Connect-markør samt ledige traktorer uden
redskab. Moden parrer dem selv, når der er brug for det. redskab. Moden parrer dem selv, når der er brug for det.
@@ -129,6 +131,12 @@ Det er **mappens** navn, der tæller. En markør, der hedder *F91 Wait* i en mar
Wait-punkt. Wait-markører bliver aldrig brugt som kilde eller mål. Et Wait-punkt regnes som optaget, Wait-punkt. Wait-markører bliver aldrig brugt som kilde eller mål. Et Wait-punkt regnes som optaget,
når det er reserveret, eller når et andet køretøj holder højst 6 m fra det. når det er reserveret, eller når et andet køretøj holder højst 6 m fra det.
> **Vigtigt: parkering og hentning virker kun ved at bakke.** Traktoren skal **bakke** redskabet ind på
> parkeringen og **bakke** hen til trækøjet, når den henter det. Lav derfor AutoDrive-ruten til både
> `P <navn>` og `Connect P <navn>` som en **bakrute** (AutoDrives bak-forbindelse) det sidste stykke, så
> traktoren står med krogen mod redskabets trækøje. Kører traktoren forlæns ind, vender krogen væk fra
> redskabet, og så kan den hverken sætte det rigtigt af eller koble på.
### Parkering: "P <navn>" ### Parkering: "P <navn>"
Moden bruger AutoDrives egen **parkeringsdestination** på hvert køretøj og redskab (sættes i AutoDrive). Moden bruger AutoDrives egen **parkeringsdestination** på hvert køretøj og redskab (sættes i AutoDrive).
@@ -149,7 +157,8 @@ En **Connect-markør** er stedet, hvor traktoren holder for at koble et redskab
- Kører du selv en traktor med AutoDrives "Kør til" til en Connect-markør, kobler moden også på. - Kører du selv en traktor med AutoDrives "Kør til" til en Connect-markør, kobler moden også på.
- Omdøber du en parkering, skal Connect-markøren omdøbes med. - Omdøber du en parkering, skal Connect-markøren omdøbes med.
Et redskab eller en vogn **uden** Connect-markør er et fast sæt og bliver aldrig lånt ud. Et redskab eller en vogn **uden** Connect-markør er et fast sæt og bliver aldrig lånt ud. Det samme
gælder et flows vogne, selv om de har en Connect-markør (se [Flow-vogne](#flow-vogne)).
### Afkobling: "Afkobl" ### Afkobling: "Afkobl"
@@ -330,6 +339,27 @@ Sæt **Ja** ud for de bygninger, flowet skal bruge. Bygninger uden markør er sk
**Vis alle bygninger** til. Listen er sorteret efter navn, så *Silo 9* kommer før *Silo 10*. En bygning, **Vis alle bygninger** til. Listen er sorteret efter navn, så *Silo 9* kommer før *Silo 10*. En bygning,
du har solgt, står som *Fjernet bygning (findes ikke mere) — sæt NEJ*. du har solgt, står som *Fjernet bygning (findes ikke mere) — sæt NEJ*.
### Statusvindue
Tryk **numpad \*** (gangetegnet på det numeriske tastatur) for at slå et lille statusvindue til eller fra øverst til højre på skærmen, lige under
spillets info-boks. Tasten virker både til fods og i et køretøj og kan ændres under *Indstillinger → Styring*
(*AD Smart Pickup: statusvindue*). Vinduet opdateres hvert sekund og viser kun de afsnit, der har indhold:
- **Flows** — én linje pr. traktor i et kørende flow: *traktor · handling · fyld %* (fx *Milk Collect 1 ·
henter Mælk ved Kostald · 45 %*). Grøn = kører, gul = venter (Wait, vogn), rød = flowet har en advarsel,
grå = stoppet.
- **Marker** — én linje pr. mark med markarbejde i gang: *F92 Høstes · Roeoptager · 45 %* (farve som i Marker-fanen).
- **Kæder** — én linje, når kæder mangler et led: *Kæder: 3 manglende led*.
- **Advarsler** — fabrikker gården er afhængig af, men som er slukket (*Slukket men nødvendig: Fermenteringssilo 3*,
se [Advarsel når en nødvendig fabrik slukkes](#advarsel-når-en-nødvendig-fabrik-slukkes)), køretøjer der har
stået stille i over 2 minutter (uden at Smart Pickup selv holder dem ved Wait/udkørsel; solgte køretøjer vises
ikke) og ventende kædeforslag (*Kæde: 2 forslag*).
Højst 18 linjer; resten samles i *+k mere*. Vinduet skjules, mens en menu eller dialog er åben, når HUD'en er
skjult, og når du sover. Om vinduet er tændt, gemmes pr. bruger i
`Dokumenter/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (ikke i savegamet);
standard er slukket.
--- ---
## 5. Forsyning: hold stalde og fabrikker fyldt ## 5. Forsyning: hold stalde og fabrikker fyldt
@@ -606,6 +636,151 @@ Markflådens visning i fanen **Marker** viser puljetraktorer og puljeredskaber m
--- ---
### Produktionskæder
> **Vigtigt:** Hver produktion, du tænder i en fabrik, regner moden som noget, du vil have produceret. Moden
> prøver så at forsyne den: marker markeret fri skifter til de afgrøder, der mangler, andre marker får
> forslag, mellemled tændes, og der oprettes flows med traktorer fra maskinpuljen. Tænd derfor **kun**
> produktioner, du faktisk vil have kørende. Vil du stoppe en kæde, så sluk produktionen eller tryk
> **[Pause]** i fanen Kæder.
#### Hvad er en kæde?
En **kæde** holder en produktion kørende helt tilbage til marken. Du slår et produkt til i en fabrik. Så
regner moden baglæns: hvad skal produktionen bruge, hvilke af dine egne fabrikker og stalde laver det, og
hvilke afgrøder skal der til. Fabrikken, du slog til, kaldes **rod-produktionen**.
- Moden ser 12 måneder frem. Den gør først noget, når afgrøden mangler mere end 10 % og mere end 0,5 ha.
- Marker, du har markeret **Fri til kæden**, skifter afgrøde af sig selv (højst 2 skift i minuttet).
- Andre marker får et **forslag**, som du godkender eller afviser.
- En afgrøde, du selv har valgt på en mark, ændres aldrig automatisk.
- Kæden gør ingenting de første 2 minutter efter indlæsning. Kan moden ikke læse alle data, slukkes eller
stoppes intet.
- Marker skiftes og foreslås først, når markscanningen har set alle dine marker. Med mange marker kan det tage
et par minutter; imens står der *Marker scannes endnu* nederst i fanen.
- Moden bruger først dine egne tændte produktioner som kilde. Den tænder kun en ny, hvis de ikke kan nok.
#### Fanen Kæder
Fanen **Kæder** viser et træ pr. rod-produktion: øverst rod-produktionen, under den hvert led med
kolonnerne **Vare**, **Kilde**, **l/md**, **Status** og **Lager**. Er der ingen kæder, står der *Ingen kæder:
slå et produkt til i en fabrik*.
- Rod-produktionens status er *dækket*, *mangel*, *venter* eller *på pause*. Efter navnet står antal
forslag.
- En farvet prik viser status: **grøn** kører, **gul** venter, **rød** fejl, **grå** stoppet. Statusteksten
får samme farve.
- Rækkerne folder ud og ind. Fold ud for at se leddene, kilderne og markerne.
- Advarsler i rækkerne: *flaskehals: -X/md*, *skal købes: X/md*, *kredsløb*, *mangler X ha*, *lager < 3 md*
og *ingen mark*.
- Markvarer, der ikke er en afgrødes hovedvare — fx hø, tørret lucerne og kløver, hakkelse, helsæd og halm —
står med kilden *fra marker*. Dem høster eller slår du selv, så kæden laver ingen afgrødeplan og ingen
advarsel for dem; lageret står i kolonnen **Lager**.
- **[Pause]** stopper kædens nye handlinger for den rod-produktion. Det, den bruger, slukkes ikke. Andre
kæder kører videre. **[Start]** genoptager den.
- Nederst står *Automatiske skift siden indlæsning*.
#### Manglende led (røde beskeder)
Mangler et led i en kæde, forklarer moden hvorfor og hvad du kan gøre. Teksten står tre steder:
- **I fanen Kæder**: fold kæden ud; under den røde vare står årsagen i rødt.
- **I Kræver handling**: én samlet rød linje for alle kæder, fx *Kæder: 3 manglende led*. Den står under
lagerets røde linjer (løber tør / næsten fuldt). Et klik åbner fanen Kæder med kæderne foldet ud.
- **Som besked på skærmen**: første gang et nyt led mangler, med kædens navn foran (fx *Brød: …*). Samme besked kommer først igen, når problemet har
været væk og vender tilbage. Der kommer højst 3 beskeder ad gangen; resten samles i én besked: *+5 manglende led — se fanen
Kæder*. De første 2 minutter efter indlæsning og mens markerne scannes, sendes ingen besked.
Beskederne:
- *Ingen af dine fabrikker laver Mel — byg en eller køb*: ingen fabrik, stald eller mark leverer varen.
- *Dine stalde leverer ikke nok Mælk — køb resten eller flere dyr*: stalden laver varen, men for lidt.
- *Mølle kan kun lave 800 af 1.0k/md*: fabrikken kan ikke følge med (flaskehals).
- *Mølle (Mel) er slukket af dig — tænd den eller sæt kæden på pause*: du har selv slukket produktionen.
- *Kredsløb omkring Mel — kæden kan ikke beregnes*: varen bruges til at lave sig selv.
#### Advarsel når en nødvendig fabrik slukkes
Slukker du en produktion, som gården er afhængig af, advarer moden dig — også når ingen kæde er i gang. En
produktion er nødvendig, når en af dens varer bruges af gården:
- et tændt forsyningsflow har varen valgt som vare,
- et tændt flow i auto kører til et mål, der tager varen, og gården forbruger varen,
- en stald æder varen lige nu, eller
- en aktiv kæde har varen som input (bruger kun kæder varen, meldes det i stedet som manglende led, se ovenfor).
Ved flows i auto og stalde skal gårdens forbrug af varen kendes; ellers regnes varen ikke som nødvendig.
Eksempel: du slukker fermenteringssiloerne, men flowene *Cow Food*, *Goat Food* og *Biogas Silage* henter
ensilage. Så kommer beskeden *Advarsel: du slukkede Fermenteringssilo 3 (Ensilage) — Cow Food, Goat Food +1
bruger det. Lageret rækker ca. 12 dage.* (dagene ud fra gårdens lager og forbrug; kendes de ikke, udelades
de). Beskeden kommer én gang pr. slukning, højst et minut efter.
Så længe produktionen er slukket og stadig nødvendig, står der en rød linje i **Kræver handling**:
*Slukket men nødvendig: Fermenteringssilo 3* (højst én linje — den mest hastende; er flere slukket, står
antallet bagefter: *Slukket men nødvendig: Fermenteringssilo 3 +2*; også i statusvinduet under Advarsler). Et klik åbner
lagerfanen **Fabrikker**.
Der advares ikke, når:
- en anden opskrift i samme fabrik laver varen og er tændt (du har valgt, hvilken opskrift der kører — fx én
ensilage-opskrift i en fermenteringssilo),
- en anden tændt produktion laver varen med nok kapacitet,
- gården har lager til mere end 60 dage,
- kæden selv slukkede produktionen, eller
- produktionen allerede var slukket, da spillet blev indlæst (så vises kun linjen i Kræver handling).
Produktioner, som ingen flows, stalde eller kæder bruger (fx et bageri uden flow), giver aldrig en advarsel.
#### Markér en mark fri til kæden
Åbn marken i markguiden og slå **Fri til kæden (moden må skifte afgrøde)** til. Kun sådan en mark skifter
afgrøde automatisk. I fanen står **Marker fri til kæden** med de marker, der er markeret.
Flerårige afgrøder (vin, oliven, poppel) og græsmarker skiftes aldrig, heller ikke når de er markeret.
#### Forslag: Godkend og Afvis
Mangler kæden areal på en mark, der ikke er fri, står der et forslag i kæde-træet: **Forslag [Godkend]**
og **[Afvis]**. Der vises højst 3 forslag pr. afgrøde. I **Kræver handling** står ét samlet punkt, fx
*Kæde: 3 forslag*; et klik åbner fanen Kæder.
- **[Godkend]** skifter markens afgrøde.
- **[Afvis]** fjerner forslaget. Det kommer igen, hvis afgrødens mangel ændrer sig mere end 25 %.
- Er forslaget forældet, står der *Forslaget gælder ikke længere*.
- Marker, moden har skiftet på egen hånd, er mærket *skiftet (fri mark)*.
#### Mellemled
Kræver en kæde en mellemvare (fx mel til brød), tænder moden den produktion, der laver den.
- Moden slukker kun en produktion igen, hvis kæden selv tændte den.
- Slukker du selv en produktion, som en kæde bruger (også en, du selv tændte), lader kæden den være slukket
og regner den ikke som kilde. Først når du tænder en ny produktion i en fabrik, må kæden bruge den igen.
- Slukker du rod-produktionen, stoppes kædens flows (se nedenfor), medmindre en anden kæde stadig bruger
dem.
#### Kæde-flows
For hvert led opretter moden et flow med navnet **Kæde: <vare> → <bygning>** (højst 2 nye i minuttet).
- Flowet får en ledig pool-traktor og en tipvogn. Moden bruger aldrig en traktor, der hører til et andet
flow.
- Findes der ingen, oprettes flowet stoppet, og kæden viser *mangler traktor/vogn*.
- Flows med paller oprettes altid stoppet, og kæden viser *palle: vælg UAL-vogn*: vælg selv en UAL-vogn.
- Slås kæden fra, **stoppes** flowene, og kæden slipper dem. De slettes ikke. Starter du et igen, lader
kæden det køre.
- Status i kæden: *flow kører*, *flow stoppet*, *mangler traktor/vogn*, *palle: vælg UAL-vogn* eller
*intet flow*.
#### Begrænsninger
- Hver produktion, du tænder i en fabrik, prøver moden at forsyne. Tænd kun det, du vil have produceret.
- Moden **køber ikke** varer og **køber ikke dyr**. Skal noget købes, står det som *skal købes*.
- Flerårige afgrøder (vin, oliven, poppel) og græsmarker skiftes aldrig.
- Planen bruger grundudbyttet uden gødningsbonus, så den regner hellere med for mange hektar end for få.
- Kæden bruger kun dine egne fabrikker og stalde.
## 8. Start, stop og gemning ## 8. Start, stop og gemning
**Start flow** gør følgende på hver godkendt traktor: **Start flow** gør følgende på hver godkendt traktor:
@@ -664,6 +839,8 @@ ens. Øverst vælger du **Vis: Varer | Bygninger**. Fanen husker valget. Tallene
| *Hvede løber tør om … — gården har …, bruger …/døgn* | Gården bruger mere, end den producerer, og varen rækker under 3 døgn (rød under 1 døgn) | | *Hvede løber tør om … — gården har …, bruger …/døgn* | Gården bruger mere, end den producerer, og varen rækker under 3 døgn (rød under 1 døgn) |
| *Mælk er 92 % fuld — fuld om …* | En produceret vare er mindst 85 % fuld (rød fra 95 %) | | *Mælk er 92 % fuld — fuld om …* | En produceret vare er mindst 85 % fuld (rød fra 95 %) |
| *Sælg Raps nu: prisen er 94 % af årets top — …* | Prisen er mindst 90 % af årets top, beholdningen er værd mindst 10.000, og gården bruger den ikke op | | *Sælg Raps nu: prisen er 94 % af årets top — …* | Prisen er mindst 90 % af årets top, beholdningen er værd mindst 10.000, og gården bruger den ikke op |
| *Kæder: 3 manglende led* | En eller flere kæder mangler et led (se [Manglende led](#manglende-led-røde-beskeder)); står under lagerets røde linjer; klik åbner fanen Kæder |
| *Slukket men nødvendig: Fermenteringssilo 3 +2* | Du har slukket en produktion, som et flow, en stald eller en kæde bruger (se [Advarsel når en nødvendig fabrik slukkes](#advarsel-når-en-nødvendig-fabrik-slukkes)); klik åbner fanen Fabrikker |
**Klik på en linje** for at handle: **Klik på en linje** for at handle:
@@ -1029,6 +1206,7 @@ FS25 kan ikke selv omdøbe bygninger, så moden har sine egne navne. De gemmes i
| `spuNameFleet` | Giver alle traktorer og lastbiler uden for flows butiksnavnet (*Fendt 942 1*, *Fendt 942 2* …) og omdøber deres egen parkering til `P <navn>`, hvis ingen anden deler den. Overskriver egne navne på de køretøjer. Gem spillet bagefter. Pas på: en eksisterende `Connect <gammelt navn>`-markør passer ikke længere | | `spuNameFleet` | Giver alle traktorer og lastbiler uden for flows butiksnavnet (*Fendt 942 1*, *Fendt 942 2* …) og omdøber deres egen parkering til `P <navn>`, hvis ingen anden deler den. Overskriver egne navne på de køretøjer. Gem spillet bagefter. Pas på: en eksisterende `Connect <gammelt navn>`-markør passer ikke længere |
| `spuConnect` | Kobler det nærmeste redskab (inden for ca. 3 m) på den maskine, du sidder i | | `spuConnect` | Kobler det nærmeste redskab (inden for ca. 3 m) på den maskine, du sidder i |
| `spuDetach` | Kobler redskaberne bag den maskine, du sidder i, af | | `spuDetach` | Kobler redskaberne bag den maskine, du sidder i, af |
| `spuSellBlock [del af navn]` | Uden navn: viser alle salgssteder, og hvilke der er slået fra. Med navn: slår de salgssteder fra/til, hvis navn indeholder teksten (fx `spuSellBlock market price`). Et salgssted, der er slået fra, bruges hverken til udkørsel eller i priserne. Gemmes pr. savegame. Til salgssteder, hvor aflæsningen går i stå |
| `spuTrip A > B > C` | Kører en tur over flere markører: Connect-markører kobler på, Afkobl eller redskabets egen parkering kobler af, og ved andre markører holdes der 10 sekunder. Mest til test | | `spuTrip A > B > C` | Kører en tur over flere markører: Connect-markører kobler på, Afkobl eller redskabets egen parkering kobler af, og ved andre markører holdes der 10 sekunder. Mest til test |
--- ---
@@ -1139,7 +1317,7 @@ Moden testes løbende, og fejlrapporter er meget velkomne.
| **Connect-markør** | Markøren `Connect <parkeringsnavn>`, hvor en traktor kobler et redskab på | | **Connect-markør** | Markøren `Connect <parkeringsnavn>`, hvor en traktor kobler et redskab på |
| **Afkobl-markør** | Markør, hvor redskaber bag traktoren kobles af | | **Afkobl-markør** | Markør, hvor redskaber bag traktoren kobles af |
| **Solo-traktor** | Traktor uden vogn eller redskab bagpå | | **Solo-traktor** | Traktor uden vogn eller redskab bagpå |
| **Fast sæt** | Redskab uden Connect-markør. Det bliver siddende og udlånes aldrig | | **Fast sæt** | Redskab uden Connect-markør. Det bliver siddende og udlånes aldrig. Et flows vogne udlånes heller aldrig |
| **Maskinpulje** | Frakoblede redskaber/vogne og ledige traktorer, som moden parrer efter behov | | **Maskinpulje** | Frakoblede redskaber/vogne og ledige traktorer, som moden parrer efter behov |
| **Markflåde** | De maskiner, markarbejdet må bruge | | **Markflåde** | De maskiner, markarbejdet må bruge |
| **UAL** | Universal Autoload, mod der læsser paller automatisk | | **UAL** | Universal Autoload, mod der læsser paller automatisk |
@@ -1148,3 +1326,7 @@ Moden testes løbende, og fejlrapporter er meget velkomne.
| **Reserve** | Det, gårdens egne stalde og fabrikker bruger det næste døgn | | **Reserve** | Det, gårdens egne stalde og fabrikker bruger det næste døgn |
| **Tid til tom** | Hvor længe en fabriks lager af en vare holder ved det nuværende forbrug | | **Tid til tom** | Hvor længe en fabriks lager af en vare holder ved det nuværende forbrug |
| **Marksilo** | En silo ved en mark, der bruges som mellemlager | | **Marksilo** | En silo ved en mark, der bruges som mellemlager |
| **Kæde** | Den samlede vej fra en produktion tilbage gennem fabrikker og stalde til afgrøderne på markerne |
| **Rod-produktion** | Den produktion, du selv slår til, og som kæden holder kørende |
| **Slukket men nødvendig** | En slukket produktion, hvis vare et flow, en stald eller en kæde bruger — moden advarer én gang og viser en rød linje i Kræver handling |
| **Fri til kæden** | Markering på en mark, der lader moden skifte afgrøde automatisk |
+197 -6
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — user manual # AD Smart Pickup — user manual
Version 1.60.11 · Farming Simulator 25 · add-on for AutoDrive Version 1.61.0 · Farming Simulator 25 · add-on for AutoDrive
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
@@ -93,8 +93,10 @@ empty*: the good that runs out first is fetched first.
**Reserve.** What the farm's own barns and factories will use in the next day. The reserve is never sold. **Reserve.** What the farm's own barns and factories will use in the next day. The reserve is never sold.
**Set.** A tractor with an implement or trailer attached, or a self-propelled machine. A **fixed set** is **Set.** A tractor with an implement or trailer attached, or a self-propelled machine. An implement or
an implement that has no Connect marker. It stays attached and is never lent out. trailer is **bound** and never lent out when it belongs to a flow (the flow's tractors and trailers, also
while the flow is stopped), or when it has no Connect marker (a **fixed set**, e.g. a vineyard tractor with
its vineyard implement).
**Machine pool.** Detached implements and trailers with a parking spot and a Connect marker, plus free **Machine pool.** Detached implements and trailers with a parking spot and a Connect marker, plus free
tractors without an implement. The mod pairs them up when needed. tractors without an implement. The mod pairs them up when needed.
@@ -129,6 +131,12 @@ It is the **folder's** name that counts. A marker called *F91 Wait* inside a fie
point. Wait markers are never used as a source or target. A Wait point counts as occupied when it is point. Wait markers are never used as a source or target. A Wait point counts as occupied when it is
reserved, or when another vehicle stands within 6 m of it. reserved, or when another vehicle stands within 6 m of it.
> **Important: parking and fetching only work by reversing.** The tractor has to **reverse** the implement
> into its parking spot and **reverse** up to the drawbar when it fetches it. So build the AutoDrive route
> to both `P <name>` and `Connect P <name>` as a **reverse route** (AutoDrive's reverse connection) for the
> last stretch, so the tractor ends with its hitch facing the implement's drawbar. If the tractor drives in
> forwards, its hitch points away from the implement, and it can neither drop it properly nor hitch it.
### Parking: "P <name>" ### Parking: "P <name>"
The mod uses AutoDrive's own **park destination** on each vehicle and implement (set in AutoDrive). The mod uses AutoDrive's own **park destination** on each vehicle and implement (set in AutoDrive).
@@ -149,7 +157,8 @@ A **Connect marker** is where the tractor stops to hitch an implement or trailer
- If you drive a tractor with AutoDrive's "Drive to" to a Connect marker yourself, the mod hitches too. - If you drive a tractor with AutoDrive's "Drive to" to a Connect marker yourself, the mod hitches too.
- If you rename a parking spot, rename the Connect marker as well. - If you rename a parking spot, rename the Connect marker as well.
An implement or trailer **without** a Connect marker is a fixed set and is never lent out. An implement or trailer **without** a Connect marker is a fixed set and is never lent out. The same goes
for a flow's trailers, even when they have a Connect marker (see [Flow trailers](#flow-trailers)).
### Detaching: "Afkobl" ### Detaching: "Afkobl"
@@ -333,6 +342,29 @@ Set **Yes** for the buildings the flow should use. Buildings without a marker ar
on **Show all buildings**. The list is sorted by name, so *Silo 9* comes before *Silo 10*. A building you on **Show all buildings**. The list is sorted by name, so *Silo 9* comes before *Silo 10*. A building you
have sold appears as *Removed building (no longer exists) — set NO*. have sold appears as *Removed building (no longer exists) — set NO*.
### Status window
Press **numpad \*** (the multiply key on the numeric keypad) to toggle a small status window in the top right corner of the screen, just below
the game's info box. The key works both on foot and in a vehicle and can be changed under *Settings →
Controls* (*AD Smart Pickup: status window*). The window refreshes every second and only shows sections
that have something in them:
- **Flows** — one line per tractor in a running flow: *tractor · action · fill %* (e.g. *Milk Collect 1 ·
fetching Milk at Cow Barn · 45 %*). Green = running, yellow = waiting (Wait, trailer), red = the flow has
a warning, grey = stopped.
- **Fields** — one line per field with field work in progress: *F92 Harvesting · Beet harvester · 45 %*
(colours as in the Fields tab).
- **Chains** — one line when chains have a missing link: *Chains: 3 missing links*.
- **Warnings** — factories the farm depends on that are switched off (*Switched off but needed: Fermenting
silo 3*, see [Warning when a needed factory is switched off](#warning-when-a-needed-factory-is-switched-off)),
vehicles that have stood still for more than 2 minutes (without Smart Pickup holding them at Wait/outbound;
sold vehicles are not shown) and pending chain suggestions (*Chain: 2 suggestions*).
At most 18 lines; the rest is summed up as *+k more*. The window is hidden while a menu or dialog is open,
when the HUD is hidden and while you sleep. Whether it is on is saved per user in
`Documents/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (not in the savegame);
it is off by default.
--- ---
## 5. Supply: keep barns and factories filled ## 5. Supply: keep barns and factories filled
@@ -603,7 +635,8 @@ and while the flow is running.
The machine pool is used by field work and field silos when no ready-made set is free. The machine pool is used by field work and field silos when no ready-made set is free.
- **Pool implement/trailer:** detached, owned by the farm, with an AutoDrive parking spot and a Connect - **Pool implement/trailer:** detached, owned by the farm, with an AutoDrive parking spot and a Connect
marker for that parking spot. Implements without a Connect marker are fixed sets and are never lent. marker for that parking spot, and not bound to a flow. Implements without a Connect marker are fixed sets
and are never lent.
- **Pool tractor:** an ordinary tractor with nothing attached at the rear, with its own AutoDrive parking - **Pool tractor:** an ordinary tractor with nothing attached at the rear, with its own AutoDrive parking
spot, on AutoDrive's road network and not driven by you. Field implements are only hitched behind spot, on AutoDrive's road network and not driven by you. Field implements are only hitched behind
tractors. Unloading trailers can also be pulled by other vehicles. tractors. Unloading trailers can also be pulled by other vehicles.
@@ -618,6 +651,157 @@ The field fleet view in the **Fields** tab shows pool tractors and pool implemen
--- ---
### Production chains
> **Important:** Every production you switch on in a factory counts as something you want produced. The mod
> then tries to supply it: fields marked free switch to the crops that are short, other fields get
> suggestions, intermediate productions are switched on, and flows are created with tractors from the
> machine pool. So **only** switch on productions you actually want running. To stop a chain, switch the
> production off or press **[Pause]** in the Chains tab.
#### What is a chain?
A **chain** keeps a production running all the way back to the field. You switch on a product in a
factory. The mod then works backwards: what the production needs, which of your own factories and barns
make it, and which crops are needed. The factory you switched on is the **root production**.
- The mod looks 12 months ahead. It acts only when a crop is short by more than 10 % and more than 0.5 ha.
- Fields you marked **Free for chains** change crop by themselves (at most 2 changes a minute).
- Other fields get a **suggestion** that you approve or reject.
- A crop you chose on a field is never changed automatically.
- The chain does nothing during the first 2 minutes after loading. If the mod cannot read all data,
nothing is switched off or stopped.
- Fields are changed and suggested only once the field scan has seen all your fields. With many fields this
can take a few minutes; meanwhile the tab says *Fields still being scanned* at the bottom.
- The mod uses your own running productions as a source first. It switches on a new one only if they are
not enough.
#### The Chains tab
The **Chains** tab shows one tree per root production: the root at the top, and below it each link with
the columns **Goods**, **Source**, **l/mo**, **Status** and **Stock**. With no chains it says *No chains:
enable a product in a factory*.
- The root's status is *covered*, *short*, *waiting* or *paused*. The number of suggestions follows the name.
- A coloured dot shows the status: **green** running, **yellow** waiting, **red** error, **grey** stopped.
The status text gets the same colour.
- Rows fold out and in. Fold out to see the links, sources and fields.
- Warnings in the rows: *bottleneck: -X/mo*, *must buy: X/mo*, *loop*, *short X ha*, *stock < 3 mo* and
*no field*.
- Field goods that are not a crop's main product — for example hay, dried alfalfa and clover, chaff, whole-crop
and straw — have the source *from fields*. You harvest or mow them yourself, so the chain makes no crop plan
and no warning for them; the stock is shown in the **Stock** column.
- **[Pause]** stops the chain's new actions for that root production. What it uses is not switched off.
Other chains carry on. **[Resume]** continues it.
- At the bottom: *Automatic switches since load*.
#### Missing links (red messages)
If a link in a chain is missing, the mod explains why and what you can do. The text appears in three places:
- **In the Chains tab**: fold the chain out; the reason is shown in red below the red goods row.
- **In Needs attention**: one combined red line for all chains, for example *Chains: 3 missing links*. It is
listed below the storage's red lines (running out / almost full). Clicking opens the Chains tab with the
chains folded out.
- **As an on-screen message**: the first time a new link goes missing, with the chain's name in front (for example *Bread: …*). The same message only returns after the
problem has gone away and come back. At most 3 messages come at a time; the rest are combined into one message:
*+5 more missing links — see the Chains tab*. No messages during the first 2 minutes after loading or while
fields are being scanned.
The messages:
- *None of your factories makes Flour — build one or buy it*: no factory, barn or field supplies the goods.
- *Barns make too little Milk — buy the rest or more animals*: the barn makes the goods, but too little.
- *Mill can only make 800 of 1.0k/mo*: the factory cannot keep up (bottleneck).
- *Mill (Flour) was turned off by you — turn it on or pause the chain*: you switched the production off yourself.
- *Loop around Flour — the chain cannot be calculated*: the goods are needed to make themselves.
#### Warning when a needed factory is switched off
If you switch off a production the farm depends on, the mod warns you — also when no chain is running. A
production is needed when one of its goods is used by the farm:
- a running supply flow has the goods chosen as goods,
- a running flow in auto goes to a target that takes the goods, and the farm consumes the goods,
- a barn is eating the goods right now, or
- an active chain has the goods as input (if only chains use the goods, it is reported as a missing link
instead, see above).
For auto flows and barns the farm's consumption of the goods must be known; otherwise the goods do not count as needed.
Example: you switch off the fermenting silos, but the flows *Cow Food*, *Goat Food* and *Biogas Silage* fetch
silage. You then get the message *Warning: you switched off Fermenting silo 3 (Silage) — Cow Food, Goat Food
+1 uses it. Stock lasts about 12 days.* (days from the farm's stock and consumption; left out when unknown).
The message comes once per switch-off, at most a minute later.
As long as the production is off and still needed, **Needs attention** shows a red line: *Switched off but
needed: Fermenting silo 3* (at most one line — the most urgent; if more are off, the number follows: *Switched off
but needed: Fermenting silo 3 +2*; also in the status window under Warnings). Clicking opens the storage
tab **Factories**.
No warning when:
- another recipe in the same factory makes the goods and is on (you chose which recipe runs — for example one
silage recipe in a fermenting silo),
- another running production makes the goods with enough capacity,
- the farm has stock for more than 60 days,
- the chain switched the production off itself, or
- the production was already off when the game was loaded (then only the Needs attention line is shown).
Productions that no flow, barn or chain uses (for example a bakery without a flow) never give a warning.
#### Mark a field free for chains
Open the field in the field guide and switch on **Free for chains (mod may change the crop)**. Only such a
field changes crop automatically. The tab lists them under **Fields free for the chain**.
Permanent crops (vine, olive, poplar) and grass fields are never changed, even when marked.
#### Suggestions: Approve and Reject
If the chain lacks area on a field that is not free, the chain tree shows a suggestion: **Suggest
[Approve]** and **[Reject]**. At most 3 suggestions are shown per crop. **Needs attention** shows one
combined entry, for example *Chain: 3 suggestions*; clicking it opens the Chains tab.
- **[Approve]** changes the field's crop.
- **[Reject]** removes the suggestion. It returns if the crop's shortage changes by more than 25 %.
- An outdated suggestion says *The suggestion no longer applies*.
- Fields the mod changed on its own are marked *switched (free field)*.
#### Intermediate goods
If a chain needs an intermediate good (for example flour for bread), the mod switches on the production
that makes it.
- The mod switches a production off again only if the chain switched it on.
- If you switch off a production a chain uses (even one you switched on yourself), the chain leaves it off
and does not count it as a source. Only when you switch on a new production in a factory may the chain
use it again.
- If you switch off the root production, the chain's flows are stopped (see below), unless another chain
still uses them.
#### Chain flows
For each link the mod creates a flow named **Chain: <goods> → <building>** (at most 2 new per minute).
- The flow gets a free pool tractor and a tipping trailer. The mod never takes a tractor that belongs to
another flow.
- If none is available, the flow is created stopped and the chain shows *no tractor/trailer*.
- Flows with pallets are always created stopped and the chain shows *pallet: UAL trailer*: choose a UAL
trailer yourself.
- When the chain is switched off, its flows are **stopped** and released by the chain. They are not
deleted. If you start one again, the chain lets it run.
- Status in the chain: *flow running*, *flow stopped*, *no tractor/trailer*, *pallet: UAL trailer* or
*no flow*.
#### Limitations
- The mod tries to supply every production you switch on in a factory. Only switch on what you want produced.
- The mod does **not buy** goods and does **not buy animals**. What must be bought shows as *must buy*.
- Permanent crops (vine, olive, poplar) and grass fields are never changed.
- The plan uses the base yield without fertiliser bonus, so it errs on too many hectares rather than too few.
- The chain uses only your own factories and barns.
## 8. Start, stop and saving ## 8. Start, stop and saving
**Start flow** does the following for each approved tractor: **Start flow** does the following for each approved tractor:
@@ -677,6 +861,8 @@ every second.
| *Wheat runs out in … — farm has …, uses …/day* | The farm uses more than it produces, and the good lasts less than 3 days (red below 1 day) | | *Wheat runs out in … — farm has …, uses …/day* | The farm uses more than it produces, and the good lasts less than 3 days (red below 1 day) |
| *Milk is 92 % full — full in …* | A produced good is at least 85 % full (red from 95 %) | | *Milk is 92 % full — full in …* | A produced good is at least 85 % full (red from 95 %) |
| *Sell Canola now: price is 94 % of the yearly top — …* | The price is at least 90 % of the yearly top, the stock is worth at least 10,000, and the farm does not use it up | | *Sell Canola now: price is 94 % of the yearly top — …* | The price is at least 90 % of the yearly top, the stock is worth at least 10,000, and the farm does not use it up |
| *Chains: 3 missing links* | One or more chains are missing a link (see [Missing links](#missing-links-red-messages)); listed below the storage's red lines; clicking opens the Chains tab |
| *Switched off but needed: Fermenting silo 3 +2* | You switched off a production that a flow, a barn or a chain uses (see [Warning when a needed factory is switched off](#warning-when-a-needed-factory-is-switched-off)); clicking opens the Factories tab |
**Click a line** to act on it: **Click a line** to act on it:
@@ -1058,6 +1244,7 @@ Open the game's console to use them:
| `spuNameFleet` | Gives all tractors and trucks outside flows their shop name (*Fendt 942 1*, *Fendt 942 2* …) and renames their own parking spot to `P <name>` if nobody else shares it. Overwrites custom names on those vehicles. Save the game afterwards. Note: an existing `Connect <old name>` marker no longer matches | | `spuNameFleet` | Gives all tractors and trucks outside flows their shop name (*Fendt 942 1*, *Fendt 942 2* …) and renames their own parking spot to `P <name>` if nobody else shares it. Overwrites custom names on those vehicles. Save the game afterwards. Note: an existing `Connect <old name>` marker no longer matches |
| `spuConnect` | Hitches the nearest implement (within about 3 m) to the machine you are sitting in | | `spuConnect` | Hitches the nearest implement (within about 3 m) to the machine you are sitting in |
| `spuDetach` | Detaches the implements behind the machine you are sitting in | | `spuDetach` | Detaches the implements behind the machine you are sitting in |
| `spuSellBlock [part of name]` | Without a name: lists all selling points and which ones are switched off. With a name: switches off/on the selling points whose name contains the text (e.g. `spuSellBlock market price`). A switched-off selling point is used neither for outbound trips nor in prices. Saved per savegame. For selling points where unloading keeps stalling |
| `spuTrip A > B > C` | Drives a trip over several markers: Connect markers hitch, Afkobl or the implement's own parking detach, and at other markers it stops for 10 seconds. Mostly for testing | | `spuTrip A > B > C` | Drives a trip over several markers: Connect markers hitch, Afkobl or the implement's own parking detach, and at other markers it stops for 10 seconds. Mostly for testing |
--- ---
@@ -1169,7 +1356,7 @@ The mod is being tested continuously, and bug reports are very welcome.
| **Connect marker** | The marker `Connect <parking name>`, where a tractor hitches an implement | | **Connect marker** | The marker `Connect <parking name>`, where a tractor hitches an implement |
| **Afkobl marker** | Marker where implements behind the tractor are detached | | **Afkobl marker** | Marker where implements behind the tractor are detached |
| **Solo tractor** | Tractor with no trailer or implement attached at the rear | | **Solo tractor** | Tractor with no trailer or implement attached at the rear |
| **Fixed set** | Implement without a Connect marker. It stays attached and is never lent out | | **Fixed set** | Implement without a Connect marker. It stays attached and is never lent out. A flow's trailers are never lent out either |
| **Machine pool** | Detached implements/trailers and free tractors that the mod pairs up as needed | | **Machine pool** | Detached implements/trailers and free tractors that the mod pairs up as needed |
| **Field fleet** | The machines field work is allowed to use | | **Field fleet** | The machines field work is allowed to use |
| **UAL** | Universal Autoload, a mod that loads pallets automatically | | **UAL** | Universal Autoload, a mod that loads pallets automatically |
@@ -1178,3 +1365,7 @@ The mod is being tested continuously, and bug reports are very welcome.
| **Reserve** | What the farm's own barns and factories use in the next day | | **Reserve** | What the farm's own barns and factories use in the next day |
| **Time to empty** | How long a factory's stock of a good lasts at the current consumption | | **Time to empty** | How long a factory's stock of a good lasts at the current consumption |
| **Field silo** | A silo near a field used as intermediate storage | | **Field silo** | A silo near a field used as intermediate storage |
| **Chain** | The whole path from a production back through factories and barns to the crops in the fields |
| **Root production** | The production you switch on yourself, which the chain keeps running |
| **Switched off but needed** | A switched-off production whose goods a flow, a barn or a chain uses — the mod warns once and shows a red line in Needs attention |
| **Free for chains** | A field marking that lets the mod change the crop automatically |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+222
View File
@@ -0,0 +1,222 @@
"""Fjern sving > MAX_TURN på AD-ruter mellem markører ved at tilføje kurve-omveje.
Kun nye waypoints tilføjes; eksisterende id'er, koordinater og forbindelser bevares
(kanter der deles får et mellempunkt på samme linje). AD's pathfinder vælger selv
kurven, fordi den er kortere og har mindre vinkel-straf.
Brug: adfix.py <ind.xml> <ud.xml> <log.json>
"""
import json, math, re, sys, collections
sys.path.insert(0, __file__.rsplit("/", 1)[0])
from adsmooth import read, write, dist, new_node, link, apply_splits
from adroutes import transit, dijkstra, angle, is_reverse
MAX_TURN = 45.5 # grænse (45° præcis er tilladt)
CURVE_STEP = 7.9 # max knæk i kurven (<8° => AD regner 27 km/h, så kurven vinder)
R_TRY = (8.0, 6.0, 4.5, 3.0, 2.0, 1.5, 1.0)
TARGET_EDGE = 7.9 # max knæk overalt i kurven inkl. ind-/udgang
def markers(doc):
return [int(float(i)) for i in re.findall(r"<mm\d+>\s*<id>([\d.]+)</id>", doc["s"])]
def node_path(best, pre, target):
s = best.get(target); seq = []
while s:
seq.append(s); s = pre[s]
seq.reverse()
return [seq[0][0]] + [b for _, b in seq] if seq else []
def collect(N, mks):
"""Returnér {(p,b,c): (antal, path, index_of_b)} for sving > MAX_TURN på ruter."""
T = transit(N); found = {}
cnt = collections.Counter()
for st in mks:
best, pre = dijkstra(N, T, st)
for tg in mks:
if tg == st or tg not in best:
continue
path = node_path(best, pre, tg)
backing = False
for i in range(1, len(path) - 1):
p, b, c = path[i - 1], path[i], path[i + 1]
a = angle(N, p, b, c)
# bakning: starter på en bakforbindelse (p ikke i b.incoming), slutter når retningen vender (>90°)
if p not in N[b]["inc"]:
backing = True
if backing:
if a > 90:
backing = False
continue
# vendepunktet lige før baklængs-kørsel (bakforbindelse c -> næste) er bevidst skarpt
if i + 2 < len(path) and c not in N[path[i + 2]]["inc"]:
continue
if a > MAX_TURN and not is_reverse(N, p, b, c):
key = (p, b, c) + window(N, path, i)
cnt[key] += 1
back = sum(dist(N[path[j]], N[path[j + 1]]) for j in range(i))
fwd = sum(dist(N[path[j]], N[path[j + 1]]) for j in range(i, len(path) - 1))
cur = found.get(key)
if cur is None:
found[key] = [(path, i, back), (path, i, fwd)]
else:
if back < cur[0][2]: cur[0] = (path, i, back)
if fwd < cur[1][2]: cur[1] = (path, i, fwd)
return {k: (cnt[k], v[0], v[1]) for k, v in found.items()}
def window(N, path, i, reach=8.5):
"""Rutens noder inden for `reach` meter før og efter path[i] (skelner grene)."""
lo = i; acc = 0.0
while lo > 0 and acc < reach:
acc += dist(N[path[lo - 1]], N[path[lo]]); lo -= 1
hi = i; acc = 0.0
while hi < len(path) - 1 and acc < reach:
acc += dist(N[path[hi]], N[path[hi + 1]]); hi += 1
return (tuple(path[lo:i]), tuple(path[i + 1:hi + 1]))
def point_along(N, path, i, R, direction):
"""Gå R meter fra path[i] bagud (-1) eller fremad (+1). Returnér
(u, v, d_from_u) for punktet på kanten u->v i kørselsretning, eller None."""
acc = 0.0; j = i
while True:
k = j + direction
if k < 0 or k >= len(path):
return None
a, b = (path[k], path[j]) if direction < 0 else (path[j], path[k])
L = dist(N[a], N[b])
if acc + L >= R:
rest = R - acc # meter fra path[j] mod path[k]
return (a, b, L - rest) if direction < 0 else (a, b, rest)
acc += L; j = k
def tangent(N, u, v):
L = dist(N[u], N[v])
return ((N[v]["x"] - N[u]["x"]) / L, (N[v]["z"] - N[u]["z"]) / L)
def pos_on(N, u, v, d):
t = d / dist(N[u], N[v])
return {a: N[u][a] + (N[v][a] - N[u][a]) * t for a in "xyz"}
def ang2(v1, v2):
l1 = math.hypot(*v1); l2 = math.hypot(*v2)
if l1 < 1e-6 or l2 < 1e-6:
return 0.0
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2)))))
def design(N, back, fwd, R):
"""Kubisk Bézier fra punkt R før b til R efter b. back/fwd = (path, i, plads) for
de ruter der har mindst plads før hhv. efter b. Returnér plan eller None."""
a = point_along(N, back[0], back[1], R, -1); b = point_along(N, fwd[0], fwd[1], R, +1)
if not a or not b:
return None
(u1, v1, d1), (u2, v2, d2) = a, b
# ligger punktet (næsten) oven i en eksisterende node, bruges noden selv
snap = lambda u, v, d: ("node", u) if d < 0.3 else ("node", v) if dist(N[u], N[v]) - d < 0.3 else ("split", u, v, d)
e_in, e_out = snap(u1, v1, d1), snap(u2, v2, d2)
if e_in[0] == "node":
d1 = 0.0 if e_in[1] == u1 else dist(N[u1], N[v1])
if e_out[0] == "node":
d2 = 0.0 if e_out[1] == u2 else dist(N[u2], N[v2])
P0 = pos_on(N, u1, v1, d1); P3 = pos_on(N, u2, v2, d2)
t0 = tangent(N, u1, v1); t3 = tangent(N, u2, v2)
# snappet til en node: retningen skal matche rutens kant IND i (start) / UD af (slut) noden
bp, fp = back[0], fwd[0]
if e_in[0] == "node" and e_in[1] == u1:
k = bp.index(u1)
if k == 0:
return None
t0 = tangent(N, bp[k - 1], u1)
if e_out[0] == "node" and e_out[1] == v2:
k = fp.index(v2)
if k >= len(fp) - 1:
return None
t3 = tangent(N, v2, fp[k + 1])
chord = math.hypot(P3["x"] - P0["x"], P3["z"] - P0["z"])
k = chord * 0.4
P1 = {"x": P0["x"] + t0[0] * k, "z": P0["z"] + t0[1] * k, "y": P0["y"] + (P3["y"] - P0["y"]) / 3}
P2 = {"x": P3["x"] - t3[0] * k, "z": P3["z"] - t3[1] * k, "y": P0["y"] + 2 * (P3["y"] - P0["y"]) / 3}
total = ang2(t0, t3)
n = max(3, math.ceil(total / CURVE_STEP) + 2)
while True:
pts = []
for s in range(n + 1):
t = s / n
pts.append({ax: (1 - t) ** 3 * P0[ax] + 3 * (1 - t) ** 2 * t * P1[ax] + 3 * (1 - t) * t * t * P2[ax] + t ** 3 * P3[ax] for ax in "xyz"})
# tjek alle knæk inkl. ind- og udgang
dirs = [t0] + [(pts[s + 1]["x"] - pts[s]["x"], pts[s + 1]["z"] - pts[s]["z"]) for s in range(n)] + [t3]
worst = max(ang2(dirs[s], dirs[s + 1]) for s in range(len(dirs) - 1))
if worst <= TARGET_EDGE:
break
n += 1
if n > 80 or chord / n < 0.15:
return None
return dict(end_in=e_in, end_out=e_out, split_in=(u1, v1, d1), split_out=(u2, v2, d2), inner=pts[1:-1], R=R, total=round(total, 1), worst=round(worst, 1))
def run(src, dst, logp):
doc = read(src); N = doc["N"]; n0 = len(N)
mks = [m for m in markers(doc) if m in N]
turns = collect(N, mks)
order = sorted(turns.items(), key=lambda kv: -kv[1][0])
plans = []; covered = set(); skipped = []
for key, (cnt, back, fwd) in order:
p, b, c = key[:3]
if key in covered:
continue
path, i = back[0], back[1]
plan = None
for R in R_TRY:
if R > back[2] - 0.3 or R > fwd[2] - 0.3:
continue
plan = design(N, back, fwd, R)
if plan:
break
if not plan:
skipped.append([p, b, c, cnt, round(angle(N, p, b, c), 1)]); continue
# marker alle sving i vinduet som dækket (fx 63°+27° i samme hjørne)
lo = path.index(plan["split_in"][1]) if plan["split_in"][1] in path else i
hi = path.index(plan["split_out"][0]) if plan["split_out"][0] in path else i
for j in range(max(1, lo), min(len(path) - 1, hi + 1)):
covered.add((path[j - 1], path[j], path[j + 1]) + window(N, path, j))
plan.update(turn=[p, b, c], count=cnt); plans.append(plan)
# 1) del kanter (samlet pr. kant), 2) byg kurver
req = {}; keyof = []
for pl in plans:
ks = []
for e in (pl["end_in"], pl["end_out"]):
if e[0] == "node":
ks.append(("node", e[1])); continue
_, u, v, d = e
lo, hi = min(u, v), max(u, v)
dd = round(d if u == lo else dist(N[u], N[v]) - d, 3)
req.setdefault((lo, hi), set()).add(dd); ks.append((lo, hi, dd))
keyof.append(ks)
made = apply_splits(N, req)
for pl, ks in zip(plans, keyof):
res = lambda k: k[1] if k[0] == "node" else made[k]
a, b = res(ks[0]), res(ks[1])
# bivej (bit 1) kun hvis begge ender er bivej — ellers ganger AD kurven med 20
sub = N[a]["fl"] & N[b]["fl"] & 1
fl = (N[pl["turn"][1]]["fl"] & ~1) | sub
chain = [a] + [new_node(N, q["x"], q["y"], q["z"], fl) for q in pl["inner"]] + [b]
for x, y in zip(chain, chain[1:]):
link(N, x, y)
pl["nodes"] = chain
del pl["inner"]
write(doc, dst)
json.dump(dict(plans=plans, skipped=skipped), open(logp, "w"))
print(f"sving>{MAX_TURN}°: {len(turns)} kurver: {len(plans)} dækket af andre kurver: {len(turns)-len(plans)-len(skipped)}"
f" sprunget over: {len(skipped)} nye noder: {len(N)-n0}")
return len(turns)
if __name__ == "__main__":
run(*sys.argv[1:4])
+36
View File
@@ -0,0 +1,36 @@
"""Fjern kurver som ingen markør-rute bruger, og omnummerér nye noder fortløbende."""
import sys, json, re, collections
sys.path.insert(0, __file__.rsplit("/", 1)[0])
import adfix
from adsmooth import read, write
from adroutes import transit, dijkstra
src, dst, n0 = sys.argv[1], sys.argv[2], int(sys.argv[3])
logs = sys.argv[4:]
doc = read(src); N = doc["N"]
mks = [m for m in adfix.markers(doc) if m in N]
T = transit(N); used = collections.Counter()
for st in mks:
best, pre = dijkstra(N, T, st)
for tg in mks:
if tg != st and tg in best:
for x in adfix.node_path(best, pre, tg):
if x > n0:
used[x] += 1
drop = set()
for lp in logs:
for p in json.load(open(lp))["plans"]:
inner = p["nodes"][1:-1]
if inner and not any(used[x] for x in inner):
drop.update(inner)
for i in drop:
del N[i]
for n in N.values():
n["out"] = [o for o in n["out"] if o not in drop]
n["inc"] = [o for o in n["inc"] if o not in drop]
old_new = sorted(i for i in N if i > n0)
remap = {o: n0 + 1 + k for k, o in enumerate(old_new)}
f = lambda i: remap.get(i, i)
doc["N"] = {f(i): dict(n, out=[f(o) for o in n["out"]], inc=[f(o) for o in n["inc"]]) for i, n in N.items()}
write(doc, dst)
print(f"fjernet {len(drop)} ubrugte kurvenoder; noder nu {len(doc['N'])} (nye: {len(doc['N']) - n0})")
+68
View File
@@ -0,0 +1,68 @@
import re,math,heapq,collections,sys,json
sys.path.insert(0, __file__.rsplit("/", 1)[0])
from adturns import load
def angle(N,p,b,c):
v1=(N[b]["x"]-N[p]["x"],N[b]["z"]-N[p]["z"]); v2=(N[c]["x"]-N[b]["x"],N[c]["z"]-N[b]["z"])
l1=math.hypot(*v1); l2=math.hypot(*v2)
if l1<1e-6 or l2<1e-6: return 0.0
return math.degrees(math.acos(max(-1,min(1,(v1[0]*v2[0]+v1[1]*v2[1])/(l1*l2)))))
def speed(a):
for lim,sp in ((3,50),(5,38),(8,27),(12,20),(15,13),(20,10),(30,7)):
if a<lim: return sp
return 4
def is_reverse(N,p,b,c):
return (b not in N[c]["inc"]) or (b in N[c]["inc"] and p not in N[b]["inc"])
def transit(N):
T={}
for b,n in N.items():
for p in n["inc"]:
if p not in N: continue
T[(p,b)]=[c for c in n["out"] if c in N and (angle(N,p,b,c)<=80 or is_reverse(N,p,b,c))]
return T
def dijkstra(N,T,start):
# tilstand = (forrige, node); returnerer bedste forgænger-tilstand pr node
dist={};pre={};best={}
h=[]
for c in N[start]["out"]:
if c not in N: continue
d=math.hypot(N[c]["x"]-N[start]["x"],N[c]["z"]-N[start]["z"])/(50/3.6)*(20 if N[c]["fl"]&1 else 1)
s=(start,c); dist[s]=d; pre[s]=None; heapq.heappush(h,(d,s))
while h:
d,s=heapq.heappop(h)
if d>dist.get(s,1e18): continue
p,b=s
if b not in best or d<dist[best[b]]: best[b]=s
# AD: efter en bakforbindelse (p ikke i b.incoming) findes ingen transitMapping -> alle udgange er tilladt
for c in T.get(s,N[b]["out"]):
a=angle(N,p,b,c); l=math.hypot(N[c]["x"]-N[b]["x"],N[c]["z"]-N[b]["z"])
nd=d+l/(speed(a)/3.6)*(20 if N[c]["fl"]&1 else 1)
ns=(b,c)
if nd<dist.get(ns,1e18): dist[ns]=nd; pre[ns]=s; heapq.heappush(h,(nd,ns))
return best,pre
def path_turns(N,best,pre,target):
s=best.get(target); turns=[]
seq=[]
while s: seq.append(s); s=pre[s]
seq.reverse()
for i in range(1,len(seq)):
p,b=seq[i-1]; _,c=seq[i]
turns.append((p,b,c))
return turns
if __name__=="__main__":
s,N,_=load(sys.argv[1])
mm=[(int(float(i)),n,g) for i,n,g in re.findall(r"<mm\d+>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>\s*<group>([^<]*)</group>",s)]
T=transit(N)
ids=[m[0] for m in mm if m[0] in N]
sharp=collections.Counter(); rev=0
for k,st in enumerate(ids):
best,pre=dijkstra(N,T,st)
for tg in ids:
if tg==st or tg not in best: continue
for (p,b,c) in path_turns(N,best,pre,tg):
a=angle(N,p,b,c)
if a>45:
if is_reverse(N,p,b,c): rev+=1; continue
sharp[(p,b,c)]+=1
json.dump([[p,b,c,n,round(angle(N,p,b,c),1)] for (p,b,c),n in sharp.items()],open(sys.argv[2],"w"))
print("markører",len(ids),"forskellige skarpe sving på ruter:",len(sharp),"bakke-sving sprunget over:",rev)
print(collections.Counter(int(angle(N,*k)//5*5) for k in sharp))
+159
View File
@@ -0,0 +1,159 @@
"""Blødgør skarpe sving (>45°) i et AutoDrive-vejnet ved at tilføje kurve-omveje.
Eksisterende waypoints og id'er røres ikke (kun nye noder tilføjes), så markører og
mod-referencer holder. For hvert sving p->b->c:
- kanten p-b deles i Pin (r meter før b), kanten b-c deles i Pout (r meter efter b)
- Pin -> kvadratisk Bézier (kontrolpunkt b) -> Pout, med nok punkter til at hvert
knæk er <= MAX_STEP grader
- er det omvendte sving (c,b,p) også på listen, bliver kurven tovejs
"""
import json, math, re, sys
MAX_STEP = 20.0 # grader pr. knæk inde i kurven
R_CAP = 10.0 # max benlængde (m) -> max afvigelse fra hjørnet ~3,2 m
R_FRAC = 0.45 # max andel af en kant der bruges til benet
R_MIN = 1.5 # kortere ben end dette -> spring over
def read(path):
raw = open(path, "rb").read()
bom = raw.startswith(b"\xef\xbb\xbf")
s = raw.decode("utf-8-sig")
w0 = s.index("<waypoints>"); w1 = s.index("</waypoints>")
w = s[w0:w1]
g = lambda t: re.search(rf"<{t}>(.*?)</{t}>", w, re.S).group(1)
ids = [int(v) for v in g("id").split(",")]
assert ids == list(range(1, len(ids) + 1)), "id'er er ikke 1..N"
xs = [float(v) for v in g("x").split(",")]
ys = [float(v) for v in g("y").split(",")]
zs = [float(v) for v in g("z").split(",")]
lst = lambda t: [[int(v) for v in e.split(",") if v != "-1"] for e in g(t).split(";")]
out, inc = lst("out"), lst("incoming")
fl = [int(v) for v in g("flags").split(",")]
N = {i: dict(x=xs[k], y=ys[k], z=zs[k], out=out[k], inc=inc[k], fl=fl[k]) for k, i in enumerate(ids)}
return dict(s=s, bom=bom, w0=w0, w1=w1, w=w, N=N)
def write(doc, path):
N = doc["N"]; ids = sorted(N)
f3 = lambda v: "%.3f" % v
joinl = lambda L: ",".join(map(str, L)) if L else "-1"
parts = {
"id": ",".join(map(str, ids)),
"x": ",".join(f3(N[i]["x"]) for i in ids),
"y": ",".join(f3(N[i]["y"]) for i in ids),
"z": ",".join(f3(N[i]["z"]) for i in ids),
"out": ";".join(joinl(N[i]["out"]) for i in ids),
"incoming": ";".join(joinl(N[i]["inc"]) for i in ids),
"flags": ",".join(str(N[i]["fl"]) for i in ids),
}
w = doc["w"]
for t, v in parts.items():
w = re.sub(rf"<{t}>.*?</{t}>", lambda m, v=v, t=t: f"<{t}>{v}</{t}>", w, count=1, flags=re.S)
s = doc["s"][:doc["w0"]] + w + doc["s"][doc["w1"]:]
data = s.encode("utf-8")
open(path, "wb").write((b"\xef\xbb\xbf" if doc["bom"] else b"") + data)
def xz(n):
return (n["x"], n["z"])
def dist(a, b):
return math.hypot(a["x"] - b["x"], a["z"] - b["z"])
def new_node(N, x, y, z, fl):
i = max(N) + 1
N[i] = dict(x=x, y=y, z=z, out=[], inc=[], fl=fl)
return i
def link(N, a, b):
if b not in N[a]["out"]:
N[a]["out"].append(b)
if a not in N[b]["inc"]:
N[b]["inc"].append(a)
def apply_splits(N, requests):
"""requests: {(u,v) u<v: set(afstand fra u)} -> {(u,v,afstand): ny node}."""
made = {}
for (u, v), ds in requests.items():
L = dist(N[u], N[v])
pts = []
for d in sorted(ds):
if pts and d - pts[-1] < 0.5:
made[(u, v, d)] = made[(u, v, pts[-1])]; continue
t = d / L
m = new_node(N, *(N[u][a] + (N[v][a] - N[u][a]) * t for a in "xyz"),
N[v]["fl"] if t > 0.5 else N[u]["fl"])
made[(u, v, d)] = m; pts.append(d)
chain = [u] + [made[(u, v, d)] for d in pts] + [v]
for a, b in ((u, v), (v, u)):
if b in N[a]["out"]:
seq = chain if a == u else chain[::-1]
N[a]["out"] = [seq[1] if o == b else o for o in N[a]["out"]]
N[b]["inc"] = [seq[-2] if o == a else o for o in N[b]["inc"]]
for x, y in zip(seq[1:-1], seq[2:]):
link(N, x, y)
N[seq[1]]["inc"].append(a) if a not in N[seq[1]]["inc"] else None
return made
def smooth(doc, turns):
N = doc["N"]; want = set(turns); plan = []; req = {}; seen = set()
for p, b, c in turns:
if (p, b, c) in seen:
continue
dual = (c, b, p) in want
seen.add((p, b, c)); seen.add((c, b, p)) if dual else None
r = min(R_CAP, R_FRAC * dist(N[p], N[b]), R_FRAC * dist(N[b], N[c]))
if r < R_MIN:
continue
keys = []
for o in (p, c):
u, v = min(o, b), max(o, b)
d = r if u == b else dist(N[u], N[v]) - r
req.setdefault((u, v), set()).add(round(d, 3)); keys.append((u, v, round(d, 3)))
plan.append((p, b, c, dual, r, keys))
orig = {i: dict(N[i]) for i in N}
made_split = apply_splits(N, req)
made = []
for p, b, c, dual, r, keys in plan:
pin, pout = made_split[keys[0]], made_split[keys[1]]
P0, P1, P2 = N[pin], N[b], N[pout]
th = turn_angle(orig[p], orig[b], orig[c])
n = max(2, math.ceil(th / MAX_STEP) + 1)
chain = [pin]
for k in range(1, n):
t = k / n
q = lambda a: (1 - t) ** 2 * P0[a] + 2 * (1 - t) * t * P1[a] + t * t * P2[a]
chain.append(new_node(N, q("x"), q("y"), q("z"), N[b]["fl"]))
chain.append(pout)
for a, bb in zip(chain, chain[1:]):
link(N, a, bb)
if dual:
link(N, bb, a)
made.append(dict(turn=[p, b, c], dual=dual, angle=round(th, 1), r=round(r, 1), nodes=chain))
return made
def turn_angle(A, B, C):
v1 = (B["x"] - A["x"], B["z"] - A["z"]); v2 = (C["x"] - B["x"], C["z"] - B["z"])
l1 = math.hypot(*v1); l2 = math.hypot(*v2)
if l1 < 1e-6 or l2 < 1e-6:
return 0.0
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2)))))
if __name__ == "__main__":
src, sharp_json, dst, log = sys.argv[1:5]
doc = read(src)
n0 = len(doc["N"])
turns = [tuple(x[:3]) for x in json.load(open(sharp_json)) if x[4] > 45.5]
made = smooth(doc, turns)
write(doc, dst)
json.dump(made, open(log, "w"), indent=0)
print(f"sving: {len(turns)} kurver lavet: {len(made)} (tovejs {sum(m['dual'] for m in made)})"
f" nye noder: {len(doc['N']) - n0} sprunget over: {len(turns) - len(made) - sum(m['dual'] for m in made)}")
+36
View File
@@ -0,0 +1,36 @@
import re,math,collections,sys
def load(path):
s=open(path,encoding="utf-8-sig").read()
w=re.search(r"<waypoints>(.*?)</waypoints>",s,re.S).group(1)
g=lambda t:re.search(rf"<{t}>(.*?)</{t}>",w,re.S).group(1)
ids=[int(v) for v in g("id").split(",")]
X=[float(v) for v in g("x").split(",")]; Z=[float(v) for v in g("z").split(",")]
sp=lambda t:[[int(v) for v in e.split(",") if v not in("","-1")] for e in g(t).split(";")]
out=sp("out"); inc=sp("incoming"); fl=[int(v) for v in g("flags").split(",")]
N={i:dict(x=X[k],z=Z[k],out=out[k],inc=inc[k],fl=fl[k]) for k,i in enumerate(ids)}
mk=[(int(a),b) for a,b in re.findall(r'<mm\d+ id="(\d+)" name="([^"]*)"',s)]
return s,N,mk
def ang(N,a,b,c):
A,B,C=N[a],N[b],N[c]
v1=(B["x"]-A["x"],B["z"]-A["z"]); v2=(C["x"]-B["x"],C["z"]-B["z"])
l1=math.hypot(*v1); l2=math.hypot(*v2)
if l1<1e-3 or l2<1e-3: return 0,l1,l2
d=(v1[0]*v2[0]+v1[1]*v2[1])/(l1*l2)
return math.degrees(math.acos(max(-1,min(1,d)))),l1,l2
if __name__=="__main__":
s,N,mk=load(sys.argv[1])
print("noder",len(N),"markører",len(mk),"flags",collections.Counter(n["fl"] for n in N.values()))
sharp=[];rev=0
for b,n in N.items():
for a in n["inc"]:
for c in n["out"]:
if a==c or a not in N or c not in N: continue
# samme retning som forbindelse? tjek at a->b og b->c er reelle kanter
if b not in N[a]["out"]: continue
t,l1,l2=ang(N,a,b,c)
if t>135: rev+=1; continue # U-vending / bakke-par, ikke et sving
if t>45: sharp.append((round(t),a,b,c,len(n["inc"]),len(n["out"]),round(l1,1),round(l2,1)))
kinds=collections.Counter("kæde" if (x[4]==1 and x[5]==1) else "kryds" for x in sharp)
print("sving >45°:",len(sharp),kinds,"(>135° udeladt:",rev,")")
print("fordeling:",collections.Counter(min(x[0]//15*15,135) for x in sharp))
nodes={x[2] for x in sharp}; print("unikke knæk-noder",len(nodes))
+66
View File
@@ -0,0 +1,66 @@
"""Savegame2 30/9: reparationer af AD-vejnettet fundet ved gennemgangen.
1. Punkt 19564 (spor til 'P Forage Wagon') lå 5,6 km væk -> midt mellem naboerne.
2. Biogas Drop 1/2/4/5 var blindgyder (kun vej ind) -> sidste kant gøres tovejs som Drop 3.
3. Chicken Wait 5: 81° knæk ved 12808 (AD tillader 80°) -> privat spor 12808..12815 lagt om i en blød kurve.
4. 'Food And More' lå på en afbrudt stump oven på vejen -> markøren flyttes til vejpunkt 8416.
Brug: netfix.py <ind.xml> <ud.xml>
"""
import math, re, sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from adsmooth import read, write, link, dist
src, dst = sys.argv[1], sys.argv[2]
doc = read(src); N = doc["N"]
mk = {m.group(2): int(float(m.group(1))) for m in re.finditer(r"<mm\d+>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>", doc["s"])}
# 1
a, b = N[19563], N[19565]
assert dist(N[19564], a) > 1000, "19564 ser allerede rigtig ud"
for ax in "xyz":
N[19564][ax] = (a[ax] + b[ax]) / 2
print("19564 ->", round(N[19564]["x"], 2), round(N[19564]["z"], 2))
# 2
for name in ("Biogas Drop 1", "Biogas Drop 2", "Biogas Drop 4", "Biogas Drop 5"):
w = mk[name]; assert not N[w]["out"], name
for p in list(N[w]["inc"]):
link(N, w, p)
print(name, "tovejs mod", N[w]["out"])
# 3 kurve fra 12802 (retning fra 12801) til 12697 (retning mod 12698)
chain = [12808, 12809, 12810, 12811, 12812, 12813, 12814, 12815]
for i in chain:
assert len(N[i]["inc"]) == 1 and len(N[i]["out"]) == 1, i
P0, P3 = N[12802], N[12697]
def unit(u, v):
L = dist(u, v); return ((v["x"] - u["x"]) / L, (v["z"] - u["z"]) / L)
t0, t3 = unit(N[12801], P0), unit(P3, N[12698])
seq = [12801, 12802] + chain + [12697, 12698]
def ang(p, b, c):
v1 = (N[b]["x"] - N[p]["x"], N[b]["z"] - N[p]["z"]); v2 = (N[c]["x"] - N[b]["x"], N[c]["z"] - N[b]["z"])
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (math.hypot(*v1) * math.hypot(*v2))))))
def shape(k0, k3, g):
P1 = {"x": P0["x"] + t0[0] * k0, "z": P0["z"] + t0[1] * k0}
P2 = {"x": P3["x"] - t3[0] * k3, "z": P3["z"] - t3[1] * k3}
n = len(chain) + 1
for j, i in enumerate(chain, 1):
t = (j / n) ** g # g skubber punkterne mod den skarpe ende
for ax in "xz":
N[i][ax] = (1 - t) ** 3 * P0[ax] + 3 * (1 - t) ** 2 * t * P1[ax] + 3 * (1 - t) * t * t * P2[ax] + t ** 3 * P3[ax]
N[i]["y"] = P0["y"] + (P3["y"] - P0["y"]) * t
return max(ang(*seq[i:i + 3]) for i in range(len(seq) - 2))
D = dist(P0, P3)
best = min(((shape(a * D, b * D, g), a, b, g) for a in (0.2, 0.35, 0.5, 0.7, 0.9, 1.2) for b in (0.2, 0.35, 0.5, 0.7, 0.9, 1.2) for g in (0.7, 0.85, 1.0, 1.2)))
worst = shape(best[1] * D, best[2] * D, best[3])
print("Chicken Wait 5-kurve: største knæk %.1f°" % worst)
assert worst <= 45
write(doc, dst)
# 4 markør
s = open(dst, encoding="utf-8-sig").read()
s, cnt = re.subn(r"(<id>)8481\.000000(</id>\s*<name>Food And More</name>)", r"\g<1>8416.000000\g<2>", s)
assert cnt == 1
open(dst, "wb").write(b"\xef\xbb\xbf" + s.encode("utf-8"))
print("Food And More -> 8416")
+211
View File
@@ -0,0 +1,211 @@
"""Savegame2 30/9: parkering til alle køretøjer + Sell Slurry-vognfejlen.
- REX-vintraktorer: parkDestination = deres vinredskabs parkering (faste sæt).
- Motorkøretøjer uden plads (FH16 x3, Finsnitter): ledig bås omdøbes "P <navn>", parkDestination sættes.
Køretøjet flyttes ikke — AutoDrive bakker det ind, når det parkeres.
- Frakoblede vogne uden plads: ledig bås + "Connect P <navn>" (nyt punkt delt ind på bakkesporet ved
vognens trækøje) og vognen stilles ind i båsen, bagende 0,5 m fra P.
- Sell Slurry 2: ploven kobles af og stilles på "P Plov"; flowets vogn bliver gylletanken på
"P Sell Slurry vogn 2".
Skriver <fil>.new i savegame-mappen. Brug: parkall.py <savegame-mappe>
"""
import math, os, re, sys, zipfile
import xml.etree.ElementTree as ET
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from adsmooth import read, write, apply_splits, dist
SG = sys.argv[1].rstrip("/") + "/"
GAME = os.path.expanduser("~/.local/share/Steam/steamapps/common/Farming Simulator 25/")
MODS = os.path.expanduser("~/FS25-data/mods/")
REAR_GAP = 0.5 # vognens bagende så langt fra P (m)
KINGPIN_BACK = 1.2 # sættevogn: kongebolt bag forenden (ukalibreret skøn)
# motorkøretøjer: driverName -> kolonne; vogne: fil -> (navn, kolonne, sættevogn?)
MOTOR = ["FH16 780 1", "FH16 780 2", "Milk Sale", "Finsnitter"]
TRAILERS = {"dpw1800": ("DPW 1800", "syd", False), "bigBodyS750": ("Big Body 750", "syd", False),
"gtw330": ("Overlæssevogn GTW 330", "syd", False), "steelDropDeck": ("Steel Drop Deck", "vest", True)}
def xmltext(fn):
if fn.startswith("data/"):
return open(GAME + fn, encoding="utf-8", errors="ignore").read()
mod, rest = fn.replace("$moddir$", "").split("/", 1)
return zipfile.ZipFile(MODS + mod + ".zip").read(rest).decode("utf-8", "ignore")
def size_of(fn):
m = re.search(r"<size [^>]*>", xmltext(fn)).group(0)
L = float(re.search(r'length="([\d.]+)"', m).group(1))
lo = re.search(r'lengthOffset="(-?[\d.]+)"', m)
return L, float(lo.group(1)) if lo else 0.0
def heading(el):
r = [float(t) for t in el.find("component").get("rotation").split()]
return math.radians(r[1] + (180 if abs(r[0]) > 90 else 0))
def angn(a):
return (a + math.pi) % (2 * math.pi) - math.pi
doc = read(SG + "AutoDrive_config.xml"); N = doc["N"]
mk = {} # mm-index -> dict(wp, name, group)
for m in re.finditer(r"<mm(\d+)>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>\s*<group>([^<]*)</group>", doc["s"]):
mk[int(m.group(1))] = dict(wp=int(float(m.group(2))), name=m.group(3), group=m.group(4))
byname = {m["name"]: i for i, m in mk.items()}
VT = ET.parse(SG + "vehicles.xml"); VR = VT.getroot()
veh = {v.get("uniqueId"): v for v in VR.iter("vehicle")}
def adname(v):
ad = v.find("AutoDrive"); return ad.get("driverName") if ad is not None else None
def park_of(v):
ad = v.find("AutoDrive"); p = ad.get("parkDestination") if ad is not None else None
return int(p) if p and p.lstrip("-").isdigit() and int(p) > 0 else None
def set_park(v, idx):
ad = v.find("AutoDrive")
if ad is None:
last = list(v)[-1]; ad = ET.SubElement(v, "AutoDrive"); ad.tail = last.tail; last.tail = "\n "
ad.set("parkDestination", str(idx))
parent = {a.get("attachedVehicleUniqueId"): v.get("uniqueId") for v in VR.iter("vehicle") for a in v.iter("attachedImplement")}
def bay_axis(p_wp):
"""Retning fra P ud mod båsens munding, og sporets noder sorteret efter afstand fra P."""
P = N[p_wp]
# nabo-kæden fra P: gå til nabo længst væk, 6 skridt, og brug den retning
cur, prev = p_wp, None
for _ in range(6):
nb = [x for x in set(N[cur]["out"]) | set(N[cur]["inc"]) if x != prev]
if not nb: break
nxt = max(nb, key=lambda x: dist(N[x], P)); prev, cur = cur, nxt
e = math.atan2(N[cur]["x"] - P["x"], N[cur]["z"] - P["z"])
return e
def along_lat(p_wp, e, x, z):
dx, dz = x - N[p_wp]["x"], z - N[p_wp]["z"]
return dx * math.sin(e) + dz * math.cos(e), dx * math.cos(e) - dz * math.sin(e)
def split_point_on_track(p_wp, e, d):
"""(u, v, afstand fra min(u,v)) for punktet d m fra P på bakkesporet."""
track = []
for i, n in N.items():
a, l = along_lat(p_wp, e, n["x"], n["z"])
if abs(l) < 0.35 and -0.1 < a < 26:
track.append((a, i))
track.sort()
for (a1, i1), (a2, i2) in zip(track, track[1:]):
linked = i2 in N[i1]["out"] or i1 in N[i2]["out"]
if linked and a1 <= d <= a2:
u, v = min(i1, i2), max(i1, i2)
du = d - (a1 if u == i1 else a2)
return u, v, round(abs(du), 3)
raise SystemExit(f"fandt ikke sporet {d:.1f} m fra P (wp {p_wp})")
def ground_offset(v):
c = v.find("component"); x, y, z = [float(t) for t in c.get("position").split()]
near = min(N.values(), key=lambda n: (n["x"] - x) ** 2 + (n["z"] - z) ** 2)
return y - near["y"]
def place(v, p_wp, e, root_along):
"""Stil køretøjet med roden root_along m fra P langs e, front mod mundingen."""
tx = N[p_wp]["x"] + root_along * math.sin(e); tz = N[p_wp]["z"] + root_along * math.cos(e)
ty = N[p_wp]["y"] + ground_offset(v)
cs = v.findall("component"); p0 = [float(t) for t in cs[0].get("position").split()]
d = angn(e - heading(v))
for c in cs:
q = [float(t) for t in c.get("position").split()]; r = [float(t) for t in c.get("rotation").split()]
rx, rz = q[0] - p0[0], q[2] - p0[2]
nx = tx + rx * math.cos(d) + rz * math.sin(d); nz = tz - rx * math.sin(d) + rz * math.cos(d)
c.set("position", "%.3f %.3f %.3f" % (nx, q[1] - p0[1] + ty, nz))
r[1] = (r[1] + math.degrees(d) + 180) % 360 - 180
c.set("rotation", "%.2f %.2f %.2f" % tuple(r))
free = {"syd": [], "vest": []}
for i, m in mk.items():
if re.fullmatch(r"P Ledig \d+", m["name"]):
x, z = N[m["wp"]]["x"], N[m["wp"]]["z"]
if -2328 < z < -2320 and x > 1003: free["syd"].append(i)
elif 905 < x < 920: free["vest"].append(i)
free["syd"].sort(key=lambda i: N[mk[i]["wp"]]["x"])
free["vest"].sort(key=lambda i: N[mk[i]["wp"]]["z"])
renames, connects, log = {}, [], []
# 1) REX -> vinredskabets plads
for v in VR.iter("vehicle"):
if v.get("filename", "").endswith("seriesREX4.xml") and park_of(v) is None:
kids = [a.get("attachedVehicleUniqueId") for a in v.iter("attachedImplement")]
pk = [park_of(veh[k]) for k in kids if k in veh and park_of(veh[k])]
if pk:
set_park(v, pk[0]); log.append(f"{adname(v)} -> {mk[pk[0]]['name']} (fast sæt)")
# 2) motorkøretøjer
for v in VR.iter("vehicle"):
if adname(v) in MOTOR and park_of(v) is None:
slot = free["vest"].pop(0); renames[slot] = "P " + adname(v); set_park(v, slot)
log.append(f"{adname(v)} -> {mk[slot]['name']} => 'P {adname(v)}'")
# 3) frakoblede vogne
req, pending = {}, []
for v in VR.iter("vehicle"):
f = v.get("filename", "").split("/")[-1].replace(".xml", "")
if f in TRAILERS and park_of(v) is None and v.get("uniqueId") not in parent:
name, col, semi = TRAILERS[f]
slot = free[col].pop(0); pw = mk[slot]["wp"]; e = bay_axis(pw)
L, lo = size_of(v.get("filename"))
root = REAR_GAP - lo + L / 2
d = REAR_GAP + L - (KINGPIN_BACK if semi else 0.0)
u, w, du = split_point_on_track(pw, e, d)
req.setdefault((u, w), set()).add(du)
place(v, pw, e, root); set_park(v, slot)
renames[slot] = "P " + name
pending.append((slot, name, (u, w, du), d))
log.append(f"{name} ({f}) -> {mk[slot]['name']} => 'P {name}', Connect {d:.2f} m, rod {root:.2f} m")
# 4) Sell Slurry: plov af, gylletank som flowets vogn
plow_park = byname["P Plov"]; tank_park = byname["P Sell Slurry vogn 2"]
plow = next(v for v in VR.iter("vehicle") if park_of(v) == plow_park)
tank = next(v for v in VR.iter("vehicle") if park_of(v) == tank_park)
tractor = veh[parent[plow.get("uniqueId")]]
aj = tractor.find("attacherJoints")
for a in list(aj):
if a.get("attachedVehicleUniqueId") == plow.get("uniqueId"):
aj.remove(a)
pw = mk[plow_park]["wp"]; e = bay_axis(pw)
cw = mk[byname["Connect P Plov"]]["wp"]; dcon, _ = along_lat(pw, e, N[cw]["x"], N[cw]["z"])
L, lo = size_of(plow.get("filename"))
place(plow, pw, e, dcon - 0.1 - lo - L / 2) # liftmonteret: forkant + 0,1 m = Connect
log.append(f"plov koblet af '{adname(tractor)}' og stillet på P Plov (Connect {dcon:.2f} m)")
RP = SG + "ADSmartPickup_runs.xml"; runs = open(RP, encoding="utf-8-sig", newline="").read() # bevar CRLF
m = re.search(r'(<run [^>]*name="Sell Slurry"[^>]*wagons=")([^"]*)(")', runs)
pairs = dict(p.split("=") for p in m.group(2).split(";") if "=" in p)
pairs[tractor.get("uniqueId")] = tank.get("uniqueId")
runs = runs[:m.start(2)] + ";".join(f"{k}={pairs[k]}" for k in sorted(pairs)) + runs[m.end(2):]
log.append(f"Sell Slurry: '{adname(tractor)}' får gylletanken på P Sell Slurry vogn 2")
# skriv AD-config: del spor, så markørnavne + nye Connect-markører
made = apply_splits(N, req)
write(doc, SG + "AutoDrive_config.xml.new")
s = open(SG + "AutoDrive_config.xml.new", encoding="utf-8-sig").read()
bom = open(SG + "AutoDrive_config.xml", "rb").read(3) == b"\xef\xbb\xbf"
for slot, new in renames.items():
s, n = re.subn(r"(<mm%d>\s*<id>[\d.]+</id>\s*<name>)[^<]*(</name>\s*<group>)[^<]*(</group>)" % slot,
r"\g<1>%s\g<2>Parkering\g<3>" % new, s)
assert n == 1, slot
nxt = max(mk) + 1; add = ""
for slot, name, key, d in pending:
add += (" <mm%d>\n <id>%d.000000</id>\n <name>Connect P %s</name>\n"
" <group>Connect</group>\n </mm%d>\n") % (nxt, made[key], name, nxt)
nxt += 1
i = s.rfind(" </mapmarker>"); s = s[:i] + add + s[i:]
open(SG + "AutoDrive_config.xml.new", "wb").write((b"\xef\xbb\xbf" if bom else b"") + s.encode("utf-8"))
body = ET.tostring(VR, encoding="unicode")
open(SG + "vehicles.xml.new", "w", encoding="utf-8").write('<?xml version="1.0" encoding="utf-8" standalone="no"?>\n' + body + "\n")
open(SG + "ADSmartPickup_runs.xml.new", "w", encoding="utf-8", newline="").write("\ufeff" + runs)
print("\n".join(log)); print("skrevet .new")