Compare commits
61 Commits
v1.60.14.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb287a8154 | |||
| e229a27624 | |||
| ba98cf83d3 | |||
| a6662d6da1 | |||
| bf3b99aca4 | |||
| cbc9b2c8e3 | |||
| 1bda82a270 | |||
| 21b48d7664 | |||
| 88ddb3d6cd | |||
| 37cf84c101 | |||
| 991252848f | |||
| 6b533e62eb | |||
| c92c870297 | |||
| 18b6c82871 | |||
| 47afdebf64 | |||
| 2290bdde95 | |||
| 0c21ba49a0 | |||
| e1f7b19fef | |||
| 74537f7ec2 | |||
| d2970a1bda | |||
| bd106a2176 | |||
| acf94c8e88 | |||
| a3d234a3a2 | |||
| 68eb26d71d | |||
| 1c60529dfd | |||
| b33f2a3b01 | |||
| d2a4cf0d23 | |||
| 928299c8ee | |||
| e9f1251471 | |||
| 9fde03f950 | |||
| 979b7f72d5 | |||
| ba2a3e94f1 | |||
| 131b1ad10c | |||
| 19a10ab67c | |||
| 1e7b2ec699 | |||
| 817fc388a2 | |||
| 83394aabfd | |||
| 9e34a16dfa | |||
| d2453f78fb | |||
| 605c4e8d45 | |||
| 9c9c2badeb | |||
| e394a77833 | |||
| b77bda5334 | |||
| f0b8df8ae3 | |||
| b389fa3564 | |||
| 32f2886a55 | |||
| 2a7769d090 | |||
| 49eed51202 | |||
| 6dd1b5cac0 | |||
| 217498aefa | |||
| 2e7a7105c1 | |||
| 27017c0f62 | |||
| f813d4b3d8 | |||
| 4f676135ac | |||
| 2a36de0837 | |||
| c415bdc7a4 | |||
| a6506fea4e | |||
| 8abcbcdcb8 | |||
| 3c67591cb3 | |||
| 784144cd4e | |||
| 13e5dbf188 |
@@ -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.21** Flow-vogne: kun et redskab med lastrum (ikke plov, kultivator eller såmaskine) huskes som flowets vogn. Før kunne en plov, der hang bag en flow-traktor, blive gemt som gyllevogn, og gyllen blev aldrig kørt.
|
||||
- **1.60.20** Vinhøsterens bredde: også en lille bredde fra Courseplay (under 1,5 m) erstattes af vinrækkens 3 m.
|
||||
- **1.60.19** Vinhøster uden kendt arbejdsbredde får vinrækkens 3 m (før "bredde ukendt", og marken blev ikke startet).
|
||||
- **1.60.18** Vinhøsterens afgrøde læses fra `inputFruitTypeIndex` (feltnavnet i FS25).
|
||||
- **1.60.17** Vinhøstere (fx Grégoire GL86) genkendes som høstere af den afgrøde, de er konfigureret til
|
||||
(spec_vineCutter). Før fik vinmarker "ingen kan" (in-game: F22 hvide druer).
|
||||
- **1.60.16** Tipvogne skiftes under AD-aflæsning til en tip-side, AutoDrive må aflæsse fra (`allowAIDischarge`).
|
||||
Farmtech EDK 650 (Unrealistic) tillader kun bagudtip; stod den på venstre, højre eller kornluge, åbnede AD
|
||||
aflæsningen, men intet løb ud, og vognen holdt med fuldt læs. CP-genstartslisten logges kun, når den ændrer sig.
|
||||
- **1.60.15** Humle (HOPS, Dakota) behandles som vinafgrøde: aldrig slåafgrøde, og markmaskiner kører på rækkerne som ved druer.
|
||||
- **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står
|
||||
med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway".
|
||||
- **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets
|
||||
|
||||
@@ -251,12 +251,43 @@ local function markerCandidates(adEnv, nodes)
|
||||
return found
|
||||
end
|
||||
|
||||
-- Markørnavnets rolle: "unload" (unload/drop/aflæs/aflever/indkast/tip) eller "load" (pickup/hent/læs/load) —
|
||||
-- aflæsning tjekkes først ("unload" og "aflæs" indeholder "load"/"læs"). nil = neutralt navn.
|
||||
local UNLOAD_WORDS = {"unload", "drop", "aflæs", "aflev", "indkast", "tip"}
|
||||
local LOAD_WORDS = {"pickup", "pick up", "hent", "læs", "load"}
|
||||
function ADBuildings.nameRole(name)
|
||||
local lower = tostring(name or ""):lower()
|
||||
for _, word in ipairs(UNLOAD_WORDS) do if lower:find(word, 1, true) then return "unload" end end
|
||||
for _, word in ipairs(LOAD_WORDS) do if lower:find(word, 1, true) then return "load" end end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Når to markører står tæt (inden for NAME_WINDOW_M af den nærmeste), afgør navnet: den første med den ønskede rolle
|
||||
-- flyttes forrest. Ellers urørt (nærmeste først). (in-game 30/9: Sell Egg tog "Chicken Storage Unload" 17 m frem for
|
||||
-- "Egg Storage Pickup" 27 m ved Æglageret)
|
||||
ADBuildings.NAME_WINDOW_M = 20
|
||||
function ADBuildings.preferByName(candidates, wantRole)
|
||||
local list = candidates or {}
|
||||
if #list < 2 then return list end
|
||||
local limit = (list[1].distance or 0) + ADBuildings.NAME_WINDOW_M
|
||||
if ADBuildings.nameRole(list[1].name) == wantRole then return list end
|
||||
for index, candidate in ipairs(list) do
|
||||
if (candidate.distance or math.huge) > limit then break end
|
||||
if ADBuildings.nameRole(candidate.name) == wantRole then
|
||||
local reordered = {candidate}
|
||||
for other, item in ipairs(list) do if other ~= index then table.insert(reordered, item) end end
|
||||
return reordered
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
||||
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
||||
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
||||
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
||||
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
||||
for _, candidate in ipairs(candidates or {}) do
|
||||
for _, candidate in ipairs(ADBuildings.preferByName(candidates, "load")) do
|
||||
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
||||
if not isUnloads then
|
||||
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
||||
@@ -314,7 +345,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
local x, z = getNodePosition(node)
|
||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||
end
|
||||
local best, bestOther = nil, nil
|
||||
local best, bestOther, nearbyAll = nil, nil, {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
if not ADSupplyPlanner.isWaitMarker(marker) then
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||
@@ -323,6 +354,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z
|
||||
local distance = math.sqrt(dx * dx + dz * dz)
|
||||
if distance < ADBuildings.MAX_MARKER_DISTANCE then
|
||||
table.insert(nearbyAll, {marker = marker, distance = distance})
|
||||
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
|
||||
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
|
||||
bestOther = {marker = marker, distance = distance}
|
||||
@@ -338,6 +370,16 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
||||
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
|
||||
chosen = bestOther
|
||||
end
|
||||
-- navnet afgør mellem tætte markører: en "Unload/Drop"-markør frem for en "Pickup"-markør (in-game 30/9)
|
||||
table.sort(nearbyAll, function(a, b) return a.distance < b.distance end)
|
||||
if chosen ~= nil and ADBuildings.nameRole(chosen.marker.name) == "load" then
|
||||
for _, entry in ipairs(nearbyAll) do
|
||||
if entry.distance <= chosen.distance + ADBuildings.NAME_WINDOW_M and ADBuildings.nameRole(entry.marker.name) == "unload" then
|
||||
chosen = entry
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if chosen == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
-- AD Chain (kæde-kørsel)
|
||||
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
|
||||
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
|
||||
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
|
||||
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
|
||||
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
|
||||
-- Spiladgang kun inde i funktioner (filen dofile's i test).
|
||||
|
||||
ADChain = {}
|
||||
ADChain.FILE_NAME = "ADSmartPickup_chain.xml"
|
||||
ADChain.ROOT = "ADSmartPickupChain"
|
||||
ADChain.TICK_MS = 60000
|
||||
ADChain.STARTUP_GRACE_MS = 120000
|
||||
ADChain.MAX_FIELD_SWITCHES = 2
|
||||
ADChain.MAX_NEW_FLOWS = 2
|
||||
ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
|
||||
ADChain.DISMISS_CHANGE = 0.25
|
||||
|
||||
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
ADChain.last = nil
|
||||
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste tick — til at se spillerens slukninger
|
||||
ADChain.prevEnabled = nil
|
||||
-- true fra indlæsning til første tick: kædens produktioner der er slukket, glemmes (spillet blev ikke gemt)
|
||||
ADChain.afterLoad = false
|
||||
-- run-id -> "noVehicle"|"pallet" (true = "noVehicle"): flows kæden oprettede stoppet, fordi der ikke var
|
||||
-- traktor/vogn eller varen er på paller (kun i hukommelsen)
|
||||
ADChain.noVehicle = {}
|
||||
ADChain.switchCount = 0
|
||||
ADChain.now = 0
|
||||
ADChain.timer = 0
|
||||
ADChain.collectFailed = false
|
||||
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
|
||||
ADChain.problemKeys = {}
|
||||
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
|
||||
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
|
||||
ADChain.depPrevEnabled = nil
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
local function sortedKeys(map)
|
||||
local keys = {}
|
||||
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return keys
|
||||
end
|
||||
|
||||
-- ---- rene hjælpere ----
|
||||
|
||||
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
|
||||
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
|
||||
function ADChain.newState()
|
||||
-- finalKeep = {[rodnøgle] = true}: slutproduktet beholdes (standard: sælg straks); soldByChain = {["rod|vare"] = true}
|
||||
-- optIn = {[opskriftsnøgle] = true}: produktioner spilleren selv har slået til som kæde (tændte produktioner er
|
||||
-- ellers ikke kæder — FS25 tænder dem som standard)
|
||||
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
end
|
||||
|
||||
function ADChain.stateToRecords(state)
|
||||
local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {},
|
||||
roots = state.roots ~= nil and sortedKeys(state.roots) or nil,
|
||||
finalKeep = sortedKeys(state.finalKeep or {}), soldByChain = sortedKeys(state.soldByChain or {}), optIn = sortedKeys(state.optIn or {})}
|
||||
local ids = {}
|
||||
for id in pairs(state.dismissed or {}) do table.insert(ids, id) end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
local entry = state.dismissed[id]
|
||||
table.insert(records.dismissed, {id = id, fruit = entry.fruit, missingHa = entry.missingHa})
|
||||
end
|
||||
return records
|
||||
end
|
||||
|
||||
function ADChain.stateFromRecords(records)
|
||||
local state = ADChain.newState()
|
||||
if records == nil then return state end
|
||||
for _, key in ipairs(records.byChain or {}) do state.byChain[key] = true end
|
||||
for _, key in ipairs(records.paused or {}) do state.paused[key] = true end
|
||||
for _, key in ipairs(records.userOff or {}) do state.userOff[key] = true end
|
||||
for _, key in ipairs(records.finalKeep or {}) do state.finalKeep[key] = true end
|
||||
for _, key in ipairs(records.soldByChain or {}) do state.soldByChain[key] = true end
|
||||
for _, key in ipairs(records.optIn or {}) do state.optIn[key] = true end
|
||||
if records.roots ~= nil then
|
||||
state.roots = {}
|
||||
for _, key in ipairs(records.roots) do state.roots[key] = true end
|
||||
end
|
||||
for _, entry in ipairs(records.dismissed or {}) do
|
||||
local id = tonumber(entry.id)
|
||||
if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then
|
||||
state.dismissed[id] = {fruit = entry.fruit, missingHa = tonumber(entry.missingHa) or 0}
|
||||
end
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
-- Afviste forslag beholdes kun mens manglen for afgrøden findes og har ændret sig højst 25 %.
|
||||
function ADChain.pruneDismissed(dismissed, shortages)
|
||||
local byFruit = {}
|
||||
for _, shortage in ipairs(shortages or {}) do byFruit[shortage.fruit] = shortage.missingHa end
|
||||
local kept = {}
|
||||
for id, entry in pairs(dismissed or {}) do
|
||||
local now = byFruit[entry.fruit]
|
||||
local before = entry.missingHa or 0
|
||||
if now ~= nil and math.abs(now - before) <= math.abs(before) * ADChain.DISMISS_CHANGE then
|
||||
kept[id] = {fruit = entry.fruit, missingHa = before}
|
||||
end
|
||||
end
|
||||
return kept
|
||||
end
|
||||
|
||||
-- {[mark] = {fruit, missingHa}} -> {[mark] = afgrøde} (ADCropPlan.choose's dismissed)
|
||||
function ADChain.dismissedFruits(dismissed)
|
||||
local result = {}
|
||||
for id, entry in pairs(dismissed or {}) do result[id] = entry.fruit end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADChain.unionFruits(usedFruits, fieldDemand)
|
||||
local result = {}
|
||||
for fruit in pairs(usedFruits or {}) do result[fruit] = true end
|
||||
for fruit in pairs(fieldDemand or {}) do result[fruit] = true end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
|
||||
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
|
||||
-- optIn = {[nøgle] = true}: kun de produktioner er rødder (andre tændte markeres notChosen: kører for sig selv som
|
||||
-- kilde, men kæden skaber intet for dem). optIn = nil = alle tændte er rødder (som før).
|
||||
function ADChain.modelRecipes(recipes, paused, userOff, optIn)
|
||||
local result = {}
|
||||
for index, recipe in ipairs(recipes or {}) do
|
||||
local copy = {}
|
||||
for key, value in pairs(recipe) do copy[key] = value end
|
||||
if (paused or {})[recipe.key] then copy.paused = true end
|
||||
if optIn ~= nil and recipe.enabled and not recipe.byChain and not optIn[recipe.key] then copy.notChosen = true end
|
||||
if (userOff or {})[recipe.key] and not recipe.enabled then copy.cyclesPerMonth = 0 end
|
||||
result[index] = copy
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function copySet(map)
|
||||
local result = {}
|
||||
for key, value in pairs(map or {}) do result[key] = value end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er kædens (= rødderne, inkl. pausede).
|
||||
function ADChain.enabledKeys(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Spillerens valg og forældede nøgler, før kæden regner (rent: state røres ikke).
|
||||
-- opts = {prevEnabled, isPartial, firstEval}. -> {userOff, byChain, paused, roots, added, forgotten, droppedPaused, cleared}
|
||||
-- * kædens produktion er slukket: af spilleren -> userOff (added); første tick efter indlæsning -> glemt (forgotten)
|
||||
-- * en produktion (ikke kædens) der var tændt sidste tick, er slukket -> userOff (added)
|
||||
-- * userOff ryddes kun når der kommer en ny rod (spilleren tænder noget) — ikke når noget "ikke længere bruges"
|
||||
-- (det gav tænd/sluk-løkker); nye slukninger i samme tick beholdes
|
||||
-- * pausede nøgler for produktioner der er væk eller slukket, droppes (droppedPaused)
|
||||
-- Delvise data: userOff ryddes ikke, rødderne opdateres ikke og pausede droppes ikke.
|
||||
function ADChain.reconcile(state, recipes, opts)
|
||||
opts = opts or {}
|
||||
local roots = ADChain.enabledKeys(recipes)
|
||||
local rootAdded = false
|
||||
if state.roots ~= nil then
|
||||
for key in pairs(roots) do if not state.roots[key] then rootAdded = true end end
|
||||
end
|
||||
local result = {userOff = copySet(state.userOff), byChain = copySet(state.byChain), paused = copySet(state.paused),
|
||||
roots = opts.isPartial and state.roots or roots, added = {}, forgotten = {}, droppedPaused = {}, cleared = false}
|
||||
if rootAdded and not opts.isPartial and next(result.userOff) ~= nil then
|
||||
result.userOff, result.cleared = {}, true
|
||||
end
|
||||
local present = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
present[recipe.key] = recipe
|
||||
if not recipe.enabled and recipe.byChain then
|
||||
result.byChain[recipe.key] = nil
|
||||
if opts.firstEval then
|
||||
table.insert(result.forgotten, recipe.key)
|
||||
elseif not result.userOff[recipe.key] then
|
||||
result.userOff[recipe.key] = true
|
||||
table.insert(result.added, recipe.key)
|
||||
end
|
||||
elseif not recipe.enabled and (opts.prevEnabled or {})[recipe.key] and not result.userOff[recipe.key] then
|
||||
result.userOff[recipe.key] = true
|
||||
table.insert(result.added, recipe.key)
|
||||
end
|
||||
end
|
||||
if not opts.isPartial then
|
||||
for key in pairs(state.paused or {}) do
|
||||
if present[key] == nil or not present[key].enabled then
|
||||
result.paused[key] = nil
|
||||
table.insert(result.droppedPaused, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(result.added); table.sort(result.forgotten); table.sort(result.droppedPaused)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Rødder hvis flows ikke må stoppes: kædens aktuelle rødder + pausede rødder (pause rører intet).
|
||||
function ADChain.activeRoots(chain, paused)
|
||||
local result = {}
|
||||
for _, root in ipairs((chain or {}).roots or {}) do result[root.key] = true end
|
||||
for key in pairs(paused or {}) do result[key] = true end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Mest brugte Wait-pulje blandt forsyningsflows til bygningen, ellers "".
|
||||
function ADChain.waitPoolFor(runs, building)
|
||||
local counts, best, bestCount = {}, "", 0
|
||||
for _, run in ipairs(runs or {}) do
|
||||
local group = run.waitPoolGroup
|
||||
if run.kind == "supply" and group ~= nil and group ~= "" and contains(run.targetBuildingIds, building) then
|
||||
counts[group] = (counts[group] or 0) + 1
|
||||
end
|
||||
end
|
||||
for _, group in ipairs(sortedKeys(counts)) do
|
||||
if counts[group] > bestCount then best, bestCount = group, counts[group] end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
function ADChain.flowName(fillTitle, buildingName)
|
||||
return "Kæde: " .. tostring(fillTitle) .. " → " .. tostring(buildingName)
|
||||
end
|
||||
|
||||
local function covers(run, edge)
|
||||
return contains(run.targetBuildingIds, edge.consumer)
|
||||
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType))
|
||||
end
|
||||
|
||||
-- Bruger en af de aktuelle kanter stadig flowet? -> kantens rodnøgle (flowet flyttes dertil), ellers nil.
|
||||
function ADChain.stillNeeded(runView, edges)
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if covers(runView, edge) then return edge.rootKey end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- {[forbruger|vare] = "ok"|"auto"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
|
||||
function ADChain.flowStatus(edges, runs, noVehicle)
|
||||
local result = {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
-- udgangen står på Fordel: spillet leverer selv, intet flow
|
||||
local status = edge.delivery == "auto" and "auto" or "missing"
|
||||
for _, run in ipairs(edge.delivery == "auto" and {} or runs or {}) do
|
||||
if run.kind == "supply" and covers(run, edge) then
|
||||
if not run.stopped then status = "ok"; break end
|
||||
local reason = (noVehicle or {})[run.id]
|
||||
if reason == "pallet" then status = "pallet"
|
||||
elseif reason then status = "noVehicle"
|
||||
elseif status == "missing" then status = "stopped" end
|
||||
end
|
||||
end
|
||||
result[tostring(edge.consumer) .. "|" .. tostring(edge.fillType)] = status
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Flows i det format ADChainActions regner på.
|
||||
function ADChain.runsView(runs)
|
||||
local list = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(list, {id = run.id, name = run.name, kind = run.kind, targetBuildingIds = run.targetBuildingIds,
|
||||
ingredientMode = run.ingredientMode, ingredients = run.ingredients, stopped = run.stopped == true,
|
||||
chain = run.chain, waitPoolGroup = run.waitPoolGroup,
|
||||
-- traktorerne: uden dem ligner alle kædeflows tomme (sammenlægning/sletning må kun ramme tomme)
|
||||
vehicleIds = run.vehicleIds or {}})
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Manglende led der ikke var der sidst (nøgle = rodnøgle|vare|art). -> nye problemer (hver nøgle én gang), nøglerne nu.
|
||||
-- Et problem der forsvinder, glemmes; kommer det igen, er det nyt. prevKeys røres ikke.
|
||||
function ADChain.problemKey(problem)
|
||||
return tostring(problem.rootKey) .. "|" .. tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||
end
|
||||
|
||||
function ADChain.newProblems(prevKeys, problems)
|
||||
local fresh, keys = {}, {}
|
||||
for _, problem in ipairs(problems or {}) do
|
||||
local key = ADChain.problemKey(problem)
|
||||
if not keys[key] then
|
||||
keys[key] = true
|
||||
if not (prevKeys or {})[key] then table.insert(fresh, problem) end
|
||||
end
|
||||
end
|
||||
return fresh, keys
|
||||
end
|
||||
|
||||
-- ---- evaluering ----
|
||||
|
||||
local function farmId()
|
||||
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||||
end
|
||||
|
||||
local function plan(data, state)
|
||||
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff, state.optIn)
|
||||
local chain = ADChainModel.build(recipes, data.husbandry, data.crops, data.fieldGoods)
|
||||
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
|
||||
local protected = {}
|
||||
if next(state.paused) ~= nil then
|
||||
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff, state.optIn), data.husbandry, data.crops, data.fieldGoods).needRecipes
|
||||
end
|
||||
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
|
||||
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
|
||||
local shortages, actions = {}, {}
|
||||
if not data.fieldsIncomplete then
|
||||
shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields)
|
||||
state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages)
|
||||
actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.priceOf or data.prices, ADChain.dismissedFruits(state.dismissed), data.harvesting)
|
||||
end
|
||||
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
|
||||
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
|
||||
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain, data.recipes)}
|
||||
end
|
||||
|
||||
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
|
||||
-- Spillerens valg og forældede nøgler ind i tilstanden (kun ved tick, før der regnes).
|
||||
local function applyReconcile(state, data)
|
||||
local result = ADChain.reconcile(state, data.recipes, {prevEnabled = ADChain.prevEnabled, isPartial = data.partial == true,
|
||||
firstEval = ADChain.afterLoad})
|
||||
if result.cleared then logInfo("ny produktion tændt — slukkede produktioner må bruges igen") end
|
||||
for _, key in ipairs(result.added) do logInfo("spilleren slukkede %s — kæden tænder den ikke igen", key) end
|
||||
for _, key in ipairs(result.forgotten) do logInfo("kæden glemmer %s (ikke tændt efter indlæsning)", key) end
|
||||
for _, key in ipairs(result.droppedPaused) do logInfo("pause for %s fjernet: produktionen er væk eller slukket", key) end
|
||||
state.userOff, state.byChain, state.paused, state.roots = result.userOff, result.byChain, result.paused, result.roots
|
||||
ADChain.prevEnabled = ADChain.enabledKeys(data.recipes)
|
||||
ADChain.afterLoad = false
|
||||
end
|
||||
|
||||
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
|
||||
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
|
||||
-- mellemtiden meldes ved næste hele tjek).
|
||||
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
|
||||
local function dependencyCheck(data, plan, runs, act, previous)
|
||||
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
|
||||
if data.readFailed then
|
||||
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
|
||||
end
|
||||
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
|
||||
local enabling, recipes = {}, {}
|
||||
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
|
||||
for _, recipe in ipairs(data.recipes or {}) do
|
||||
if not enabling[recipe.key] then table.insert(recipes, recipe) end
|
||||
end
|
||||
local prev = act and ADChain.depPrevEnabled or nil
|
||||
local result = {
|
||||
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||
}
|
||||
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
|
||||
return result
|
||||
end
|
||||
|
||||
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
|
||||
function ADChain.evaluate(adEnv, act)
|
||||
local state = ADChain.state
|
||||
local ok, data = pcall(ADChainData.collect, adEnv, farmId(), state.byChain)
|
||||
if not ok then
|
||||
if not ADChain.collectFailed then Logging.warning(ADChain.LOG_PREFIX .. "gården kunne ikke læses: %s", tostring(data)) end
|
||||
ADChain.collectFailed = true
|
||||
return ADChain.last
|
||||
end
|
||||
ADChain.collectFailed = false
|
||||
if act then
|
||||
local okState, errState = pcall(applyReconcile, state, data)
|
||||
if not okState then Logging.warning(ADChain.LOG_PREFIX .. "tilstanden kunne ikke opdateres: %s", tostring(errState)) end
|
||||
end
|
||||
local okPlan, result = pcall(plan, data, state)
|
||||
if not okPlan then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "kæden kunne ikke beregnes: %s", tostring(result))
|
||||
return ADChain.last
|
||||
end
|
||||
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
|
||||
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
|
||||
if not okDep then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
|
||||
dependency = nil
|
||||
end
|
||||
local paused = {}
|
||||
for key in pairs(state.paused) do paused[key] = true end
|
||||
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
|
||||
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
|
||||
paused = paused, userOff = copySet(state.userOff), finalKeep = copySet(state.finalKeep or {}),
|
||||
sellable = ADChain.sellableRoots(data, result, runs), candidates = ADChain.candidates(data.recipes, state), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
|
||||
dependency = dependency}
|
||||
return ADChain.last
|
||||
end
|
||||
|
||||
-- Alder (ms, spillets ur) af det sidste resultat; math.huge når der intet er. Fanen genbruger et friskt resultat.
|
||||
function ADChain.lastAge()
|
||||
if ADChain.last == nil then return math.huge end
|
||||
return ADChain.now - (ADChain.last.at or 0)
|
||||
end
|
||||
|
||||
-- Engangsbesked pr. nyt manglende led (kun fra tick). Ikke i opstartsfristen og ikke mens gården er læst delvist
|
||||
-- eller markscanningen kører (ellers falske alarmer lige efter indlæsning) — så huskes intet, og det meldes senere.
|
||||
-- Højst MAX_PROBLEM_NOTES beskeder pr. tick; resten samles i én besked (alle logges stadig).
|
||||
ADChain.MAX_PROBLEM_NOTES = 3
|
||||
function ADChain.notifyProblems(last)
|
||||
if last == nil or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||
if last.fieldsIncomplete or (last.data ~= nil and last.data.partial) then return 0 end
|
||||
local fresh, keys = ADChain.newProblems(ADChain.problemKeys, last.chain ~= nil and last.chain.problems or {})
|
||||
ADChain.problemKeys = keys
|
||||
if #fresh == 0 or ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||
local fmt = SmartPickupFrame ~= nil and SmartPickupFrame.formatCompact or nil
|
||||
local rootNames = {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do
|
||||
rootNames[root.key] = tostring((last.titles or {})[root.name] or root.name)
|
||||
end
|
||||
for index, problem in ipairs(fresh) do
|
||||
-- kædens navn foran, så to kæder der mangler samme vare kan skelnes
|
||||
local text = tostring(rootNames[problem.rootKey] or problem.rootKey) .. ": " .. ADChainView.problemText(problem, last, textOf, nil, fmt)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "manglende led: %s", text)
|
||||
if index <= ADChain.MAX_PROBLEM_NOTES then ADFieldJobs.notify(text, true) end
|
||||
end
|
||||
-- resten samles i én besked (fx mange fermenteringssiloer efter indlæsning); alle står i fanen Kæder
|
||||
if #fresh > ADChain.MAX_PROBLEM_NOTES then
|
||||
local ok, text = pcall(string.format, textOf("spu_chain_notifyMore"), #fresh - ADChain.MAX_PROBLEM_NOTES)
|
||||
ADFieldJobs.notify(ok and text or ("+" .. tostring(#fresh - ADChain.MAX_PROBLEM_NOTES)), true)
|
||||
end
|
||||
return #fresh
|
||||
end
|
||||
|
||||
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
|
||||
function ADChain.notifyDependencies(last)
|
||||
local dependency = last ~= nil and last.dependency or nil
|
||||
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||
local warnings = dependency.warnings
|
||||
dependency.warnings = {}
|
||||
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
|
||||
for _, warning in ipairs(warnings) do
|
||||
local text = ADDependency.notifyText(warning, textOf, titleOf)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
|
||||
ADFieldJobs.notify(text, true)
|
||||
end
|
||||
return #warnings
|
||||
end
|
||||
|
||||
function ADChain.tick(adEnv, dt)
|
||||
if adEnv == nil then return end
|
||||
ADChain.now = ADChain.now + (dt or 0)
|
||||
ADChain.timer = ADChain.timer + (dt or 0)
|
||||
if ADChain.now < ADChain.STARTUP_GRACE_MS or ADChain.timer < ADChain.TICK_MS then return end
|
||||
ADChain.timer = 0
|
||||
local last = ADChain.evaluate(adEnv, true)
|
||||
local ok, err = pcall(ADChain.notifyProblems, last)
|
||||
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
|
||||
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
|
||||
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
|
||||
end
|
||||
|
||||
-- ---- GUI-handlinger ----
|
||||
|
||||
local function suggestionFor(fieldId)
|
||||
for _, action in ipairs(ADChain.last ~= nil and ADChain.last.actions or {}) do
|
||||
if action.fieldId == fieldId then return action end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Godkend forslag: marken får kædens afgrøde som mål (kilde "chain").
|
||||
function ADChain.approve(fieldId)
|
||||
local action = suggestionFor(fieldId)
|
||||
if action == nil then return false end
|
||||
local ok, err = pcall(function()
|
||||
-- et auto-skift må aldrig overskrive en afgrøde spilleren har sat siden; et forslag godkender spilleren selv
|
||||
if action.mode == "auto" then
|
||||
local plan = ADFieldJobs.getField(fieldId).plan or {}
|
||||
if plan.targetSource == "user" then error("spilleren har selv valgt afgrøde") end
|
||||
end
|
||||
ADFieldJobs.setTarget(fieldId, action.to, "chain")
|
||||
end)
|
||||
if not ok then
|
||||
Logging.warning(ADChain.LOG_PREFIX .. "F%s: forslag kunne ikke godkendes: %s", tostring(fieldId), tostring(err))
|
||||
return false
|
||||
end
|
||||
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag godkendt: %s -> %s", tostring(fieldId), tostring(action.from), tostring(action.to))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Afvis forslag: vises ikke igen, før manglen for afgrøden ændrer sig > 25 %.
|
||||
function ADChain.dismiss(fieldId)
|
||||
local action = suggestionFor(fieldId)
|
||||
if action == nil then return false end
|
||||
local missingHa = action.hectares or 0
|
||||
for _, shortage in ipairs(ADChain.last.shortages or {}) do
|
||||
if shortage.fruit == action.to then missingHa = shortage.missingHa end
|
||||
end
|
||||
ADChain.state.dismissed[fieldId] = {fruit = action.to, missingHa = missingHa}
|
||||
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag %s afvist", tostring(fieldId), tostring(action.to))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er valgt som kæde (og ikke pauset): {{key, name, buildingName}} til fanen Kæder.
|
||||
function ADChain.candidates(recipes, state)
|
||||
local list = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain and not (state.optIn or {})[recipe.key] and not (state.paused or {})[recipe.key] then
|
||||
table.insert(list, {key = recipe.key, name = recipe.name, buildingName = recipe.buildingName})
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if tostring(a.buildingName) ~= tostring(b.buildingName) then return tostring(a.buildingName) < tostring(b.buildingName) end
|
||||
return tostring(a.name) < tostring(b.name)
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Slå en produktion til/fra som kæde (fra = også pausen ophæves, kæden slipper den ved næste tick).
|
||||
function ADChain.setOptIn(recipeKey, on)
|
||||
if recipeKey == nil then return end
|
||||
ADChain.state.optIn[recipeKey] = on and true or nil
|
||||
if not on then ADChain.state.paused[recipeKey] = nil end
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(recipeKey), on and "slået til som kæde" or "fjernet fra kæderne")
|
||||
end
|
||||
|
||||
-- Rødder med et slutprodukt der kan sælges straks (gården bruger det ikke selv). -> {[rodnøgle] = true}
|
||||
function ADChain.sellableRoots(data, result, runs)
|
||||
local set = {}
|
||||
local ok, sales = pcall(ADChainActions.finalSales, result.chain, data.recipes, result.edges, {}, ADChainActions.farmUses(data, runs))
|
||||
for _, sale in ipairs(ok and sales or {}) do set[sale.key] = true end
|
||||
return set
|
||||
end
|
||||
|
||||
-- Slutprodukt for en kæde: keep = true -> "Behold", ellers "Sælg straks" (standard). Anvendes ved næste tick.
|
||||
function ADChain.setFinalKeep(rootKey, keep)
|
||||
if rootKey == nil then return end
|
||||
ADChain.state.finalKeep[rootKey] = keep and true or nil
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s: slutprodukt %s", tostring(rootKey), keep and "beholdes" or "sælges straks")
|
||||
end
|
||||
|
||||
function ADChain.setPaused(rootKey, isPaused)
|
||||
if rootKey == nil then return end
|
||||
ADChain.state.paused[rootKey] = isPaused and true or nil
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(rootKey), isPaused and "på pause" or "genoptaget")
|
||||
end
|
||||
|
||||
-- ---- gem / indlæs (samme sti-opslag som runs; skrives kun fra ADRunsController.save) ----
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADChain.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADChain.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupChain", path, ADChain.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local records = ADChain.stateToRecords(ADChain.state)
|
||||
for index, key in ipairs(records.byChain) do
|
||||
xmlFile:setString(string.format("%s.byChain.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.paused) do
|
||||
xmlFile:setString(string.format("%s.paused.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.userOff) do
|
||||
xmlFile:setString(string.format("%s.userOff.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.finalKeep) do
|
||||
xmlFile:setString(string.format("%s.finalKeep.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.soldByChain) do
|
||||
xmlFile:setString(string.format("%s.soldByChain.output(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
for index, key in ipairs(records.optIn) do
|
||||
xmlFile:setString(string.format("%s.optIn.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
if records.roots ~= nil then
|
||||
xmlFile:setBool(ADChain.ROOT .. ".roots#known", true)
|
||||
for index, key in ipairs(records.roots) do
|
||||
xmlFile:setString(string.format("%s.roots.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||
end
|
||||
end
|
||||
for index, entry in ipairs(records.dismissed) do
|
||||
local key = string.format("%s.dismissed.field(%d)", ADChain.ROOT, index - 1)
|
||||
xmlFile:setInt(key .. "#id", entry.id)
|
||||
xmlFile:setString(key .. "#fruit", entry.fruit)
|
||||
xmlFile:setString(key .. "#missingHa", string.format("%.2f", entry.missingHa or 0))
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADChain.load()
|
||||
ADChain.state = ADChain.newState()
|
||||
ADChain.noVehicle = {}
|
||||
ADChain.prevEnabled, ADChain.afterLoad = nil, true
|
||||
ADChain.problemKeys = {}
|
||||
ADChain.depPrevEnabled = nil
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
|
||||
if xmlFile == nil then return end
|
||||
local records = {byChain = {}, paused = {}, userOff = {}, dismissed = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||
xmlFile:iterate(ADChain.ROOT .. ".optIn.recipe", function(_, key)
|
||||
table.insert(records.optIn, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".finalKeep.root", function(_, key)
|
||||
table.insert(records.finalKeep, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".soldByChain.output", function(_, key)
|
||||
table.insert(records.soldByChain, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".byChain.recipe", function(_, key)
|
||||
table.insert(records.byChain, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".paused.root", function(_, key)
|
||||
table.insert(records.paused, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
xmlFile:iterate(ADChain.ROOT .. ".userOff.recipe", function(_, key)
|
||||
table.insert(records.userOff, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
if xmlFile:getBool(ADChain.ROOT .. ".roots#known", false) then
|
||||
records.roots = {}
|
||||
xmlFile:iterate(ADChain.ROOT .. ".roots.recipe", function(_, key)
|
||||
table.insert(records.roots, xmlFile:getString(key .. "#key"))
|
||||
end)
|
||||
end
|
||||
xmlFile:iterate(ADChain.ROOT .. ".dismissed.field", function(_, key)
|
||||
table.insert(records.dismissed, {id = xmlFile:getInt(key .. "#id"), fruit = xmlFile:getString(key .. "#fruit"),
|
||||
missingHa = tonumber(xmlFile:getString(key .. "#missingHa"))})
|
||||
end)
|
||||
xmlFile:delete()
|
||||
ADChain.state = ADChain.stateFromRecords(records)
|
||||
Logging.info(ADChain.LOG_PREFIX .. "%d produktioner tændt af kæden, %d pausede kæder indlæst", #records.byChain, #records.paused)
|
||||
end
|
||||
@@ -0,0 +1,230 @@
|
||||
-- AD Chain Actions (ren logik): mellemled der skal tændes/slukkes, flows der mangler eller skal stoppes.
|
||||
|
||||
ADChainActions = {}
|
||||
|
||||
-- protected = {[nøgle] = cyklusser}: behov fra pausede rødder — slukkes ikke, men tændes heller ikke.
|
||||
function ADChainActions.productions(recipes, needRecipes, protected)
|
||||
local result = {enable = {}, disable = {}}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
local needed = ((needRecipes or {})[recipe.key] or 0) > 0
|
||||
local kept = ((protected or {})[recipe.key] or 0) > 0
|
||||
if needed and not recipe.enabled then table.insert(result.enable, recipe.key) end
|
||||
if not needed and not kept and recipe.enabled and recipe.byChain then table.insert(result.disable, recipe.key) end
|
||||
end
|
||||
table.sort(result.enable); table.sort(result.disable)
|
||||
return result
|
||||
end
|
||||
|
||||
local SUPPLIED = {production = true, husbandry = true, field = true}
|
||||
|
||||
-- Hvordan varen når frem, ud fra producenternes udgang: "auto" = alle fordeler (spillet leverer selv til gårdens
|
||||
-- fabrikker; kædens forbrugere er altid fabrikker) -> intet flow; "sold" = en producent sælger mellemvaren væk;
|
||||
-- ellers "haul" (Behold: spawn/lager ved fabrikken, eller stald/mark) -> et flow henter. recipesByKey = {[nøgle] = opskrift}.
|
||||
function ADChainActions.delivery(edge, recipesByKey)
|
||||
local producers = edge.producers or {}
|
||||
if #producers == 0 or recipesByKey == nil then return "haul" end
|
||||
local all, sold = true, false
|
||||
for _, key in ipairs(producers) do
|
||||
local mode = ADChainActions.producerMode(recipesByKey, key, edge.fillType)
|
||||
if mode == "sell" then sold = true end
|
||||
if mode ~= "distribute" then all = false end
|
||||
end
|
||||
if sold then return "sold" end
|
||||
return all and "auto" or "haul"
|
||||
end
|
||||
|
||||
function ADChainActions.producerMode(recipesByKey, key, fillType)
|
||||
local recipe = (recipesByKey or {})[key]
|
||||
return recipe ~= nil and (recipe.outputModes or {})[fillType] or nil
|
||||
end
|
||||
|
||||
local function producerKeys(node)
|
||||
local keys = {}
|
||||
for _, producer in ipairs(node.producers or {}) do table.insert(keys, producer.key) end
|
||||
return keys
|
||||
end
|
||||
|
||||
-- recipes (valgfri): gårdens opskrifter med outputModes -> kanten får delivery (se ADChainActions.delivery).
|
||||
function ADChainActions.edges(chain, recipes)
|
||||
local list, seen, byKey = {}, {}, nil
|
||||
if recipes ~= nil then
|
||||
byKey = {}
|
||||
for _, recipe in ipairs(recipes) do byKey[recipe.key] = recipe end
|
||||
end
|
||||
local function walk(nodes, rootKey)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
local id = tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||
if SUPPLIED[node.source] and node.consumer ~= nil and not seen[id] then
|
||||
seen[id] = true
|
||||
local edge = {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey,
|
||||
producers = producerKeys(node)}
|
||||
edge.delivery = ADChainActions.delivery(edge, byKey)
|
||||
edge.soldBy = {}
|
||||
for _, key in ipairs(edge.producers) do
|
||||
if ADChainActions.producerMode(byKey, key, edge.fillType) == "sell" then table.insert(edge.soldBy, key) end
|
||||
end
|
||||
table.insert(list, edge)
|
||||
end
|
||||
walk(node.children, rootKey)
|
||||
end
|
||||
end
|
||||
for _, root in ipairs((chain or {}).roots or {}) do walk(root.children, root.key) end
|
||||
return list
|
||||
end
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADChainActions.isCovered(edge, runs)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.kind == "supply" and contains(run.targetBuildingIds, edge.consumer)
|
||||
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType)) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ADChainActions.missingFlows(edges, runs)
|
||||
local list = {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if edge.delivery ~= "auto" and not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Mellemvarer en producent sælger væk (kanten er "sold") -> {{key, fillType}} én gang hver; kæden sætter dem til Behold.
|
||||
function ADChainActions.soldIntermediates(edges)
|
||||
local list, seen = {}, {}
|
||||
for _, edge in ipairs(edges or {}) do
|
||||
if edge.delivery == "sold" then
|
||||
for _, key in ipairs(edge.soldBy or {}) do
|
||||
local id = key .. "|" .. edge.fillType
|
||||
if not seen[id] then
|
||||
seen[id] = true
|
||||
table.insert(list, {key = key, fillType = edge.fillType})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
function ADChainActions.flowsToStop(runs, activeRootKeys)
|
||||
local list = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
-- også stoppede kædeflows uden traktor (de ryddes op); stoppede med traktor er allerede sluppet/spillerens
|
||||
local idle = #(run.vehicleIds or {}) == 0
|
||||
if run.chain ~= nil and run.chain ~= "" and (not run.stopped or idle) and not (activeRootKeys or {})[run.chain] then
|
||||
table.insert(list, run)
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- ---- ét kædeflow pr. bygning (in-game 30/9: ét flow pr. vare og bygning gav mange stoppede flows) ----
|
||||
|
||||
-- Manglende kanter samlet pr. modtagerbygning: {consumer, consumerName, rootKey, fillTypes} i fundet rækkefølge.
|
||||
function ADChainActions.groupMissing(missing)
|
||||
local groups, byConsumer = {}, {}
|
||||
for _, edge in ipairs(missing or {}) do
|
||||
local group = byConsumer[edge.consumer]
|
||||
if group == nil then
|
||||
group = {consumer = edge.consumer, consumerName = edge.consumerName, rootKey = edge.rootKey, fillTypes = {}}
|
||||
byConsumer[edge.consumer] = group
|
||||
table.insert(groups, group)
|
||||
end
|
||||
if not contains(group.fillTypes, edge.fillType) then table.insert(group.fillTypes, edge.fillType) end
|
||||
end
|
||||
return groups
|
||||
end
|
||||
|
||||
local function isChainRun(run)
|
||||
return run.chain ~= nil and run.chain ~= "" and run.kind == "supply" and run.ingredientMode == "manual"
|
||||
end
|
||||
|
||||
-- Kædens eget flow (manuelle varer) til bygningen -> run-id, så nye varer lægges i det i stedet for et nyt flow.
|
||||
function ADChainActions.chainRunFor(runs, consumer)
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if isChainRun(run) and contains(run.targetBuildingIds, consumer) then return run.id end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Flere tomme kædeflows (uden traktorer) til samme bygning: det første beholdes med alle varerne, resten slettes.
|
||||
-- -> {{keepId, add = {varer}, deleteIds = {id}}}
|
||||
function ADChainActions.mergeIdle(runs)
|
||||
local byConsumer, order = {}, {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
local target = (run.targetBuildingIds or {})[1]
|
||||
if isChainRun(run) and target ~= nil and #(run.targetBuildingIds or {}) == 1 and #(run.vehicleIds or {}) == 0 then
|
||||
if byConsumer[target] == nil then byConsumer[target] = {}; table.insert(order, target) end
|
||||
table.insert(byConsumer[target], run)
|
||||
end
|
||||
end
|
||||
local merges = {}
|
||||
for _, target in ipairs(order) do
|
||||
local list = byConsumer[target]
|
||||
if #list > 1 then
|
||||
local keep, add, deleteIds = list[1], {}, {}
|
||||
for index = 2, #list do
|
||||
for _, name in ipairs(list[index].ingredients or {}) do
|
||||
if not contains(keep.ingredients, name) and not contains(add, name) then table.insert(add, name) end
|
||||
end
|
||||
table.insert(deleteIds, list[index].id)
|
||||
end
|
||||
table.insert(merges, {keepId = keep.id, add = add, deleteIds = deleteIds})
|
||||
end
|
||||
end
|
||||
return merges
|
||||
end
|
||||
|
||||
-- Varer gården selv bruger: stalde æder dem, en tændt produktion bruger dem, gården har forbrug af dem, eller et
|
||||
-- flow (ikke udkørsel) kører dem. -> {[vare] = true}. Den slags sælges aldrig straks (in-game 30/9: TMR og roesnitter).
|
||||
function ADChainActions.farmUses(data, runs)
|
||||
local uses = {}
|
||||
for name in pairs((data or {}).barnFood or {}) do uses[name] = true end
|
||||
for name, rate in pairs((data or {}).rates or {}) do
|
||||
if (rate.consumption or 0) > 0 then uses[name] = true end
|
||||
end
|
||||
for _, recipe in ipairs((data or {}).recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name in pairs(recipe.inputs or {}) do uses[name] = true end
|
||||
end
|
||||
end
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.kind ~= "outbound" and run.ingredientMode == "manual" then
|
||||
for _, name in ipairs(run.ingredients or {}) do uses[name] = true end
|
||||
end
|
||||
end
|
||||
return uses
|
||||
end
|
||||
|
||||
-- Slutprodukter der skal sælges straks: aktive rødders udgange, som ikke selv er en kant i kæden (mellemvare) og
|
||||
-- som gården ikke selv bruger (farmUses). keep = {[rodnøgle] = true} for rødder sat til "Behold". -> {{key, fillType}}
|
||||
function ADChainActions.finalSales(chain, recipes, edges, keep, farmUses)
|
||||
local intermediate, byKey, list = {}, {}, {}
|
||||
for _, edge in ipairs(edges or {}) do intermediate[edge.fillType] = true end
|
||||
for _, recipe in ipairs(recipes or {}) do byKey[recipe.key] = recipe end
|
||||
for _, root in ipairs((chain or {}).roots or {}) do
|
||||
local recipe = byKey[root.key]
|
||||
if recipe ~= nil and not (keep or {})[root.key] then
|
||||
local names = {}
|
||||
for name in pairs(recipe.outputs or {}) do table.insert(names, name) end
|
||||
table.sort(names)
|
||||
for _, name in ipairs(names) do
|
||||
if not intermediate[name] and not (farmUses or {})[name] then table.insert(list, {key = root.key, fillType = name}) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Kædeflow for en kæde der er slået fra: uden traktor slettes det (auto-oprettet og tomt), ellers stoppes og slippes
|
||||
-- det (bliver spillerens eget). (in-game 30/9: ellers 12 tomme "Kæde: …"-flows blandt spillerens egne)
|
||||
function ADChainActions.releaseAction(run)
|
||||
if #(run.vehicleIds or {}) == 0 then return "delete" end
|
||||
return "release"
|
||||
end
|
||||
@@ -0,0 +1,511 @@
|
||||
-- AD Chain Data (læse-adapter)
|
||||
-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
|
||||
-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
|
||||
-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
|
||||
-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
|
||||
|
||||
ADChainData = {}
|
||||
ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||
ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
|
||||
ADChainData.warned = {}
|
||||
|
||||
-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
|
||||
function ADChainData.isPermanent(fruitName)
|
||||
if type(fruitName) ~= "string" or fruitName == "" then return false end
|
||||
return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
|
||||
end
|
||||
|
||||
function ADChainData.recipeKey(placeableUid, productionId)
|
||||
return tostring(placeableUid) .. ":" .. tostring(productionId)
|
||||
end
|
||||
|
||||
-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
|
||||
function ADChainData.cyclesPerMonth(production)
|
||||
return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
|
||||
end
|
||||
|
||||
function ADChainData.warnOnce(key, fmt, ...)
|
||||
if ADChainData.warned[key] then return end
|
||||
ADChainData.warned[key] = true
|
||||
if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
|
||||
end
|
||||
|
||||
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
|
||||
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
|
||||
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
|
||||
local function failed(data, key, fmt, ...)
|
||||
data.partial, data.readFailed = true, true
|
||||
ADChainData.warnOnce(key, fmt, ...)
|
||||
end
|
||||
|
||||
-- ---- hjælpere ----
|
||||
|
||||
-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
|
||||
local function nameResolver(data)
|
||||
return function(index)
|
||||
if index == nil or g_fillTypeManager == nil then return nil end
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(index)
|
||||
if name == nil then return nil end
|
||||
if data.fillIndex[name] == nil then
|
||||
data.fillIndex[name] = index
|
||||
local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
|
||||
data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
|
||||
end
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
|
||||
local function amountsByName(list, nameOf)
|
||||
local result = {}
|
||||
for _, entry in ipairs(list or {}) do
|
||||
local name = nameOf(entry.type)
|
||||
if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function placeableId(placeable)
|
||||
if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||
return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
|
||||
end
|
||||
|
||||
-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
|
||||
local function placeableName(placeable, fallback)
|
||||
local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||
if own ~= nil then return own end
|
||||
if placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then return tostring(name) end
|
||||
end
|
||||
return fallback
|
||||
end
|
||||
|
||||
local function isEnabled(point, production)
|
||||
if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
|
||||
for _, active in ipairs(point.activeProductions or {}) do
|
||||
if active == production then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ---- læsning pr. del ----
|
||||
|
||||
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
|
||||
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
|
||||
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
|
||||
function ADChainData.outputModeName(mode)
|
||||
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
|
||||
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
|
||||
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
|
||||
return "keep"
|
||||
end
|
||||
|
||||
local function outputModes(point, outputs, nameOf)
|
||||
local modes = {}
|
||||
if point.getOutputDistributionMode == nil then return modes end
|
||||
for _, output in ipairs(outputs or {}) do
|
||||
local name = output.type ~= nil and nameOf(output.type) or nil
|
||||
if name ~= nil then
|
||||
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
|
||||
modes[name] = ADChainData.outputModeName(ok and mode or nil)
|
||||
end
|
||||
end
|
||||
return modes
|
||||
end
|
||||
|
||||
local function readFactory(placeable, point, byChain, nameOf)
|
||||
local uid = placeableId(placeable)
|
||||
if uid == nil then return {}, {} end
|
||||
local buildingName = placeableName(placeable, uid)
|
||||
local recipes, points = {}, {}
|
||||
for _, production in ipairs(point.productions or {}) do
|
||||
local key = ADChainData.recipeKey(uid, production.id)
|
||||
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
|
||||
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
|
||||
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
|
||||
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
|
||||
outputModes = outputModes(point, production.outputs, nameOf)})
|
||||
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
|
||||
end
|
||||
return recipes, points
|
||||
end
|
||||
|
||||
local function readHerd(placeable, nameOf)
|
||||
local uid = placeableId(placeable)
|
||||
local outputs = {}
|
||||
for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
|
||||
end
|
||||
if uid == nil or next(outputs) == nil then return nil end
|
||||
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
|
||||
end
|
||||
|
||||
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
|
||||
local function readBarnFood(data, placeable, nameOf)
|
||||
if ADSources.getHusbandryConsumption == nil then return end
|
||||
local barnName
|
||||
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil and (perDay or 0) > 0 then
|
||||
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
|
||||
data.barnFood[name] = data.barnFood[name] or {}
|
||||
table.insert(data.barnFood[name], barnName)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function readPlaceables(data, farmId, byChain, nameOf)
|
||||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||
for index, placeable in pairs(placeables) do
|
||||
local ok, err = pcall(function()
|
||||
if not ADPrices.isOwnedBy(placeable, farmId) then return end
|
||||
local uid = placeableId(placeable)
|
||||
if uid ~= nil then data.placeables[uid] = placeable end
|
||||
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
|
||||
if point ~= nil then
|
||||
local recipes, points = readFactory(placeable, point, byChain, nameOf)
|
||||
for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
|
||||
for key, entry in pairs(points) do data.points[key] = entry end
|
||||
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
|
||||
local herd = readHerd(placeable, nameOf)
|
||||
if herd ~= nil then table.insert(data.husbandry, herd) end
|
||||
readBarnFood(data, placeable, nameOf)
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
local key = tostring(placeable ~= nil and placeable.uniqueId or index)
|
||||
failed(data, "placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
|
||||
end
|
||||
end
|
||||
table.sort(data.recipes, function(a, b) return a.key < b.key end)
|
||||
end
|
||||
|
||||
-- Markedspris for en afgrøde, først når kæden spørger (kun kandidatmarkernes afgrøder, kun ved mangel);
|
||||
-- hver afgrøde slås højst op én gang pr. læsning. Fejl -> 0 (logget én gang).
|
||||
local function priceResolver(data)
|
||||
local cache = {}
|
||||
return function(fruitName)
|
||||
if cache[fruitName] ~= nil then return cache[fruitName] end
|
||||
local index = data.fruitFillIndex[fruitName]
|
||||
local price = 0
|
||||
if index ~= nil then
|
||||
local ok, quote = pcall(ADSources.getMarketQuote, index)
|
||||
if ok then
|
||||
price = quote ~= nil and quote.price or 0
|
||||
else
|
||||
ADChainData.warnOnce("price:" .. tostring(fruitName), "pris for %s kunne ikke læses: %s", tostring(fruitName), tostring(quote))
|
||||
end
|
||||
end
|
||||
cache[fruitName] = price
|
||||
return price
|
||||
end
|
||||
end
|
||||
|
||||
local function readCrops(data, nameOf)
|
||||
local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
|
||||
for _, fruit in ipairs(fruits) do
|
||||
if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
|
||||
local ok, err = pcall(function()
|
||||
local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
|
||||
if fillName == nil then return end
|
||||
if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
|
||||
data.crops[fillName] = fruit.name
|
||||
data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
|
||||
data.fruitFillIndex[fruit.name] = fruit.fillType.index
|
||||
end)
|
||||
if not ok then failed(data, "fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- markvarer (hø, hakkelse, halm …) ----
|
||||
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
|
||||
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
|
||||
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
|
||||
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
|
||||
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
|
||||
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
|
||||
|
||||
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
|
||||
local function windrowIndex(fruit)
|
||||
local windrow = fruit.windrowFillType
|
||||
if type(windrow) == "number" then return windrow end
|
||||
if type(windrow) == "table" then
|
||||
if windrow.index ~= nil then return windrow.index end
|
||||
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
|
||||
local function converterTargets(converter, add)
|
||||
for _, entry in pairs(converter or {}) do
|
||||
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||
end
|
||||
end
|
||||
|
||||
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
|
||||
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
|
||||
local function readFieldGoods(data, nameOf)
|
||||
local indices = {}
|
||||
local function add(index) if type(index) == "number" then indices[index] = true end end
|
||||
local function guarded(key, fn)
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
|
||||
end
|
||||
if g_fillTypeManager == nil then return end
|
||||
local fruitManager = g_fruitTypeManager
|
||||
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
|
||||
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
|
||||
end
|
||||
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
|
||||
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
|
||||
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
|
||||
end
|
||||
end
|
||||
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
|
||||
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
|
||||
end
|
||||
end
|
||||
-- tørrede varianter (vender: græs-vindue -> hø)
|
||||
if g_fillTypeManager.getConverterDataByName ~= nil then
|
||||
guarded("TEDDER", function()
|
||||
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
|
||||
local windrows = {}
|
||||
for index in pairs(indices) do table.insert(windrows, index) end
|
||||
for _, index in ipairs(windrows) do
|
||||
local entry = tedder[index]
|
||||
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||
end
|
||||
end)
|
||||
end
|
||||
for index in pairs(indices) do
|
||||
local ok, name = pcall(nameOf, index)
|
||||
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
|
||||
end
|
||||
end
|
||||
|
||||
-- Læs i gårdens køretøjer (vogne, høstertanke, marksilo-vogne) tæller som lager: kun afgrøder og markvarer, ikke
|
||||
-- diesel o.l. loads = {{name, liters}} (in-game 30/9: høst på vej hjem gav falsk mangel og forslag).
|
||||
function ADChainData.addVehicleLoads(data, loads)
|
||||
for _, load in ipairs(loads or {}) do
|
||||
local name, liters = load.name, load.liters or 0
|
||||
local fruit = name ~= nil and (data.crops or {})[name] or nil
|
||||
if liters > 0 and (fruit ~= nil or (data.fieldGoods or {})[name]) then
|
||||
data.stockByFill[name] = (data.stockByFill[name] or 0) + liters
|
||||
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function readVehicleLoads(data, farmId, nameOf)
|
||||
local ok, err = pcall(function()
|
||||
local loads = {}
|
||||
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
|
||||
local spec = vehicle.spec_fillUnit
|
||||
if spec ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
|
||||
for index, unit in ipairs(spec.fillUnits or {}) do
|
||||
local level = vehicle:getFillUnitFillLevel(index) or 0
|
||||
if level > 0 then
|
||||
table.insert(loads, {name = nameOf(vehicle:getFillUnitFillType(index) or unit.fillType), liters = level})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ADChainData.addVehicleLoads(data, loads)
|
||||
end)
|
||||
if not ok then ADChainData.warnOnce("vehicleLoads", "læs i køretøjer kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
local function readStock(data, adEnv, farmId, nameOf)
|
||||
local ok, err = pcall(function()
|
||||
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
|
||||
local fruit = data.crops[name or ""]
|
||||
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
|
||||
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
|
||||
local function readRates(data, adEnv, farmId, nameOf)
|
||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
|
||||
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
|
||||
local ok, err = pcall(function()
|
||||
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
|
||||
local name = nameOf(index)
|
||||
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
|
||||
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
|
||||
local function readUsedFruits(data, adEnv, farmId, nameOf)
|
||||
for _, recipe in ipairs(data.recipes) do
|
||||
if recipe.enabled then
|
||||
for name in pairs(recipe.inputs) do
|
||||
if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, true) or {}) do
|
||||
local fruit = data.crops[nameOf(index) or ""]
|
||||
if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
|
||||
end
|
||||
end)
|
||||
if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
|
||||
end
|
||||
|
||||
-- Markscanningen har ikke nået alle marker endnu (efter indlæsning ~én mark pr. 2 s): marklisten er ufuldstændig,
|
||||
-- så udbud undervurderes -> fieldsIncomplete (kæden rører ingen marker) og partial (intet slukkes/stoppes).
|
||||
local function readFields(data)
|
||||
if ADFields.isScanComplete ~= nil and not ADFields.isScanComplete() then
|
||||
data.partial, data.fieldsIncomplete = true, true
|
||||
end
|
||||
for _, info in ipairs(ADFields.list() or {}) do
|
||||
local ok, entry = pcall(function()
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
local plan = field.plan or {}
|
||||
local fruit = info.growing and info.fruitName or field.lastFruit
|
||||
-- høst i gang: ingen forslag/skift for afgrøden, mens læssene er på vej hjem
|
||||
if field.stage == "harvesting" and info.fruitName ~= nil then data.harvesting[info.fruitName] = true end
|
||||
return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
|
||||
targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
|
||||
permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
|
||||
end)
|
||||
if ok then
|
||||
table.insert(data.fields, entry)
|
||||
else
|
||||
failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- afhængighedstjek (spec afsnit 8) ----
|
||||
|
||||
local function unloadStationsOf(placeable)
|
||||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||
local result = {}
|
||||
if storage == nil or storage.getUnloadingStations == nil then return result end
|
||||
for _, station in pairs(storage:getUnloadingStations() or {}) do
|
||||
if station.owningPlaceable == placeable then table.insert(result, station) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
|
||||
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
|
||||
function ADChainData.acceptsMap(data, flows, goods)
|
||||
local result = {}
|
||||
for _, flow in ipairs(flows or {}) do
|
||||
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
|
||||
for _, id in ipairs(flow.targetBuildingIds or {}) do
|
||||
local placeable = data.placeables[id]
|
||||
if result[id] == nil and placeable ~= nil then
|
||||
result[id] = {}
|
||||
local ok, err = pcall(function()
|
||||
local stations = unloadStationsOf(placeable)
|
||||
for _, name in ipairs(goods) do
|
||||
local index = data.fillIndex[name]
|
||||
for _, station in ipairs(stations) do
|
||||
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
|
||||
end
|
||||
end
|
||||
end)
|
||||
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
|
||||
function ADChainData.chainInputs(chain, titles)
|
||||
local result = {}
|
||||
local function walk(nodes, rootTitle)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
result[node.fillType] = result[node.fillType] or {}
|
||||
local list = result[node.fillType]
|
||||
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
|
||||
walk(node.children, rootTitle)
|
||||
end
|
||||
end
|
||||
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
|
||||
walk(root.children, tostring((titles or {})[root.name] or root.name))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
|
||||
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
|
||||
function ADChainData.dependencyInputs(data, chain, flows)
|
||||
local goods, seen = {}, {}
|
||||
for _, recipe in ipairs(data.recipes or {}) do
|
||||
if not recipe.enabled and not recipe.byChain then
|
||||
for name in pairs(recipe.outputs or {}) do
|
||||
if not seen[name] then seen[name] = true; table.insert(goods, name) end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(goods)
|
||||
-- auto-flowets mål tager mange varer (fx biogas: korn, ensilage, gylle): kun varer gården faktisk forbruger
|
||||
-- (stald eller fabrik) eller en kæde vil have, tæller
|
||||
local consumed = {}
|
||||
for _, name in ipairs(goods) do
|
||||
local rate = (data.rates or {})[name]
|
||||
if (rate ~= nil and (rate.consumption or 0) > 0) or ((chain ~= nil and chain.demand or {})[name] or 0) > 0 then
|
||||
table.insert(consumed, name)
|
||||
end
|
||||
end
|
||||
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
|
||||
#consumed > 0 and ADChainData.acceptsMap(data, flows, consumed) or {})
|
||||
local adjustment = data.timeAdjustment or 1
|
||||
local capacity, demand, stockDays = {}, {}, {}
|
||||
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
|
||||
for _, name in ipairs(goods) do
|
||||
local rate = (data.rates or {})[name] or {}
|
||||
local consumption, production = rate.consumption or 0, rate.production or 0
|
||||
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
|
||||
demand[name] = math.max(consumption, chainDemand)
|
||||
if consumption > production then
|
||||
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
|
||||
elseif consumption > 0 then
|
||||
stockDays[name] = math.huge
|
||||
end
|
||||
end
|
||||
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
|
||||
end
|
||||
|
||||
-- ---- samlet ----
|
||||
|
||||
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
|
||||
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
|
||||
-- ikke har nået alle marker.
|
||||
function ADChainData.collect(adEnv, farmId, byChain)
|
||||
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
|
||||
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}, harvesting = {}}
|
||||
local nameOf = nameResolver(data)
|
||||
data.priceOf = priceResolver(data)
|
||||
readPlaceables(data, farmId, byChain, nameOf)
|
||||
readCrops(data, nameOf)
|
||||
readFieldGoods(data, nameOf)
|
||||
readStock(data, adEnv, farmId, nameOf)
|
||||
readVehicleLoads(data, farmId, nameOf)
|
||||
readRates(data, adEnv, farmId, nameOf)
|
||||
readUsedFruits(data, adEnv, farmId, nameOf)
|
||||
readFields(data)
|
||||
return data
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
-- AD Chain Model (ren logik)
|
||||
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
|
||||
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
|
||||
|
||||
ADChainModel = {}
|
||||
ADChainModel.MAX_DEPTH = 8
|
||||
|
||||
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
|
||||
|
||||
-- producenter pr. vare: tændte først, så stabil rækkefølge
|
||||
local function indexProducers(recipes)
|
||||
local index = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
|
||||
index[name] = index[name] or {}
|
||||
table.insert(index[name], recipe)
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, list in pairs(index) do
|
||||
table.sort(list, function(a, b)
|
||||
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
|
||||
return byKey(a, b)
|
||||
end)
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function indexHerds(husbandry)
|
||||
local index = {}
|
||||
for _, herd in ipairs(husbandry or {}) do
|
||||
for name, perMonth in pairs(herd.outputs or {}) do
|
||||
if (perMonth or 0) > 0 then
|
||||
index[name] = index[name] or {}
|
||||
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
|
||||
end
|
||||
end
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
local function sortedNames(map)
|
||||
local names = {}
|
||||
for name in pairs(map or {}) do table.insert(names, name) end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
|
||||
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
|
||||
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
|
||||
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
|
||||
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
|
||||
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
|
||||
-- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion).
|
||||
-- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den.
|
||||
local capacity, roots, standalone = {}, {}, {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
capacity[recipe.key] = recipe.cyclesPerMonth or 0
|
||||
if recipe.enabled and not recipe.byChain then
|
||||
if not recipe.paused and not recipe.notChosen then table.insert(roots, recipe) end
|
||||
standalone[recipe.key] = true
|
||||
end
|
||||
end
|
||||
table.sort(roots, byKey)
|
||||
|
||||
-- detail = manglende liter/md (flaskehals/køb), need = varens behov pr. måned i den gren
|
||||
local function problem(kind, fillType, rootKey, detail, need)
|
||||
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail, need = need})
|
||||
end
|
||||
|
||||
local need
|
||||
local function inputsOf(recipe, cycles, depth, path, rootKey)
|
||||
local children = {}
|
||||
for _, name in ipairs(sortedNames(recipe.inputs)) do
|
||||
local amount = recipe.inputs[name] or 0
|
||||
if amount > 0 and cycles > 0 then
|
||||
local child = need(name, amount * cycles, depth + 1, path, rootKey)
|
||||
child.consumer, child.consumerName = recipe.building, recipe.buildingName
|
||||
table.insert(children, child)
|
||||
end
|
||||
end
|
||||
return children
|
||||
end
|
||||
|
||||
need = function(name, perMonth, depth, path, rootKey)
|
||||
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
|
||||
chain.demand[name] = (chain.demand[name] or 0) + perMonth
|
||||
if depth > ADChainModel.MAX_DEPTH or path[name] then
|
||||
node.source = "cycle"
|
||||
problem("cycle", name, rootKey, nil, perMonth)
|
||||
return node
|
||||
end
|
||||
path[name] = true
|
||||
local rest = perMonth
|
||||
-- a) gårdens fabrikker
|
||||
local makers = producers[name]
|
||||
if makers ~= nil then
|
||||
node.source = "production"
|
||||
for _, recipe in ipairs(makers) do
|
||||
local perCycle = recipe.outputs[name]
|
||||
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
|
||||
if cycles > 0 then
|
||||
capacity[recipe.key] = capacity[recipe.key] - cycles
|
||||
rest = rest - cycles * perCycle
|
||||
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
|
||||
if not standalone[recipe.key] then
|
||||
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
|
||||
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
|
||||
end
|
||||
end
|
||||
if rest <= 1e-9 then break end
|
||||
end
|
||||
if rest > 1e-9 and #node.producers > 0 then
|
||||
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||
node.shortPerMonth = rest
|
||||
end
|
||||
if rest > 1e-9 and #node.producers == 0 then
|
||||
node.source = nil
|
||||
end
|
||||
end
|
||||
-- b) stalde
|
||||
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
|
||||
node.source = "husbandry"
|
||||
for _, herd in ipairs(herds[name]) do
|
||||
local take = math.min(rest, herd.left)
|
||||
if take > 0 then
|
||||
herd.left = herd.left - take
|
||||
rest = rest - take
|
||||
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
|
||||
end
|
||||
end
|
||||
end
|
||||
-- c) mark
|
||||
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
|
||||
node.source, node.fruit = "field", crops[name]
|
||||
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
|
||||
rest = 0
|
||||
end
|
||||
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
|
||||
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
|
||||
node.source, node.fruit = "field", nil
|
||||
rest = 0
|
||||
end
|
||||
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
|
||||
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
|
||||
node.source = "production"
|
||||
node.shortPerMonth = rest
|
||||
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
|
||||
if node.source == nil then node.source = "buy" end
|
||||
node.shortPerMonth = rest
|
||||
problem("buy", name, rootKey, rest, perMonth)
|
||||
end
|
||||
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
|
||||
path[name] = nil
|
||||
return node
|
||||
end
|
||||
|
||||
for _, root in ipairs(roots) do
|
||||
local cycles = root.cyclesPerMonth or 0
|
||||
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
|
||||
children = inputsOf(root, cycles, 0, {}, root.key)})
|
||||
end
|
||||
return chain
|
||||
end
|
||||
@@ -0,0 +1,362 @@
|
||||
-- AD Chain Run (kædens handlinger i spillet)
|
||||
-- Udfører ADChain's plan: skifter frie marker (kun "auto", højst MAX_FIELD_SWITCHES pr. tick), tænder/slukker
|
||||
-- mellemled, opretter manglende forsyningsflows (højst MAX_NEW_FLOWS pr. tick) og stopper flows for rødder der
|
||||
-- er slået fra. Hver del kører i pcall, så en fejl ikke stopper resten. Spilkald kun inde i funktioner.
|
||||
|
||||
ADChainRun = {}
|
||||
|
||||
local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||
|
||||
local UNLOADER = "unloader"
|
||||
|
||||
-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool).
|
||||
local function isUnloader(implement)
|
||||
return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true)
|
||||
end
|
||||
|
||||
-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle
|
||||
-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig.
|
||||
local function belongsToFlow(tractor)
|
||||
return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil
|
||||
end
|
||||
|
||||
-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og
|
||||
-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu).
|
||||
function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked)
|
||||
if fillTypeIndex == nil then return nil end
|
||||
used, blocked = used or {}, blocked or {}
|
||||
local free = {}
|
||||
for _, tractor in ipairs(tractors or {}) do
|
||||
if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then
|
||||
table.insert(free, tractor)
|
||||
end
|
||||
end
|
||||
for _, implement in ipairs(implements or {}) do
|
||||
if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then
|
||||
local tractor = ADPool.bestFit(free, implement, "unloader")
|
||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ---- marker ----
|
||||
|
||||
local function fieldById(fields, id)
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if field.id == id then return field end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADChainRun.switchFields(data, actions)
|
||||
local switched = 0
|
||||
for _, action in ipairs(actions or {}) do
|
||||
if switched >= ADChain.MAX_FIELD_SWITCHES then break end
|
||||
local field = fieldById(data.fields, action.fieldId)
|
||||
-- brugerens egen målafgrøde røres aldrig (dobbelttjek oven i ADCropPlan)
|
||||
if action.mode == "auto" and field ~= nil and field.targetSource ~= "user" then
|
||||
local ok, err = pcall(ADFieldJobs.setTarget, action.fieldId, action.to, "chain")
|
||||
if ok then
|
||||
switched = switched + 1
|
||||
ADChain.switchCount = ADChain.switchCount + 1
|
||||
local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from),
|
||||
tostring(action.to), action.hectares or 0, tostring(action.reason))
|
||||
log("%s", text)
|
||||
pcall(ADFieldJobs.notify, text, false)
|
||||
else
|
||||
warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- mellemled ----
|
||||
|
||||
local function setProduction(data, key, isOn)
|
||||
local entry = data.points[key]
|
||||
if entry == nil or entry.point == nil or entry.point.setProductionState == nil then
|
||||
warn("produktion %s findes ikke (længere)", tostring(key))
|
||||
return false
|
||||
end
|
||||
local ok, err = pcall(entry.point.setProductionState, entry.point, entry.productionId, isOn)
|
||||
if not ok then warn("produktion %s kunne ikke %s: %s", tostring(key), isOn and "tændes" or "slukkes", tostring(err)) end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen.
|
||||
-- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra).
|
||||
function ADChainRun.toggleProductions(data, prod)
|
||||
local state = ADChain.state
|
||||
local byChain = state.byChain
|
||||
for _, key in ipairs(prod.enable or {}) do
|
||||
if not state.userOff[key] and setProduction(data, key, true) then
|
||||
byChain[key] = true
|
||||
log("tænder %s (mellemled i kæden)", key)
|
||||
end
|
||||
end
|
||||
if data.partial then return end
|
||||
for _, key in ipairs(prod.disable or {}) do
|
||||
if setProduction(data, key, false) then
|
||||
byChain[key] = nil
|
||||
log("slukker %s (kæden bruger den ikke længere)", key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- flows ----
|
||||
|
||||
local function collectPool(adEnv)
|
||||
local ok, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
||||
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
||||
end)
|
||||
if not ok then
|
||||
warn("maskinpuljen kunne ikke læses: %s", tostring(implements))
|
||||
return {}, {}
|
||||
end
|
||||
return implements or {}, tractors or {}
|
||||
end
|
||||
|
||||
-- kind = "noVehicle"|"pallet": vises som status på kanten i fanen Kæder
|
||||
local function createStopped(run, kind, reason, ...)
|
||||
run.stopped = true
|
||||
ADChain.noVehicle[run.id] = kind
|
||||
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
|
||||
end
|
||||
|
||||
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
|
||||
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
|
||||
local function flowTitle(data, fillTypes)
|
||||
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
|
||||
if #fillTypes == 1 then return first end
|
||||
return string.format("%s +%d", tostring(first), #fillTypes - 1)
|
||||
end
|
||||
|
||||
local function startWithPair(adEnv, run, runId, pair, used)
|
||||
used[pair.tractor.id], used[pair.implement.id] = true, true
|
||||
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
|
||||
run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local wagons = ADRuns.copyWagons(run.wagons)
|
||||
wagons[pair.tractor.id] = pair.implement.id
|
||||
run.wagons = wagons
|
||||
ADRunsController.resolveMarkers(adEnv, run)
|
||||
local results = ADRunsController.startRun(adEnv, runId)
|
||||
local started = 0
|
||||
for _, result in ipairs(results or {}) do if result.ok then started = started + 1 end end
|
||||
log("flow '%s' oprettet med '%s' + vogn '%s' (%d startet)", tostring(run.name), tostring(pair.tractor.name),
|
||||
tostring(pair.implement.name), started)
|
||||
end
|
||||
|
||||
local function createGroupFlow(adEnv, data, group, fillTypes, pool, used)
|
||||
local title = flowTitle(data, fillTypes)
|
||||
local created = ADRunsController.createRun(ADChain.flowName(title, group.consumerName or group.consumer), "supply")
|
||||
local runId = created.id
|
||||
ADRunsController.updateRun(runId, {targetBuildingIds = {group.consumer}, ingredientMode = "manual", ingredients = fillTypes,
|
||||
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, group.consumer), chain = group.rootKey})
|
||||
-- updateRun erstatter store'en: slå altid run'et op igen
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
local fillIndex = data.fillIndex[fillTypes[1]]
|
||||
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
|
||||
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
|
||||
return
|
||||
end
|
||||
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
|
||||
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
|
||||
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
|
||||
if pair == nil then
|
||||
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
|
||||
return
|
||||
end
|
||||
startWithPair(adEnv, run, runId, pair, used)
|
||||
end
|
||||
|
||||
-- Nye varer til en bygning, der allerede har et kædeflow: lægges i det flow.
|
||||
local function extendFlow(runId, fillTypes)
|
||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
if run == nil then return false end
|
||||
local ingredients = {}
|
||||
for _, name in ipairs(run.ingredients or {}) do table.insert(ingredients, name) end
|
||||
local added = {}
|
||||
for _, name in ipairs(fillTypes) do
|
||||
local present = false
|
||||
for _, have in ipairs(ingredients) do if have == name then present = true end end
|
||||
if not present then table.insert(ingredients, name); table.insert(added, name) end
|
||||
end
|
||||
if #added == 0 then return false end
|
||||
ADRunsController.updateRun(runId, {ingredients = ingredients})
|
||||
log("flow '%s' får også %s", tostring(run.name), table.concat(added, ", "))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tidligere kædeflows (ét pr. vare) til samme bygning uden traktorer lægges sammen i ét.
|
||||
function ADChainRun.mergeIdleFlows(adEnv)
|
||||
local merged = 0
|
||||
for _, merge in ipairs(ADChainActions.mergeIdle(ADChain.runsView(ADRunsController.store.runs))) do
|
||||
if #merge.add > 0 then extendFlow(merge.keepId, merge.add) end
|
||||
for _, id in ipairs(merge.deleteIds) do
|
||||
local ok, err = pcall(ADRunsController.deleteRun, id, adEnv)
|
||||
if ok then
|
||||
ADChain.noVehicle[id] = nil
|
||||
merged = merged + 1
|
||||
else
|
||||
warn("kædeflow %s kunne ikke lægges sammen: %s", tostring(id), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
if merged > 0 then log("%d tomme kædeflows lagt sammen (ét flow pr. bygning)", merged) end
|
||||
return merged
|
||||
end
|
||||
|
||||
function ADChainRun.createFlows(adEnv, data, edges)
|
||||
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||
local missing = ADChainActions.missingFlows(edges, runs)
|
||||
local pool, used, count = {}, {}, 0
|
||||
for _, group in ipairs(ADChainActions.groupMissing(missing)) do
|
||||
if count >= ADChain.MAX_NEW_FLOWS then break end
|
||||
local bulk, pallets = {}, {}
|
||||
for _, name in ipairs(group.fillTypes) do
|
||||
local index = data.fillIndex[name]
|
||||
table.insert((index ~= nil and ADSources.isPalletFillType(index)) and pallets or bulk, name)
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
if #bulk > 0 then
|
||||
local existing = ADChainActions.chainRunFor(runs, group.consumer)
|
||||
if existing ~= nil then
|
||||
if extendFlow(existing, bulk) then count = count + 1 end
|
||||
else
|
||||
createGroupFlow(adEnv, data, group, bulk, pool, used)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(pallets) do
|
||||
createGroupFlow(adEnv, data, group, {name}, pool, used)
|
||||
count = count + 1
|
||||
end
|
||||
end)
|
||||
if not ok then warn("flow til %s kunne ikke oprettes: %s", tostring(group.consumerName), tostring(err)) end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Slutprodukter: sælg straks (fabrikkens udgang -> "Sælg direkte"), medmindre kæden står på "Behold". Varer kæden
|
||||
-- selv satte til salg, sættes tilbage til "Behold", når spilleren vælger det.
|
||||
local function outputMode(point, fillIndex)
|
||||
if point.getOutputDistributionMode == nil then return nil end
|
||||
local ok, mode = pcall(point.getOutputDistributionMode, point, fillIndex)
|
||||
return ok and mode or nil
|
||||
end
|
||||
|
||||
function ADChainRun.applyFinalSales(data, chain, edges)
|
||||
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||
local state, changed = ADChain.state, 0
|
||||
local wanted = {}
|
||||
local uses = ADChainActions.farmUses(data, ADChain.runsView(ADRunsController.store.runs))
|
||||
for _, sale in ipairs(ADChainActions.finalSales(chain, data.recipes, edges, state.finalKeep, uses)) do
|
||||
local id = sale.key .. "|" .. sale.fillType
|
||||
wanted[id] = true
|
||||
local entry, fillIndex = (data.points or {})[sale.key], data.fillIndex[sale.fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||
and outputMode(entry.point, fillIndex) ~= ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.DIRECT_SELL)
|
||||
state.soldByChain[id] = true
|
||||
changed = changed + 1
|
||||
log("slutprodukt %s i %s sælges straks", tostring(sale.fillType), tostring(entry.buildingName or sale.key))
|
||||
end
|
||||
end
|
||||
for id in pairs(state.soldByChain) do
|
||||
local key, fillType = id:match("^(.*)|([^|]+)$")
|
||||
-- sat til salg af kæden, men ikke længere ønsket (Behold, gården bruger varen, kæden væk): tilbage til Behold
|
||||
if key ~= nil and not wanted[id] then
|
||||
local entry, fillIndex = (data.points or {})[key], data.fillIndex[fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil
|
||||
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||
changed = changed + 1
|
||||
log("slutprodukt %s beholdes igen", tostring(fillType))
|
||||
end
|
||||
state.soldByChain[id] = nil
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
|
||||
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
|
||||
function ADChainRun.keepIntermediates(data, edges)
|
||||
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||
local changed = 0
|
||||
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
|
||||
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
|
||||
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
|
||||
changed = changed + 1
|
||||
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
|
||||
tostring(entry.buildingName or item.key))
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
|
||||
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
|
||||
function ADChainRun.stopFlows(adEnv, chain, edges)
|
||||
local changed = 0
|
||||
local active = ADChain.activeRoots(chain, ADChain.state.paused)
|
||||
for _, run in ipairs(ADChainActions.flowsToStop(ADChain.runsView(ADRunsController.store.runs), active)) do
|
||||
local rootKey = ADChain.stillNeeded(run, edges)
|
||||
if rootKey ~= nil then
|
||||
ADRunsController.updateRun(run.id, {chain = rootKey})
|
||||
log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey))
|
||||
changed = changed + 1
|
||||
elseif ADChainActions.releaseAction(run) == "delete" then
|
||||
local ok, err = pcall(ADRunsController.deleteRun, run.id, adEnv)
|
||||
if ok then
|
||||
ADChain.noVehicle[run.id] = nil
|
||||
log("tomt kædeflow '%s' slettet: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||
changed = changed + 1
|
||||
else
|
||||
warn("flow '%s' kunne ikke slettes: %s", tostring(run.name), tostring(err))
|
||||
end
|
||||
else
|
||||
local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id)
|
||||
if ok then
|
||||
ADRunsController.updateRun(run.id, {chain = ""})
|
||||
log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||
changed = changed + 1
|
||||
else
|
||||
warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Hele planen; hver del for sig i pcall.
|
||||
function ADChainRun.apply(adEnv, data, result)
|
||||
local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions)
|
||||
if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end
|
||||
local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod)
|
||||
if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end
|
||||
local okMerge, mergedCount = pcall(ADChainRun.mergeIdleFlows, adEnv)
|
||||
if not okMerge then warn("sammenlægning af kædeflows fejlede: %s", tostring(mergedCount)) end
|
||||
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
|
||||
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
|
||||
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
|
||||
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
|
||||
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
|
||||
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
|
||||
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
|
||||
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
|
||||
local okStop, stopped = true, 0
|
||||
if not data.partial then
|
||||
okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges)
|
||||
if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end
|
||||
end
|
||||
if (okNew and created > 0) or (okStop and stopped > 0) then
|
||||
local okSave, errSave = pcall(ADRunsController.save)
|
||||
if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,453 @@
|
||||
-- AD Chain View (ren logik)
|
||||
-- Rækkerne til fanen Kæder ud fra ADChain.last. Hver rod er foldet sammen som standard (én linje med samlet
|
||||
-- status + antal forslag); foldet ud vises varerne rekursivt (kilde, behov pr. måned, status, lager). Forslag
|
||||
-- vises altid (under roden eller under afgrøden). Pausede rødder (som modellen ikke længere ser som rødder)
|
||||
-- får en grå linje med [Genoptag]. Nederst frie marker + antal automatiske skift (+ "marker scannes endnu"). Ingen spiladgang:
|
||||
-- titleOf(navn) -> titel, fmt(liter) -> kompakt tal, textOf(l10n-nøgle) -> tekst gives udefra.
|
||||
-- Manglende led (Task 10): foldet ud står problemteksten (problemText) som en rød tekstrække ("problem", ingen celler)
|
||||
-- lige under varen — teksten er for lang til en celle og ombrydes af fanen.
|
||||
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty"|"problem", indent, level, cells={c1..c5}, text,
|
||||
-- actions={[celle]={kind="approve"|"dismiss"|"pause"|"resume", fieldId|rootKey}}, toggle=rootKey, fillType}
|
||||
|
||||
ADChainView = {}
|
||||
ADChainView.INDENT = " "
|
||||
-- Tegnbudget pr. celle i flowListPrefab (gui/SmartPickupFrame.xml: textMaxWidth 192/162/62/244/92 px).
|
||||
-- Antaget ~11,5 px pr. tegn (spuInfoText: 800 px ~ 70 tegn), fed ~12,5 px; c3 har kun tal (~10 px pr. tegn).
|
||||
ADChainView.CELL_CHARS = {22, 20, 7, 28, 10} -- ca. tegn pr. celle (c1 192 px fed, c2 162, c3 62, c4 244, c5 92 fed)
|
||||
ADChainView.MAX_SOURCE_NAMES = 2
|
||||
-- Tegnbudget for en tekstlinje (addTextRow/spuInfoText: 800 px ~ 70 tegn), fx linjen med frie marker.
|
||||
ADChainView.TEXT_ROW_CHARS = 70
|
||||
-- "stramt": lager for en mark-afgrøde under så mange måneders behov -> gul
|
||||
ADChainView.TIGHT_MONTHS = 3
|
||||
-- nøgler rækkerne bruger (til l10n-testen)
|
||||
ADChainView.TEXT_KEYS = {"spu_chain_optIn", "spu_chain_optOut", "spu_chain_notChosen", "spu_chain_candidatesShow", "spu_chain_candidatesHide", "spu_chain_finalFarm", "spu_chain_finalSell", "spu_chain_finalKeep", "spu_chain_suggestHeader", "spu_chain_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
|
||||
"spu_chain_flowOk", "spu_chain_flowMissing", "spu_chain_flowPallet", "spu_chain_flowNone", "spu_chain_flowStopped", "spu_chain_bottleneck",
|
||||
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource", "spu_chain_fromFields",
|
||||
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
|
||||
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
|
||||
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
|
||||
"spu_chain_flowAuto", "spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
|
||||
|
||||
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
|
||||
pallet = "spu_chain_flowPallet", auto = "spu_chain_flowAuto"}
|
||||
local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError", waiting = "spu_chain_rootWaiting", stopped = "spu_chain_rootPaused"}
|
||||
|
||||
local function identity(value) return tostring(value) end
|
||||
|
||||
-- samme som SmartPickupFrame.formatCompact (standard-fmt når ingen gives, fx i beskeden fra kædens tick)
|
||||
function ADChainView.compact(liters)
|
||||
liters = math.floor((liters or 0) + 0.5)
|
||||
if liters >= 1000000 then return string.format("%.1fM", liters / 1000000)
|
||||
elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end
|
||||
return string.format("%d", liters)
|
||||
end
|
||||
|
||||
-- ---- UTF-8-sikker længde/afkortning (æøå og → er flere bytes) ----
|
||||
|
||||
function ADChainView.textLength(text)
|
||||
local count = 0
|
||||
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
local function utf8Prefix(text, chars)
|
||||
local result, count = {}, 0
|
||||
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||
if count >= chars then break end
|
||||
table.insert(result, char)
|
||||
count = count + 1
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
-- Afkort til celle-budgettet med ".." til sidst.
|
||||
function ADChainView.fit(text, chars)
|
||||
text = tostring(text or "")
|
||||
if ADChainView.textLength(text) <= chars then return text end
|
||||
return utf8Prefix(text, math.max(0, chars - 2)) .. ".."
|
||||
end
|
||||
|
||||
local function fitCells(cells)
|
||||
local result = {}
|
||||
for index = 1, 5 do result[index] = ADChainView.fit(cells[index] or "", ADChainView.CELL_CHARS[index]) end
|
||||
return result
|
||||
end
|
||||
|
||||
local function haText(ha)
|
||||
return string.format("%.0f", ha or 0)
|
||||
end
|
||||
|
||||
local function plannedFruit(field)
|
||||
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||
return field.fruit
|
||||
end
|
||||
|
||||
local function edgeKey(node)
|
||||
return tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||
end
|
||||
|
||||
local function shortageByFruit(last)
|
||||
local result = {}
|
||||
for _, shortage in ipairs(last.shortages or {}) do result[shortage.fruit] = shortage end
|
||||
return result
|
||||
end
|
||||
|
||||
local function stockOf(last, fruit)
|
||||
local stock = last.data ~= nil and last.data.stock or nil
|
||||
return stock ~= nil and fruit ~= nil and stock[fruit] or nil
|
||||
end
|
||||
|
||||
-- Lager for en mark-node: afgrødens lager, eller varens eget lager for en markvare uden afgrøde (hø, hakkelse …).
|
||||
local function fieldStockOf(last, node)
|
||||
if node.fruit ~= nil then return stockOf(last, node.fruit) end
|
||||
local byFill = last.data ~= nil and last.data.stockByFill or nil
|
||||
return byFill ~= nil and byFill[node.fillType] or nil
|
||||
end
|
||||
|
||||
-- Mark-afgrøde uden mangel men med lager under TIGHT_MONTHS måneders behov.
|
||||
local function isTight(node, last, short)
|
||||
if node.source ~= "field" or node.fruit == nil or short[node.fruit] ~= nil then return false end
|
||||
return (stockOf(last, node.fruit) or 0) < ADChainView.TIGHT_MONTHS * (node.perMonth or 0)
|
||||
end
|
||||
|
||||
local function eachNode(nodes, fn, depth)
|
||||
for _, node in ipairs(nodes or {}) do
|
||||
fn(node, depth or 1)
|
||||
eachNode(node.children, fn, (depth or 1) + 1)
|
||||
end
|
||||
end
|
||||
|
||||
function ADChainView.rootLevel(root, last)
|
||||
last = last or {}
|
||||
if (last.paused or {})[root.key] then return "stopped" end
|
||||
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||
if problem.rootKey == root.key then return "error" end
|
||||
end
|
||||
local short, flowStatus = shortageByFruit(last), last.flowStatus or {}
|
||||
local hasShortage, hasWaiting = false, false
|
||||
eachNode(root.children, function(node)
|
||||
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
|
||||
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
|
||||
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
|
||||
if (status ~= nil and status ~= "ok" and status ~= "auto") or isTight(node, last, short) then hasWaiting = true end
|
||||
end)
|
||||
if hasShortage then return "error" end
|
||||
if hasWaiting then return "waiting" end
|
||||
return "running"
|
||||
end
|
||||
|
||||
-- Højst MAX_SOURCE_NAMES navne + " +k".
|
||||
local function joinCapped(names)
|
||||
local shown = {}
|
||||
for index = 1, math.min(#names, ADChainView.MAX_SOURCE_NAMES) do shown[index] = names[index] end
|
||||
local text = table.concat(shown, ", ")
|
||||
if #names > #shown then text = text .. " +" .. tostring(#names - #shown) end
|
||||
return text
|
||||
end
|
||||
|
||||
-- Opskrifter (alle, også slukkede) der laver varen, i datarækkefølge.
|
||||
local function producersOf(last, fillType)
|
||||
local list = {}
|
||||
for _, recipe in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||
if ((recipe.outputs or {})[fillType] or 0) > 0 then table.insert(list, recipe) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Årsag i klart sprog med råd for et problem fra kædemodellen ({kind="buy"|"bottleneck"|"cycle", fillType, rootKey,
|
||||
-- detail = manglende l/md, need = behov l/md}). Køb af en rest efter gårdens stalde -> stald-tekst. En producent spilleren selv har slukket (last.userOff, stadig
|
||||
-- slukket) slår igennem som "slukket af dig" — det er den reelle årsag, uanset om modellen så køb eller flaskehals.
|
||||
function ADChainView.problemText(problem, last, textOf, titleOf, fmt)
|
||||
last, textOf, fmt = last or {}, textOf or identity, fmt or ADChainView.compact
|
||||
titleOf = titleOf or function(name) return (last.titles or {})[name] or tostring(name) end
|
||||
local item = titleOf(problem.fillType)
|
||||
if problem.kind == "cycle" then return string.format(textOf("spu_chain_problem_cycle"), item) end
|
||||
local userOff, names, seen = last.userOff or {}, {}, {}
|
||||
for _, recipe in ipairs(producersOf(last, problem.fillType)) do
|
||||
if userOff[recipe.key] and not recipe.enabled then
|
||||
return string.format(textOf("spu_chain_problem_off"), tostring(recipe.buildingName), titleOf(recipe.name))
|
||||
end
|
||||
local name = tostring(recipe.buildingName)
|
||||
if (recipe.cyclesPerMonth or 0) > 0 and not seen[name] then
|
||||
seen[name] = true
|
||||
table.insert(names, name)
|
||||
end
|
||||
end
|
||||
if problem.kind == "bottleneck" and #names > 0 then
|
||||
local need = problem.need or problem.detail or 0
|
||||
local made = math.max(0, need - (problem.detail or 0))
|
||||
return string.format(textOf("spu_chain_problem_bottleneck"), joinCapped(names), fmt(math.floor(made + 0.5)), fmt(math.floor(need + 0.5)))
|
||||
end
|
||||
-- modellen melder også "køb" for resten, når gårdens stalde leverer for lidt: så er rådet ikke "byg en fabrik"
|
||||
for _, herd in ipairs(last.data ~= nil and last.data.husbandry or {}) do
|
||||
if ((herd.outputs or {})[problem.fillType] or 0) > 0 then
|
||||
return string.format(textOf("spu_chain_problem_barn"), item)
|
||||
end
|
||||
end
|
||||
return string.format(textOf("spu_chain_problem_buy"), item)
|
||||
end
|
||||
|
||||
local function sourceText(node, last, textOf)
|
||||
local names = {}
|
||||
if node.source == "production" then
|
||||
for _, producer in ipairs(node.producers or {}) do table.insert(names, tostring(producer.buildingName)) end
|
||||
elseif node.source == "husbandry" then
|
||||
for _, herd in ipairs(node.herds or {}) do table.insert(names, tostring(herd.buildingName)) end
|
||||
elseif node.source == "field" then
|
||||
-- markvare uden egen afgrøde (hø, hakkelse …): ingen afgrødeplan, bare "fra marker"
|
||||
if node.fruit == nil then return textOf("spu_chain_fromFields") end
|
||||
for _, field in ipairs(last.fields or {}) do
|
||||
if plannedFruit(field) == node.fruit then table.insert(names, "F" .. tostring(field.id)) end
|
||||
end
|
||||
if #names == 0 then return textOf("spu_chain_noField") end
|
||||
elseif node.source == "buy" then
|
||||
return textOf("spu_chain_buySource")
|
||||
else
|
||||
return "-"
|
||||
end
|
||||
return joinCapped(names)
|
||||
end
|
||||
|
||||
-- Nodens status (tekst, niveau): kredsløb/flaskehals/køb/mangel (rød) > flow ikke ok (gul) > stramt lager (gul) > flow ok.
|
||||
local function nodeStatus(node, last, fmt, textOf, short)
|
||||
if node.source == "cycle" then return textOf("spu_chain_cycle"), "error" end
|
||||
if (node.shortPerMonth or 0) > 0 then
|
||||
local key = node.source == "production" and "spu_chain_bottleneck" or "spu_chain_buy"
|
||||
return string.format(textOf(key), fmt(node.shortPerMonth)), "error"
|
||||
end
|
||||
local shortage = node.fruit ~= nil and short[node.fruit] or nil
|
||||
if shortage ~= nil then return string.format(textOf("spu_chain_fieldShort"), haText(shortage.missingHa)), "error" end
|
||||
local status = node.consumer ~= nil and (last.flowStatus or {})[edgeKey(node)] or nil
|
||||
-- udgangen står på Fordel: spillet leverer selv
|
||||
if status == "auto" then return textOf("spu_chain_flowAuto"), "running" end
|
||||
if status ~= nil and status ~= "ok" then return textOf(FLOW_KEYS[status] or "spu_chain_flowNone"), "waiting" end
|
||||
if isTight(node, last, short) then return textOf("spu_chain_tight"), "waiting" end
|
||||
if status == "ok" then return textOf("spu_chain_flowOk"), "running" end
|
||||
return "", "running"
|
||||
end
|
||||
|
||||
local function joinText(cells)
|
||||
local parts = {}
|
||||
for _, cell in ipairs(cells) do
|
||||
if cell ~= nil and cell ~= "" then table.insert(parts, cell) end
|
||||
end
|
||||
return table.concat(parts, " · ")
|
||||
end
|
||||
|
||||
local function makeRow(kind, indent, level, cells, actions, toggle)
|
||||
local fitted = fitCells(cells)
|
||||
return {kind = kind, indent = indent, level = level, cells = fitted, actions = actions or {}, toggle = toggle, text = joinText(fitted)}
|
||||
end
|
||||
|
||||
local function suggestRow(action, indent, titleOf, textOf)
|
||||
local pad = string.rep(ADChainView.INDENT, indent)
|
||||
local from = (action.from ~= nil and action.from ~= "") and titleOf(action.from) or "-"
|
||||
local cells = {pad .. "F" .. tostring(action.fieldId) .. " " .. from, "→ " .. titleOf(action.to), haText(action.hectares) .. " ha"}
|
||||
if action.mode == "suggest" then
|
||||
cells[4], cells[5] = textOf("spu_chain_approve"), textOf("spu_chain_dismiss")
|
||||
return makeRow("suggest", indent, "waiting", cells,
|
||||
{[4] = {kind = "approve", fieldId = action.fieldId}, [5] = {kind = "dismiss", fieldId = action.fieldId}})
|
||||
end
|
||||
cells[4] = textOf("spu_chain_auto")
|
||||
return makeRow("auto", indent, "running", cells)
|
||||
end
|
||||
|
||||
-- Handlinger for de afgrøder (i rækkefølge) der hentes fra mark under roden.
|
||||
local function rootFruits(root)
|
||||
local fruits, seen = {}, {}
|
||||
eachNode(root.children, function(node)
|
||||
if node.source == "field" and node.fruit ~= nil and not seen[node.fruit] then
|
||||
seen[node.fruit] = true
|
||||
table.insert(fruits, node.fruit)
|
||||
end
|
||||
end)
|
||||
return fruits
|
||||
end
|
||||
|
||||
local function actionsFor(last, fruit, modes)
|
||||
local list = {}
|
||||
for _, action in ipairs(last.actions or {}) do
|
||||
if action.to == fruit and modes[action.mode] then table.insert(list, action) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Alle forslag (mode "suggest") én gang pr. mark, efter marknummer (in-game 30/9: samme forslag stod under
|
||||
-- hver kæde der bruger afgrøden, spredt mellem kæderne).
|
||||
function ADChainView.suggestionList(last)
|
||||
local byField, list = {}, {}
|
||||
for _, action in ipairs((last or {}).actions or {}) do
|
||||
if action.mode == "suggest" and action.fieldId ~= nil and not byField[action.fieldId] then
|
||||
byField[action.fieldId] = true
|
||||
table.insert(list, action)
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return (tonumber(a.fieldId) or 0) < (tonumber(b.fieldId) or 0) end)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Kæder efter status (fejl/mangel, venter, dækket, pause), så bygning, så produkt.
|
||||
local LEVEL_RANK = {error = 1, waiting = 2, running = 3, stopped = 4}
|
||||
function ADChainView.sortRoots(roots, last)
|
||||
local keyed = {}
|
||||
for index, root in ipairs(roots or {}) do
|
||||
table.insert(keyed, {root = root, rank = LEVEL_RANK[ADChainView.rootLevel(root, last)] or 5,
|
||||
building = tostring(root.buildingName or ""), name = tostring(root.name or ""), index = index})
|
||||
end
|
||||
table.sort(keyed, function(a, b)
|
||||
if a.rank ~= b.rank then return a.rank < b.rank end
|
||||
if a.building ~= b.building then return a.building < b.building end
|
||||
if a.name ~= b.name then return a.name < b.name end
|
||||
return a.index < b.index
|
||||
end)
|
||||
local sorted = {}
|
||||
for _, item in ipairs(keyed) do table.insert(sorted, item.root) end
|
||||
return sorted
|
||||
end
|
||||
|
||||
local function rootRows(rows, root, last, ctx, isExpanded)
|
||||
local level = ADChainView.rootLevel(root, last)
|
||||
local paused = (last.paused or {})[root.key] == true
|
||||
local suggestions = {}
|
||||
for _, fruit in ipairs(rootFruits(root)) do
|
||||
for _, action in ipairs(actionsFor(last, fruit, {suggest = true})) do table.insert(suggestions, action) end
|
||||
end
|
||||
local status = ctx.textOf(ROOT_STATUS[level])
|
||||
if #suggestions > 0 then status = string.format(ctx.textOf("spu_chain_rootSummary"), status, #suggestions) end
|
||||
local marker = (#(root.children or {}) > 0) and (isExpanded and "– " or "+ ") or ""
|
||||
-- slutproduktet: "Sælg straks" (standard) eller "Behold" — klik på feltet skifter
|
||||
-- varer gården selv bruger (last.sellable uden roden) sælges aldrig: "Gård" uden klik
|
||||
local keep = (last.finalKeep or {})[root.key] == true
|
||||
local sellable = last.sellable == nil or last.sellable[root.key] == true
|
||||
local finalText = not sellable and "spu_chain_finalFarm" or (keep and "spu_chain_finalKeep" or "spu_chain_finalSell")
|
||||
local actions = {[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}
|
||||
if sellable then actions[3] = {kind = "finalMode", rootKey = root.key, keep = not keep} end
|
||||
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""),
|
||||
ctx.textOf(finalText), status, ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")}, actions, root.key))
|
||||
if not isExpanded then return end
|
||||
local shown, problems, told = {}, {}, {}
|
||||
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||
if problem.rootKey == root.key then table.insert(problems, problem) end
|
||||
end
|
||||
local function problemRows(fillType, indent)
|
||||
for _, problem in ipairs(problems) do
|
||||
local key = tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||
if (fillType == nil or problem.fillType == fillType) and not told[key] then
|
||||
told[key] = true
|
||||
table.insert(rows, {kind = "problem", indent = indent, level = "error",
|
||||
text = string.rep(ADChainView.INDENT, indent) .. ADChainView.problemText(problem, last, ctx.textOf, ctx.titleOf, ctx.fmt)})
|
||||
end
|
||||
end
|
||||
end
|
||||
eachNode(root.children, function(node, depth)
|
||||
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
|
||||
local stock = node.source == "field" and fieldStockOf(last, node) or nil
|
||||
local row = makeRow("node", depth, nodeLevel, {string.rep(ADChainView.INDENT, depth) .. ctx.titleOf(node.fillType),
|
||||
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
|
||||
row.fillType = node.fillType
|
||||
-- klik åbner flowet der kører varen til modtageren (in-game 30/9: "flow stopped" kunne ikke redigeres)
|
||||
if node.consumer ~= nil then row.openConsumer, row.openFillType = node.consumer, node.fillType end
|
||||
table.insert(rows, row)
|
||||
problemRows(node.fillType, depth + 1)
|
||||
if node.source == "field" and node.fruit ~= nil and not shown[node.fruit] then
|
||||
shown[node.fruit] = true
|
||||
for _, action in ipairs(actionsFor(last, node.fruit, {auto = true})) do
|
||||
table.insert(rows, suggestRow(action, depth + 1, ctx.titleOf, ctx.textOf))
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- problemer uden en vist vare (bør ikke ske) står sidst under roden
|
||||
problemRows(nil, 1)
|
||||
end
|
||||
|
||||
-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag].
|
||||
local function pausedRows(rows, last, ctx)
|
||||
local present, keys = {}, {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do present[root.key] = true end
|
||||
for key in pairs(last.paused or {}) do
|
||||
if not present[key] then table.insert(keys, key) end
|
||||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
for _, key in ipairs(keys) do
|
||||
local recipe
|
||||
for _, candidate in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||
if candidate.key == key then recipe = candidate end
|
||||
end
|
||||
local name = recipe ~= nil and recipe.name ~= nil and ctx.titleOf(recipe.name) or tostring(key)
|
||||
-- pauset kæde: [Start] genoptager, [Fjern] tager den ud af kæderne
|
||||
table.insert(rows, makeRow("root", 0, "stopped", {name, recipe ~= nil and tostring(recipe.buildingName or "") or "", "",
|
||||
ctx.textOf("spu_chain_optOut"), ctx.textOf("spu_chain_resume")},
|
||||
{[4] = {kind = "optOut", rootKey = key}, [5] = {kind = "resume", rootKey = key}}))
|
||||
end
|
||||
return #keys
|
||||
end
|
||||
|
||||
-- Tændte produktioner uden kæde: foldet sammen under én linje; udfoldet med [Kæde til] pr. produktion.
|
||||
local function candidateRows(rows, last, ctx, isExpanded)
|
||||
local list = last.candidates or {}
|
||||
if #list == 0 then return end
|
||||
local key = isExpanded and "spu_chain_candidatesHide" or "spu_chain_candidatesShow"
|
||||
table.insert(rows, makeRow("root", 0, nil, {(isExpanded and "– " or "+ ") .. string.format(ctx.textOf(key), #list), "", "", "", ""}, {}, "__candidates"))
|
||||
if not isExpanded then return end
|
||||
for _, candidate in ipairs(list) do
|
||||
table.insert(rows, makeRow("root", 1, nil, {ADChainView.INDENT .. ctx.titleOf(candidate.name), tostring(candidate.buildingName or ""), "",
|
||||
ctx.textOf("spu_chain_notChosen"), ctx.textOf("spu_chain_optIn")}, {[5] = {kind = "optIn", rootKey = candidate.key}}))
|
||||
end
|
||||
end
|
||||
|
||||
-- "Marker fri til kæden: F1, F2 +k" — så mange navne som tekstlinjen kan rumme, resten talt.
|
||||
function ADChainView.freeText(names, textOf)
|
||||
local format = textOf("spu_chain_free")
|
||||
if #names == 0 then return string.format(format, "-") end
|
||||
for count = #names, 1, -1 do
|
||||
local shown = {}
|
||||
for index = 1, count do shown[index] = names[index] end
|
||||
local list = table.concat(shown, ", ")
|
||||
if count < #names then list = list .. " +" .. tostring(#names - count) end
|
||||
local text = string.format(format, list)
|
||||
if ADChainView.textLength(text) <= ADChainView.TEXT_ROW_CHARS then return text end
|
||||
end
|
||||
return ADChainView.fit(string.format(format, "+" .. tostring(#names)), ADChainView.TEXT_ROW_CHARS)
|
||||
end
|
||||
|
||||
local function footerRows(rows, last, textOf)
|
||||
local free = {}
|
||||
for _, field in ipairs(last.fields or {}) do
|
||||
if field.chainFree then table.insert(free, field.id) end
|
||||
end
|
||||
table.sort(free)
|
||||
local names = {}
|
||||
for _, id in ipairs(free) do table.insert(names, "F" .. tostring(id)) end
|
||||
table.insert(rows, {kind = "info", indent = 0, text = ADChainView.freeText(names, textOf)})
|
||||
table.insert(rows, {kind = "info", indent = 0, text = string.format(textOf("spu_chain_switches"), last.switchCount or 0)})
|
||||
if last.fieldsIncomplete then table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_scanning")}) end
|
||||
end
|
||||
|
||||
-- expanded = {[rodnøgle] = true} for udfoldede rødder (alle andre er foldet sammen).
|
||||
function ADChainView.rows(last, titleOf, fmt, textOf, expanded)
|
||||
textOf, fmt, expanded = textOf or identity, fmt or identity, expanded or {}
|
||||
local titles = last ~= nil and last.titles or {}
|
||||
titleOf = titleOf or function(name) return (titles or {})[name] or tostring(name) end
|
||||
local rows = {}
|
||||
local roots = last ~= nil and last.chain ~= nil and last.chain.roots or {}
|
||||
local hasPaused = last ~= nil and next(last.paused or {}) ~= nil
|
||||
if #roots == 0 and not hasPaused then
|
||||
table.insert(rows, {kind = "empty", indent = 0, text = textOf("spu_chain_empty")})
|
||||
end
|
||||
if last == nil then return rows end
|
||||
if #roots > 0 or hasPaused then
|
||||
table.insert(rows, makeRow("header", 0, nil, {textOf("spu_chain_col_item"), textOf("spu_chain_col_source"),
|
||||
textOf("spu_chain_col_perMonth"), textOf("spu_chain_col_status"), textOf("spu_chain_col_stock")}))
|
||||
end
|
||||
local ctx = {titleOf = titleOf, fmt = fmt, textOf = textOf, short = shortageByFruit(last)}
|
||||
local suggestions = ADChainView.suggestionList(last)
|
||||
if #suggestions > 0 then
|
||||
table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_suggestHeader")})
|
||||
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 0, titleOf, textOf)) end
|
||||
end
|
||||
for _, root in ipairs(ADChainView.sortRoots(roots, last)) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
|
||||
pausedRows(rows, last, ctx)
|
||||
candidateRows(rows, last, ctx, expanded["__candidates"] == true)
|
||||
footerRows(rows, last, textOf)
|
||||
return rows
|
||||
end
|
||||
@@ -169,12 +169,43 @@ function ADCpBridge.getSpecs(vehicle)
|
||||
return specs
|
||||
end
|
||||
|
||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
|
||||
-- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter
|
||||
-- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere).
|
||||
function ADCpBridge.vineFruitSet(spec)
|
||||
local fruits = {}
|
||||
if spec == nil then return fruits end
|
||||
for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do
|
||||
if type(spec[key]) == "number" then fruits[spec[key]] = true end
|
||||
end
|
||||
if type(spec.fruitTypes) == "table" then
|
||||
for key, value in pairs(spec.fruitTypes) do
|
||||
if type(value) == "number" then fruits[value] = true
|
||||
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
|
||||
local vineLogged = setmetatable({}, {__mode = "k"})
|
||||
|
||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere i kæden). nil = ikke en høster.
|
||||
function ADCpBridge.getHarvestFruits(vehicle)
|
||||
if vehicle.spec_combine == nil then return nil end
|
||||
local fruits = {}
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
||||
if object.spec_vineCutter ~= nil then
|
||||
local vine = ADCpBridge.vineFruitSet(object.spec_vineCutter)
|
||||
for fruit in pairs(vine) do fruits[fruit] = true end
|
||||
if next(vine) == nil and not vineLogged[object] then
|
||||
-- ukendt feltnavn i denne spilversion: vis nøglerne, så reglen kan rettes
|
||||
vineLogged[object] = true
|
||||
local keys = {}
|
||||
for key, value in pairs(object.spec_vineCutter) do table.insert(keys, tostring(key) .. "=" .. type(value)) end
|
||||
table.sort(keys)
|
||||
Logging.info("ADSmartPickup: vinhøster uden afgrøde fundet — spec_vineCutter: %s", table.concat(keys, ", "))
|
||||
end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
@@ -360,9 +391,21 @@ local function courseplayWidth(vehicle)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Vinhøster uden AI-markører: bredden er vinrækken (3 m = Courseplays vin-gitter for druer).
|
||||
ADCpBridge.VINE_ROW_WIDTH = 3
|
||||
function ADCpBridge.widthOrVine(width, hasVineCutter)
|
||||
-- under markarbejdets minimum (1,5 m) — også en lille bredde fra Courseplay (in-game 29/9: stadig widthUnknown)
|
||||
if hasVineCutter and (width or 0) < 1.5 then return ADCpBridge.VINE_ROW_WIDTH end
|
||||
return width
|
||||
end
|
||||
|
||||
function ADCpBridge.getWorkWidth(vehicle)
|
||||
local hasVine = false
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_vineCutter ~= nil then hasVine = true end
|
||||
end
|
||||
local cpWidth = courseplayWidth(vehicle)
|
||||
if cpWidth ~= nil then return cpWidth end
|
||||
if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) end
|
||||
local best = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
local variable = variableWidth(object)
|
||||
@@ -374,7 +417,7 @@ function ADCpBridge.getWorkWidth(vehicle)
|
||||
if ok and type(width) == "number" and width > best then best = width end
|
||||
end
|
||||
end
|
||||
return best
|
||||
return ADCpBridge.widthOrVine(best, hasVine)
|
||||
end
|
||||
|
||||
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
|
||||
@@ -476,6 +519,24 @@ function ADCpBridge.courseHasHeadlands(course)
|
||||
return not ok or type(n) ~= "number" or n > 0
|
||||
end
|
||||
|
||||
-- Kursusbredde til Courseplay: højst 50 m (CP's grænse; Unrealistic-redskaber kan sættes bredere). nil = urørt.
|
||||
ADCpBridge.CP_MAX_WIDTH = 50
|
||||
function ADCpBridge.cpCourseWidth(width)
|
||||
if type(width) == "number" and width > ADCpBridge.CP_MAX_WIDTH then return ADCpBridge.CP_MAX_WIDTH end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function clampCourseWidth(vehicle)
|
||||
pcall(function()
|
||||
local setting = vehicle:getCourseGeneratorSettings().workWidth
|
||||
local capped = ADCpBridge.cpCourseWidth(setting:getValue())
|
||||
if capped ~= nil then
|
||||
setting:setValue(capped)
|
||||
Logging.info("ADSmartPickup: kursusbredde for '%s' begrænset til %d m (Courseplays maksimum)", tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), capped)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||
local function setMultiTools(vehicle, n)
|
||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||
@@ -500,7 +561,7 @@ end
|
||||
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
||||
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
||||
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
||||
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
|
||||
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||
function ADCpBridge.vineOffsetFor(fruitName, role)
|
||||
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
||||
if role == "harvester" then return 0 end
|
||||
@@ -576,6 +637,7 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||
end
|
||||
setMultiTools(vehicle, opts.multiTools or 1)
|
||||
clampCourseWidth(vehicle)
|
||||
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
|
||||
local function generate(headlands)
|
||||
ADCpBridge.setHeadlands(vehicle, headlands)
|
||||
|
||||
@@ -110,7 +110,12 @@ function ADCpResume.save(adEnv)
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
-- gemmes ved hver runs-gem (flere gange i minuttet): log kun når listen ændrer sig
|
||||
local signature = table.concat(ids, ",")
|
||||
if signature ~= ADCpResume.lastLogged then
|
||||
ADCpResume.lastLogged = signature
|
||||
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
-- AD Crop Plan (ren logik)
|
||||
-- Mangel pr. afgrøde over 12 måneder (lager + forventet høst mod kædens behov) og valg af marker der skal skifte
|
||||
-- målafgrøde: "fri til kæden" -> auto, ellers forslag. Brugerens egen målafgrøde ændres aldrig automatisk.
|
||||
|
||||
ADCropPlan = {}
|
||||
ADCropPlan.HORIZON_MONTHS = 12
|
||||
ADCropPlan.MIN_SHARE = 0.10
|
||||
ADCropPlan.MIN_HA = 0.5
|
||||
-- højst så mange forslag (ikke auto-skift) pr. afgrøde, så listen ikke vokser til snesevis af rækker
|
||||
ADCropPlan.MAX_SUGGEST_PER_FRUIT = 3
|
||||
|
||||
function ADCropPlan.plannedFruit(field)
|
||||
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||
return field.fruit
|
||||
end
|
||||
|
||||
function ADCropPlan.shortages(demand, stock, fields, yields)
|
||||
local list = {}
|
||||
for fruit, perMonth in pairs(demand or {}) do
|
||||
local perHa = (yields or {})[fruit] or 0
|
||||
if perHa > 0 and (perMonth or 0) > 0 then
|
||||
local need = perMonth * ADCropPlan.HORIZON_MONTHS
|
||||
local supply = (stock or {})[fruit] or 0
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if ADCropPlan.plannedFruit(field) == fruit then supply = supply + (field.areaHa or 0) * perHa end
|
||||
end
|
||||
local missing = need - supply
|
||||
local missingHa = missing / perHa
|
||||
if missing > need * ADCropPlan.MIN_SHARE and missingHa > ADCropPlan.MIN_HA then
|
||||
table.insert(list, {fruit = fruit, needLiters = need, supplyLiters = supply, missingLiters = missing, missingHa = missingHa})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.missingHa ~= b.missingHa then return a.missingHa > b.missingHa end
|
||||
return a.fruit < b.fruit
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
local function isCandidate(field, used, dismissedFruit, fruit)
|
||||
if field.permanent or field.excluded then return false end
|
||||
local planned = ADCropPlan.plannedFruit(field)
|
||||
if planned ~= nil and planned ~= "" and used[planned] then return false end
|
||||
return dismissedFruit ~= fruit
|
||||
end
|
||||
|
||||
-- prices: {[afgrøde] = pris} eller en funktion afgrøde -> pris (slås kun op for kandidaternes afgrøder).
|
||||
-- harvesting = {[afgrøde] = true}: afgrøder der høstes lige nu — ingen forslag/skift for dem (læssene er på vej).
|
||||
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed, harvesting)
|
||||
local actions, taken = {}, {}
|
||||
local used = usedFruits or {}
|
||||
local priceOf = type(prices) == "function" and prices or function(fruit) return (prices or {})[fruit] or 0 end
|
||||
local priceCache = {}
|
||||
local function valueOf(fruit)
|
||||
if fruit == "" then return 0 end
|
||||
if priceCache[fruit] == nil then priceCache[fruit] = ((yields or {})[fruit] or 0) * (priceOf(fruit) or 0) end
|
||||
return priceCache[fruit]
|
||||
end
|
||||
for _, shortage in ipairs(shortages or {}) do
|
||||
local remaining = (harvesting or {})[shortage.fruit] and 0 or shortage.missingHa
|
||||
local candidates = {}
|
||||
for _, field in ipairs(fields or {}) do
|
||||
if not taken[field.id] and isCandidate(field, used, (dismissed or {})[field.id], shortage.fruit) then
|
||||
local planned = ADCropPlan.plannedFruit(field) or ""
|
||||
local value = valueOf(planned)
|
||||
table.insert(candidates, {field = field, value = value})
|
||||
end
|
||||
end
|
||||
table.sort(candidates, function(a, b)
|
||||
if a.value ~= b.value then return a.value < b.value end
|
||||
local da, db = math.abs((a.field.areaHa or 0) - remaining), math.abs((b.field.areaHa or 0) - remaining)
|
||||
if da ~= db then return da < db end
|
||||
return a.field.id < b.field.id
|
||||
end)
|
||||
local suggested = 0
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if remaining <= 0 then break end
|
||||
local field = candidate.field
|
||||
local auto = field.chainFree == true and field.targetSource ~= "user"
|
||||
if auto or suggested < ADCropPlan.MAX_SUGGEST_PER_FRUIT then
|
||||
table.insert(actions, {fieldId = field.id, from = ADCropPlan.plannedFruit(field) or "", to = shortage.fruit,
|
||||
hectares = field.areaHa or 0, reason = shortage.fruit, mode = auto and "auto" or "suggest"})
|
||||
taken[field.id] = true
|
||||
remaining = remaining - (field.areaHa or 0)
|
||||
if not auto then suggested = suggested + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
return actions
|
||||
end
|
||||
@@ -0,0 +1,190 @@
|
||||
-- AD Dependency (ren logik)
|
||||
-- Advarsel når spilleren slukker en produktion gården er afhængig af (spec 2026-09-30-produktionskaeder afsnit 8).
|
||||
-- En vare er "nødvendig" når et tændt forsyningsflow kører den (manuel vare, eller flowets mål tager den i
|
||||
-- auto-tilstand), en stald æder den, eller en aktiv kæde har den som input. Varer er NAVNE; kapacitet og forbrug
|
||||
-- i samme enhed (adapteren i adChain.lua vælger), lagerdage i spillets døgn. Ingen spiladgang.
|
||||
|
||||
ADDependency = {}
|
||||
ADDependency.STOCK_DAYS_OK = 60 -- lager til mere end så mange døgn: ingen advarsel
|
||||
ADDependency.MAX_USERS = 2 -- brugere nævnt i beskeden; resten "+k"
|
||||
|
||||
local function contains(list, value)
|
||||
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
local function sortedKeys(map)
|
||||
local keys = {}
|
||||
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||
table.sort(keys)
|
||||
return keys
|
||||
end
|
||||
|
||||
-- flows = {{name, kind, stopped, targetBuildingIds, ingredientMode, ingredients, chain}} (chain = kædens rodnøgle for kædens flows)
|
||||
-- barnFood = {[vare] = {staldnavn...}}, chainInputs = {[vare] = {kædenavn...}}
|
||||
-- accepts = {[bygnings-id] = {[vare] = true}}: hvad auto-flowenes mål tager (adapteren slår op)
|
||||
-- -> {[vare] = {users = {navn...}, onlyChain, strong}} (brugere i rækkefølgen flows, stalde, kæder; hver kun én gang).
|
||||
-- strong = varen er valgt som manuel vare i et flow eller er kæde-input: behovet gælder også uden kendt forbrug.
|
||||
-- Stald-foder og auto-flowenes mål er "svage": uden kendt forbrug (lagerdage) er de ikke nødvendige.
|
||||
function ADDependency.neededOutputs(flows, barnFood, chainInputs, accepts)
|
||||
local result = {}
|
||||
local function add(name, user, isChain, isStrong)
|
||||
if name == nil or user == nil then return end
|
||||
local entry = result[name]
|
||||
if entry == nil then
|
||||
entry = {users = {}, onlyChain = true, strong = false}
|
||||
result[name] = entry
|
||||
end
|
||||
if not isChain then entry.onlyChain = false end
|
||||
if isStrong then entry.strong = true end
|
||||
if not contains(entry.users, user) then table.insert(entry.users, user) end
|
||||
end
|
||||
for _, flow in ipairs(flows or {}) do
|
||||
if flow.kind ~= "outbound" and not flow.stopped then
|
||||
local user = tostring(flow.name or "?")
|
||||
-- kædens egne flows (flow.chain) er kæde-behov: Task 10 melder kæderne, så de gør ikke onlyChain falsk
|
||||
local isChainFlow = flow.chain ~= nil and flow.chain ~= ""
|
||||
if flow.ingredientMode == "manual" then
|
||||
for _, name in ipairs(flow.ingredients or {}) do add(name, user, isChainFlow, true) end
|
||||
else
|
||||
for _, building in ipairs(flow.targetBuildingIds or {}) do
|
||||
for _, name in ipairs(sortedKeys((accepts or {})[building])) do add(name, user, isChainFlow, isChainFlow) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(sortedKeys(barnFood)) do
|
||||
for _, barn in ipairs(barnFood[name]) do add(name, tostring(barn), false) end
|
||||
end
|
||||
for _, name in ipairs(sortedKeys(chainInputs)) do
|
||||
for _, root in ipairs(chainInputs[name]) do add(name, tostring(root), true, true) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioner der ikke er kædens (samme regel som ADChain.enabledKeys) -> {[nøgle] = true}
|
||||
function ADDependency.enabledKeys(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Tændte produktioners kapacitet pr. vare (outputs × cyklusser pr. måned) -> {[vare] = mængde}
|
||||
function ADDependency.capacity(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
result[name] = (result[name] or 0) + (amount or 0) * (recipe.cyclesPerMonth or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Dækker andre tændte produktioner varen? Ukendt/intet forbrug: enhver kapacitet dækker.
|
||||
local function isCovered(name, otherCapacity, demand)
|
||||
local capacity = (otherCapacity or {})[name] or 0
|
||||
return capacity > 0 and capacity >= ((demand or {})[name] or 0)
|
||||
end
|
||||
|
||||
-- Rækker lageret længe nok? nil = ukendt (advar, uden dage).
|
||||
local function stockLasts(name, stockDays)
|
||||
local days = (stockDays or {})[name]
|
||||
return days ~= nil and days > ADDependency.STOCK_DAYS_OK
|
||||
end
|
||||
|
||||
-- {[bygning|vare] = true} for tændte opskrifter: spilleren har valgt hvilken opskrift i fabrikken der kører.
|
||||
local function enabledInBuilding(recipes)
|
||||
local result = {}
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if recipe.enabled then
|
||||
for name, amount in pairs(recipe.outputs or {}) do
|
||||
if (amount or 0) > 0 then result[tostring(recipe.building) .. "|" .. name] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Er behovet for varen reelt? Kun kæder: nej (Task 10 melder kæderne). Svagt behov (stald-foder, auto-flowets mål)
|
||||
-- kræver kendt forbrug (lagerdage); manuel flow-vare og kæde-input gælder også uden.
|
||||
local function isNeeded(need, name, stockDays)
|
||||
if need == nil or need.onlyChain then return false end
|
||||
return need.strong == true or (stockDays or {})[name] ~= nil
|
||||
end
|
||||
|
||||
-- Første nødvendige, udækkede vare (efter navn) for en slukket produktion (ikke kædens) -> advarsel eller nil.
|
||||
local function check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
if recipe.enabled or recipe.byChain then return nil end
|
||||
for _, name in ipairs(sortedKeys(recipe.outputs)) do
|
||||
local need = (needed or {})[name]
|
||||
if isNeeded(need, name, stockDays) and (recipe.outputs[name] or 0) > 0
|
||||
and not alternatives[tostring(recipe.building) .. "|" .. name]
|
||||
and not isCovered(name, otherCapacity, demand) and not stockLasts(name, stockDays) then
|
||||
local days = (stockDays or {})[name]
|
||||
return {recipeKey = recipe.key, building = recipe.building, buildingName = recipe.buildingName, fillType = name,
|
||||
users = need.users, onlyChain = need.onlyChain == true, days = days}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Alle slukkede, nødvendige produktioner (én pr. fabrik) — "Slukket men nødvendig" i Kræver handling.
|
||||
function ADDependency.offNeeded(recipes, needed, stockDays, otherCapacity, demand)
|
||||
local result, seen = {}, {}
|
||||
local alternatives = enabledInBuilding(recipes)
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
if warning ~= nil and not seen[tostring(warning.building)] then
|
||||
seen[tostring(warning.building)] = true
|
||||
table.insert(result, warning)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Fælles regler (warnings og offNeeded): ingen advarsel når en anden opskrift i SAMME fabrik laver varen og er
|
||||
-- tændt, når andre tændte produktioner dækker behovet, når lageret rækker > 60 dage, eller når kun kæder bruger varen.
|
||||
-- Nye slukninger siden sidste evaluering: tændt i prevEnabled, slukket nu, nødvendig og ikke dækket.
|
||||
-- prevEnabled = nil (første evaluering efter indlæsning) -> ingen. Én pr. fabrik og vare.
|
||||
function ADDependency.warnings(prevEnabled, recipes, needed, stockDays, otherCapacity, demand)
|
||||
local result, seen = {}, {}
|
||||
if prevEnabled == nil then return result end
|
||||
local alternatives = enabledInBuilding(recipes)
|
||||
for _, recipe in ipairs(recipes or {}) do
|
||||
if prevEnabled[recipe.key] then
|
||||
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||
local id = warning ~= nil and (tostring(warning.building) .. "|" .. warning.fillType) or nil
|
||||
if warning ~= nil and not seen[id] then
|
||||
seen[id] = true
|
||||
table.insert(result, warning)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- "A, B +2"
|
||||
function ADDependency.usersText(users)
|
||||
local shown = {}
|
||||
for index = 1, math.min(#(users or {}), ADDependency.MAX_USERS) do shown[index] = tostring(users[index]) end
|
||||
if #shown == 0 then return "?" end
|
||||
local text = table.concat(shown, ", ")
|
||||
if #users > #shown then text = text .. " +" .. tostring(#users - #shown) end
|
||||
return text
|
||||
end
|
||||
|
||||
-- Beskedtekst (spu_dep_warn med dage, spu_dep_warnNoDays uden).
|
||||
function ADDependency.notifyText(warning, textOf, titleOf)
|
||||
local title = tostring(titleOf ~= nil and titleOf(warning.fillType) or warning.fillType)
|
||||
local users = ADDependency.usersText(warning.users)
|
||||
local building = tostring(warning.buildingName or warning.building or "?")
|
||||
local days = warning.days
|
||||
if days ~= nil and days < math.huge then
|
||||
return string.format(textOf("spu_dep_warn"), building, title, users, math.max(0, math.floor(days)))
|
||||
end
|
||||
return string.format(textOf("spu_dep_warnNoDays"), building, title, users)
|
||||
end
|
||||
@@ -146,7 +146,7 @@ end
|
||||
|
||||
local PLAN_VALUES = {
|
||||
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
|
||||
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
|
||||
fertilize = true, weed = true, straw = true, chainFree = true, soil = true, rigs = true, unloaders = true,
|
||||
}
|
||||
|
||||
function ADFieldGuide.choose(guide, id, value)
|
||||
@@ -206,6 +206,7 @@ local function rowsCrop(guide)
|
||||
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
|
||||
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
|
||||
end
|
||||
table.insert(rows, toggle("chainFree", L("spu_field_chainFree"), plan.chainFree))
|
||||
return rows
|
||||
end
|
||||
|
||||
@@ -442,6 +443,8 @@ end
|
||||
function ADFieldGuide.result(guide, start)
|
||||
local draft = guide.draft
|
||||
local plan = ADGuide.deepCopy(draft.plan)
|
||||
-- kilden ejes af setTarget; kladdens gamle værdi må ikke overskrive den
|
||||
plan.targetSource = nil
|
||||
local t = plan.targetFruit or ""
|
||||
if t ~= "" then
|
||||
plan.mode = "auto"
|
||||
|
||||
@@ -309,8 +309,8 @@ function ADFieldJobs.forceReady(id)
|
||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
end
|
||||
|
||||
function ADFieldJobs.setTarget(id, fruitName)
|
||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
||||
function ADFieldJobs.setTarget(id, fruitName, source)
|
||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName, source))
|
||||
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
@@ -43,6 +43,8 @@ local DEFAULT_PLAN = {
|
||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
||||
-- kæde: marken er frigivet til produktionskæden; hvem der valgte målafgrøden ("user"|"chain"|"")
|
||||
chainFree = false, targetSource = "",
|
||||
}
|
||||
local STRAW_VALUES = {chop = true, collect = true, bale = true}
|
||||
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
||||
@@ -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
|
||||
-- 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)
|
||||
copy.plan.targetFruit = fruitName or ""
|
||||
-- kilden huskes kun mens der er et mål; standard er at spilleren valgte det
|
||||
copy.plan.targetSource = copy.plan.targetFruit ~= "" and (source or "user") or ""
|
||||
copy.held = nil
|
||||
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
||||
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
||||
@@ -376,13 +380,18 @@ ADFieldModel.MIN_WORK_WIDTH = 1.5
|
||||
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
||||
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
||||
|
||||
-- Bredden kurset laves med: højst CP's maksimum.
|
||||
function ADFieldModel.effectiveWidth(width)
|
||||
if width == nil then return nil end
|
||||
return math.min(width, ADFieldModel.CP_MAX_WIDTH)
|
||||
end
|
||||
|
||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
||||
return false, "widthUnknown", {width}
|
||||
end
|
||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||
end
|
||||
-- bredere end CP's 50 m (Unrealistic kan sættes til 120 m): bruges; kurset begrænses til 50 m (brugerønske 30/9)
|
||||
width = ADFieldModel.effectiveWidth(width)
|
||||
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
|
||||
return false, "tooNarrow", {width, requiredWidth}
|
||||
end
|
||||
@@ -463,6 +472,7 @@ function ADFieldModel.toRecord(field)
|
||||
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
||||
chainFree = flag(plan.chainFree), targetSource = plan.targetSource or "",
|
||||
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||
mowWidth = field.mowWidth or 0,
|
||||
mowVehicle = field.mowVehicleId or "",
|
||||
@@ -503,6 +513,9 @@ function ADFieldModel.fromRecord(record)
|
||||
plan.weed = readFlag(record.weed, true)
|
||||
plan.targetFruit = record.targetFruit or ""
|
||||
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
||||
plan.chainFree = readFlag(record.chainFree, false)
|
||||
-- gamle saves: et eksisterende mål er valgt af spilleren
|
||||
plan.targetSource = record.targetSource or (plan.targetFruit ~= "" and "user" or "")
|
||||
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
||||
field.kind = readText(record.kind)
|
||||
field.lastFruit = readText(record.lastFruit)
|
||||
|
||||
@@ -12,6 +12,7 @@ ADFieldStorage.FIELD_TYPES = {
|
||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
||||
chainFree = "Int", targetSource = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
|
||||
dedicated = "String",
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ function ADFieldWork.getRigs(adEnv)
|
||||
manual = (state.manual or {})[id] == true,
|
||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
||||
roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), filledRole = specs.filledRole, dumpable = specs.dumpable,
|
||||
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 == "seeder" then
|
||||
@@ -379,6 +379,10 @@ local function refillPlan(adEnv, fieldId, vehicle, role, x, z)
|
||||
if fillType == nil then return nil end
|
||||
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
|
||||
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
|
||||
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}
|
||||
end
|
||||
|
||||
@@ -604,11 +608,65 @@ function ADFieldWork.buyingSupports(station, fillType)
|
||||
return ok and result == true
|
||||
end
|
||||
|
||||
-- AD-markøren ved stationen: AutoDrives eget opslag (30 m fra triggeren), ellers nærmeste ikke-Wait
|
||||
-- markør inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren).
|
||||
-- Parkering, kobling og ventepladser er aldrig en læssemarkør.
|
||||
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 okAd, wayPointId = pcall(function() return adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 30) end)
|
||||
if okAd and wayPointId ~= nil and wayPointId > 0 then return wayPointId end
|
||||
local okOwn, ownId = pcall(function()
|
||||
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
|
||||
if node == nil then return nil end
|
||||
local px, _, pz = getWorldTranslation(node)
|
||||
|
||||
@@ -10,6 +10,9 @@ ADFields.MARKER_MAX_DISTANCE = 400
|
||||
ADFields.cache = {}
|
||||
ADFields.order = {}
|
||||
ADFields.scanIndex = 0
|
||||
-- {[markId] = true}: marker hvis scanning er afsluttet (med resultat) eller fejlede — til isScanComplete
|
||||
ADFields.scanFailed = {}
|
||||
ADFields.ownedDone = false
|
||||
ADFields.mowable = nil
|
||||
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
|
||||
ADFields.OWNED_REFRESH_MS = 30000
|
||||
@@ -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
|
||||
-- (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?
|
||||
function ADFields.isMowableFruit(fruitName, mowableEntry)
|
||||
@@ -291,6 +294,17 @@ function ADFields.shapeSignature(verts)
|
||||
roundTo(cx, 1), roundTo(cz, 1))
|
||||
end
|
||||
|
||||
-- Markens areal i hektar (shoelace på {x,z}-polygonen, /10000); 0 uden en flade (under 3 hjørner).
|
||||
function ADFields.polygonAreaHa(verts)
|
||||
if type(verts) ~= "table" or #verts < 3 then return 0 end
|
||||
local n, twiceArea = #verts, 0
|
||||
for index = 1, n do
|
||||
local a, b = verts[index], verts[index % n + 1]
|
||||
twiceArea = twiceArea + a.x * b.z - b.x * a.z
|
||||
end
|
||||
return math.abs(twiceArea) / 2 / 10000
|
||||
end
|
||||
|
||||
-- Hjørnernes gennemsnit (nil uden hjørner).
|
||||
function ADFields.polygonCenter(verts)
|
||||
if verts == nil or #verts == 0 then return nil end
|
||||
@@ -555,9 +569,20 @@ local function finishOwnedScan(scan)
|
||||
if not ids[id] then ADFields.cache[id] = nil end
|
||||
end
|
||||
ADFields.order = scan.result
|
||||
ADFields.ownedDone = true
|
||||
ADFields.listCache = nil
|
||||
end
|
||||
|
||||
-- Har scanningen nået alle ejede marker mindst én gang (resultat i cachen, eller scanningen fejlede)?
|
||||
-- Indtil da er marklisten ufuldstændig (efter indlæsning fyldes den ~én mark pr. SCAN_INTERVAL_MS).
|
||||
function ADFields.isScanComplete()
|
||||
if not ADFields.ownedDone then return false end
|
||||
for _, entry in ipairs(ADFields.order) do
|
||||
if ADFields.cache[entry.id] == nil and not ADFields.scanFailed[entry.id] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
|
||||
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
|
||||
ADFields.SCAN_INTERVAL_MS = 2000
|
||||
@@ -601,6 +626,7 @@ function ADFields.scanStep(adEnv, farmId, now)
|
||||
local ok, err = pcall(task)
|
||||
if not ok then
|
||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
|
||||
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||
ADFields.activeScan = nil
|
||||
return
|
||||
end
|
||||
@@ -617,7 +643,10 @@ function ADFields.scanStep(adEnv, farmId, now)
|
||||
else
|
||||
ok, err = pcall(finishScan, adEnv, scan)
|
||||
end
|
||||
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
|
||||
if not ok then
|
||||
Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err))
|
||||
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||
end
|
||||
end
|
||||
|
||||
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||
|
||||
@@ -69,7 +69,7 @@ end
|
||||
local RUN_FIELDS = {
|
||||
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
||||
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
||||
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
|
||||
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons", "chain",
|
||||
}
|
||||
|
||||
local function draftFrom(run, isNew)
|
||||
@@ -339,7 +339,12 @@ local function rowsRules(guide)
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local rows = {}
|
||||
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
||||
if sells then
|
||||
-- "Altid" (0) øverst: sælg uanset pris (in-game 30/9)
|
||||
local options = {{label = L("spu_sellAt_always"), value = 0}}
|
||||
for _, option in ipairs(percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5)) do table.insert(options, option) end
|
||||
table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), options, draft.sellAtPercent))
|
||||
end
|
||||
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||
@@ -489,7 +494,7 @@ local SUMMARY = {
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if sells then table.insert(parts, draft.sellAtPercent == 0 and L("spu_guide_flow_sumSellAlways") or L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||
|
||||
@@ -52,20 +52,61 @@ end
|
||||
|
||||
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
|
||||
-- {{kind, runs}, ...}; tomme grupper udelades.
|
||||
-- Spillerens egne flows først (forsyning, udkørsel), så kædernes egne flows (run.chain) samlet til sidst
|
||||
-- (in-game 30/9: "Kæde: …"-flows blandede sig alfabetisk ind mellem de flows spilleren selv har sat op).
|
||||
function ADFlowView.boardGroups(runs)
|
||||
local byKind = {supply = {}, outbound = {}}
|
||||
local chainRuns = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
table.insert(chainRuns, run)
|
||||
else
|
||||
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
||||
end
|
||||
end
|
||||
local groups = {}
|
||||
for _, kind in ipairs({"supply", "outbound"}) do
|
||||
if #byKind[kind] > 0 then
|
||||
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")})
|
||||
table.insert(groups, {kind = kind, chain = false, runs = sortedBy(byKind[kind], "name")})
|
||||
end
|
||||
end
|
||||
if #chainRuns > 0 then table.insert(groups, {kind = "supply", chain = true, runs = sortedBy(chainRuns, "name")}) end
|
||||
return groups
|
||||
end
|
||||
|
||||
-- Kort maskinnavn til lister: " Unrealistic" og parenteser som "(+ Custom Crops)" fjernes (lange navne blev ombrudt
|
||||
-- med traktorens nummer alene på næste linje, in-game 30/9). Bliver intet tilbage, bruges det fulde navn.
|
||||
function ADFlowView.shortVehicleName(name)
|
||||
local full = tostring(name or "")
|
||||
local short = full:gsub("%s*%b()", ""):gsub("%s+[Uu]nrealistic", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if short == "" then return full end
|
||||
return short
|
||||
end
|
||||
|
||||
-- Kædeflow uden traktor (venter på en ledig traktor/vogn).
|
||||
function ADFlowView.isIdleChain(run)
|
||||
return run.chain ~= nil and run.chain ~= "" and #(run.vehicleIds or {}) == 0
|
||||
end
|
||||
|
||||
-- Kædeflows uden traktor foldes sammen i listen (de venter på en ledig traktor/vogn). -> active, idle
|
||||
function ADFlowView.splitIdle(runs)
|
||||
local active, idle = {}, {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(#(run.vehicleIds or {}) > 0 and active or idle, run)
|
||||
end
|
||||
return active, idle
|
||||
end
|
||||
|
||||
-- Navn i listen: kædeflows står under overskriften "Kædernes egne flows", så "Kæde: " fjernes (ellers skjuler
|
||||
-- afkortningen hvilken bygning det gælder).
|
||||
function ADFlowView.listName(run)
|
||||
local name = tostring(run.name or "")
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
name = name:gsub("^Kæde:%s*", ""):gsub("^Chain:%s*", "")
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
|
||||
function ADFlowView.orderedRuns(runs)
|
||||
local result = {}
|
||||
|
||||
@@ -23,6 +23,12 @@ function ADFlowWagons.remember(wagons, tractorId, wagonId)
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Må redskabet bag traktoren huskes som flowets vogn? Kun noget der kan rumme last — ikke plov/kultivator (ingen
|
||||
-- plads) og ikke såmaskine (frøtank). s = {capacity, isSowingMachine}. in-game 30/9: 'Sell Slurry 2' huskede ploven.
|
||||
function ADFlowWagons.isRememberableWagon(s)
|
||||
return (s.capacity or 0) > 0 and not s.isSowingMachine
|
||||
end
|
||||
|
||||
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
|
||||
function ADFlowWagons.startAction(s)
|
||||
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
|
||||
@@ -453,11 +459,16 @@ end
|
||||
|
||||
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows).
|
||||
-- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
|
||||
local function isCargoWagon(object)
|
||||
local _, capacity = ADPool.cargoOf(object)
|
||||
return ADFlowWagons.isRememberableWagon({capacity = capacity, isSowingMachine = object.spec_sowingMachine ~= nil})
|
||||
end
|
||||
|
||||
local function rememberWagonOf(vehicle)
|
||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||
local wagonId = rear[1] ~= nil and 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
|
||||
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
@@ -551,6 +562,15 @@ function ADFlowWagons.isEmptying(vehicle)
|
||||
return emptying[vehicle] ~= nil
|
||||
end
|
||||
|
||||
-- Kun læsning (statusvinduet): hvad flowets vogn-logik har traktoren i gang med -> "fetching"|"waiting"|"emptying"|"homing"|nil
|
||||
function ADFlowWagons.statusOf(vehicle)
|
||||
if fetching[vehicle] ~= nil then return "fetching" end
|
||||
if waiting[vehicle] ~= nil then return "waiting" end
|
||||
if emptying[vehicle] ~= nil then return "emptying" end
|
||||
if homing[vehicle] ~= nil then return "homing" end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADFlowWagons.sendHome(adEnv, run)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
@@ -615,7 +635,7 @@ local function rememberAll(adEnv)
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||
if wagonId ~= nil then
|
||||
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
|
||||
if updated ~= run.wagons then
|
||||
|
||||
@@ -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
|
||||
@@ -660,6 +660,13 @@ end
|
||||
|
||||
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
||||
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
||||
-- Palleskur: moden tager ét objekt ud ad gangen og afslutter selv pålæsningen (fuld / tomt lager / 30 s uden
|
||||
-- udlevering). Imens holdes AD's egen pålæsning, ellers kører AD, så snart triggeren et øjeblik er tom (in-game 30/9:
|
||||
-- Sell Egg kørte halvfuld). Først når AD har aktiveret UAL (det gør AD selv ved ankomst).
|
||||
function ADOutbound.holdForSpawn(sourceKind, done, ualActive)
|
||||
return sourceKind == "objectStorage" and not done and ualActive == true
|
||||
end
|
||||
|
||||
local function updateSpawn(adEnv, state, task, source, dt)
|
||||
local vehicle = task.vehicle
|
||||
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
||||
@@ -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
|
||||
spawn.pending = nil
|
||||
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType)))
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift: lager %d -> %d, vogn %d -> %d objekter); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType), spawn.pending.stored, stored, spawn.pending.loaded, loaded))
|
||||
state.done = true
|
||||
return
|
||||
else
|
||||
@@ -763,6 +770,7 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||||
task:finished()
|
||||
return true
|
||||
end
|
||||
if ADOutbound.holdForSpawn(source.kind, state.done, task.activatedUALLoading) then return true end
|
||||
end
|
||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||
if state.lastFill == nil or level > state.lastFill then
|
||||
|
||||
@@ -67,7 +67,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
||||
storagePercent = storagePercent,
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
-- sælg fra 0 % = "Altid": uanset pris (også uden prisdata)
|
||||
priceOk = input.sellAtPercent == 0 or (percent ~= nil and percent >= (input.sellAtPercent or 90)),
|
||||
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ function ADPool.roleOf(specs)
|
||||
return role
|
||||
end
|
||||
|
||||
-- Ekstra roller for et redskab: sprøjtens roller (gødning/kalk/ukrudt) — men ikke for en såmaskine med
|
||||
-- gødningstank (Kinze 4905), den er kun såmaskine (in-game 30/9: hentet til ren gødning).
|
||||
function ADPool.rolesOf(role, sprayRoles)
|
||||
if role ~= "seeder" and sprayRoles ~= nil then return sprayRoles end
|
||||
return ADFieldFleet.rolesFor(role)
|
||||
end
|
||||
|
||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||
function ADPool.numberOf(value)
|
||||
if type(value) == "number" then return value end
|
||||
@@ -402,7 +409,7 @@ function ADPool.collect(adEnv, isBusy)
|
||||
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||
table.insert(implements, {carries = carries, capacity = capacity,
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
role = role, roles = ADPool.rolesOf(role, specs.sprayRoles), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -110,6 +110,8 @@ function ADProfiler.install()
|
||||
{ADSources, "getFarmInventory", "gårdens lagre"},
|
||||
{ADWaitPool, "hasRouteTo", "rutetjek"},
|
||||
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
|
||||
{ADChain, "evaluate", "kæde"},
|
||||
{ADChainData, "collect", "kæde: læs gården"},
|
||||
}
|
||||
for _, target in ipairs(targets) do
|
||||
ADProfiler.wrap(target[1], target[2], target[3])
|
||||
|
||||
@@ -97,6 +97,7 @@ local function copyRun(run)
|
||||
vehicleIds = copyList(run.vehicleIds),
|
||||
stopped = run.stopped == true,
|
||||
wagons = ADRuns.copyWagons(run.wagons),
|
||||
chain = run.chain or "",
|
||||
}
|
||||
end
|
||||
|
||||
@@ -145,6 +146,7 @@ function ADRuns.createRun(store, name, kind)
|
||||
loops = 0,
|
||||
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
||||
vehicleIds = {},
|
||||
chain = "",
|
||||
}
|
||||
new.nextId = new.nextId + 1
|
||||
table.insert(new.runs, run)
|
||||
@@ -200,10 +202,12 @@ function ADRuns.updateRun(store, runId, changes)
|
||||
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end
|
||||
if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end
|
||||
if changes.products ~= nil then run.products = copyProducts(changes.products) end
|
||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||
-- 0 = "Altid" (sælg uanset pris); ellers 50-100 %
|
||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = tonumber(changes.sellAtPercent) == 0 and 0 or clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
||||
if changes.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
|
||||
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
||||
if changes.chain ~= nil then run.chain = tostring(changes.chain) end
|
||||
return new
|
||||
end
|
||||
|
||||
@@ -440,6 +444,7 @@ function ADRuns.toRecords(store)
|
||||
collectRound = run.collectRound and 1 or 0,
|
||||
stopped = run.stopped and 1 or 0,
|
||||
wagons = ADRuns.serializeWagons(run.wagons),
|
||||
chain = run.chain or "",
|
||||
}
|
||||
end
|
||||
return records
|
||||
@@ -480,7 +485,7 @@ function ADRuns.fromRecords(records)
|
||||
targetBuildingIds = parseIds(record.targetBuildings),
|
||||
storeBuildingIds = parseIds(record.storeBuildings),
|
||||
products = ADRuns.parseProducts(record.products),
|
||||
sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||
sellAtPercent = tonumber(record.sellAt) == 0 and 0 or clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT),
|
||||
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT),
|
||||
collectRound = tonumber(record.collectRound) == 1,
|
||||
@@ -494,6 +499,7 @@ function ADRuns.fromRecords(records)
|
||||
vehicleIds = copyList(record.vehicleIds),
|
||||
stopped = tonumber(record.stopped) == 1,
|
||||
wagons = ADRuns.parseWagons(record.wagons),
|
||||
chain = record.chain or "",
|
||||
})
|
||||
store.nextId = math.max(store.nextId, id + 1)
|
||||
end
|
||||
|
||||
@@ -1476,6 +1476,9 @@ function ADRunsController.save()
|
||||
pcall(ADFieldJobs.save)
|
||||
end
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||
-- kædens tilstand (tændte mellemled, pausede kæder, afviste forslag) — samme gem-regel som navnefilen
|
||||
if ADChain ~= nil and ADChain.save ~= nil then pcall(ADChain.save) end
|
||||
if ADSellBlock ~= nil and ADSellBlock.save ~= nil then pcall(ADSellBlock.save) end
|
||||
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver
|
||||
-- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem)
|
||||
if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
|
||||
@@ -1495,6 +1498,8 @@ function ADRunsController.load(adEnv)
|
||||
if ADNames ~= nil then pcall(ADNames.load) end
|
||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
||||
if ADChain ~= nil and ADChain.load ~= nil then pcall(ADChain.load) end
|
||||
if ADSellBlock ~= nil and ADSellBlock.load ~= nil then pcall(ADSellBlock.load) end
|
||||
if ADCpResume ~= nil then pcall(ADCpResume.load) end
|
||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||
for _, run in ipairs(store.runs) do
|
||||
|
||||
@@ -51,6 +51,7 @@ function ADRunsStorage.writeRecords(xmlFile, records)
|
||||
-- maskinpulje: flowet stoppet + husket vogn pr. traktor
|
||||
xmlFile:setInt(key .. "#stopped", record.stopped or 0)
|
||||
xmlFile:setString(key .. "#wagons", record.wagons or "")
|
||||
xmlFile:setString(key .. "#chain", record.chain or "")
|
||||
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||
end
|
||||
@@ -82,6 +83,7 @@ function ADRunsStorage.readRecords(xmlFile)
|
||||
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
||||
stopped = xmlFile:getInt(key .. "#stopped"),
|
||||
wagons = xmlFile:getString(key .. "#wagons"),
|
||||
chain = xmlFile:getString(key .. "#chain"),
|
||||
vehicleIds = {},
|
||||
}
|
||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
-- AD Sell Block
|
||||
-- Salgssteder spilleren har slået fra (in-game 30/9: "Market Price Liquid Fill Station" fra FarmFillStations gik i stå
|
||||
-- ved aflæsning af gylle). Konsol: spuSellBlock viser salgsstederne; spuSellBlock <del af navn> slår dem fra/til.
|
||||
-- Udkørsel (ADSources.findSellMarkers) og priser (ADSources.getMarketQuote) springer udelukkede over. Gemmes pr.
|
||||
-- savegame i ADSmartPickup_sellblock.xml (skrives fra ADRunsController.save, som de andre filer).
|
||||
|
||||
ADSellBlock = {}
|
||||
ADSellBlock.FILE_NAME = "ADSmartPickup_sellblock.xml"
|
||||
ADSellBlock.ROOT = "sellBlock"
|
||||
ADSellBlock.blocked = {} -- {[stationId] = visningsnavn}
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- stations = {{id, names = {...}}}, query = del af et navn (små/store bogstaver ligegyldige) -> matchende stationer
|
||||
function ADSellBlock.match(stations, query)
|
||||
local wanted = string.lower(tostring(query or ""))
|
||||
local hits = {}
|
||||
if wanted == "" then return hits end
|
||||
for _, station in ipairs(stations or {}) do
|
||||
for _, name in ipairs(station.names or {}) do
|
||||
if string.find(string.lower(tostring(name)), wanted, 1, true) ~= nil then
|
||||
table.insert(hits, station)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return hits
|
||||
end
|
||||
|
||||
-- Ny tabel: hver station skiftes (fra -> til, til -> fra). Første navn huskes til visning.
|
||||
function ADSellBlock.toggle(blocked, stations)
|
||||
local result = {}
|
||||
for id, name in pairs(blocked or {}) do result[id] = name end
|
||||
for _, station in ipairs(stations or {}) do
|
||||
if result[station.id] ~= nil then
|
||||
result[station.id] = nil
|
||||
else
|
||||
result[station.id] = tostring((station.names or {})[1] or station.id)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function stationId(station)
|
||||
local placeable = station ~= nil and station.owningPlaceable or nil
|
||||
if placeable ~= nil and placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADSellBlock.isBlocked(station)
|
||||
local id = stationId(station)
|
||||
return id ~= nil and ADSellBlock.blocked[id] ~= nil
|
||||
end
|
||||
|
||||
local function stationNames(station)
|
||||
local names = {}
|
||||
local placeable = station.owningPlaceable
|
||||
if placeable ~= nil and placeable.getName ~= nil then
|
||||
local ok, name = pcall(placeable.getName, placeable)
|
||||
if ok and name ~= nil then table.insert(names, name) end
|
||||
end
|
||||
if station.getName ~= nil then
|
||||
local ok, name = pcall(station.getName, station)
|
||||
if ok and name ~= nil then table.insert(names, name) end
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
-- Alle salgssteder med AD-markør -> {{id, names}} (markørens navn først: det spilleren ser i AutoDrive).
|
||||
local function sellStations(adEnv)
|
||||
local list, byId = {}, {}
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||
local id = station ~= nil and ADPrices.isSellingStation(station) and stationId(station) or nil
|
||||
if id ~= nil then
|
||||
if byId[id] == nil then
|
||||
byId[id] = {id = id, names = {}}
|
||||
table.insert(list, byId[id])
|
||||
for _, name in ipairs(stationNames(station)) do table.insert(byId[id].names, name) end
|
||||
end
|
||||
table.insert(byId[id].names, 1, marker.name)
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return tostring(a.names[1]) < tostring(b.names[1]) end)
|
||||
return list
|
||||
end
|
||||
|
||||
function ADSellBlock:consoleSellBlock(...)
|
||||
local query = table.concat({...}, " ")
|
||||
local adEnv = FS25_AutoDrive
|
||||
if adEnv == nil or adEnv.ADGraphManager == nil then return "AutoDrive ikke fundet" end
|
||||
local stations = sellStations(adEnv)
|
||||
if query == "" then
|
||||
local lines = {"Salgssteder (spuSellBlock <del af navn> slår fra/til):"}
|
||||
for _, station in ipairs(stations) do
|
||||
table.insert(lines, string.format(" %s %s", ADSellBlock.blocked[station.id] ~= nil and "[FRA]" or "[til]", table.concat(station.names, " / ")))
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
local hits = ADSellBlock.match(stations, query)
|
||||
if #hits == 0 then return string.format("Intet salgssted matcher '%s' (spuSellBlock uden navn viser listen)", query) end
|
||||
ADSellBlock.blocked = ADSellBlock.toggle(ADSellBlock.blocked, hits)
|
||||
local lines = {}
|
||||
for _, station in ipairs(hits) do
|
||||
local text = string.format("%s: %s", station.names[1], ADSellBlock.blocked[station.id] ~= nil and "slået FRA som salgssted" or "bruges igen")
|
||||
table.insert(lines, text)
|
||||
Logging.info("ADSmartPickup: salgssted " .. text)
|
||||
end
|
||||
if ADRunsController ~= nil and ADRunsController.save ~= nil then pcall(ADRunsController.save) end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function getPath()
|
||||
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if path == nil then return nil end
|
||||
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADSellBlock.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADSellBlock.save()
|
||||
local path = getPath()
|
||||
if path == nil then return false end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupSellBlock", path, ADSellBlock.ROOT)
|
||||
if xmlFile == nil then return false end
|
||||
local index = 0
|
||||
for id, name in pairs(ADSellBlock.blocked) do
|
||||
local key = string.format("%s.station(%d)", ADSellBlock.ROOT, index)
|
||||
xmlFile:setString(key .. "#id", id)
|
||||
xmlFile:setString(key .. "#name", name)
|
||||
index = index + 1
|
||||
end
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
function ADSellBlock.load()
|
||||
ADSellBlock.blocked = {}
|
||||
local path = getPath()
|
||||
if path == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupSellBlock", path)
|
||||
if xmlFile == nil then return end
|
||||
xmlFile:iterate(ADSellBlock.ROOT .. ".station", function(_, key)
|
||||
local id = xmlFile:getString(key .. "#id")
|
||||
if id ~= nil then ADSellBlock.blocked[id] = xmlFile:getString(key .. "#name") or id end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
if addConsoleCommand ~= nil then
|
||||
addConsoleCommand("spuSellBlock", "Salgssteder: uden navn = liste; spuSellBlock <del af navn> slår fra/til", "consoleSellBlock", ADSellBlock)
|
||||
end
|
||||
@@ -28,6 +28,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupChainTab.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
||||
@@ -1151,6 +1152,9 @@ local function installHook(adEnv)
|
||||
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
||||
ADHitchCreep.install(adEnv)
|
||||
end
|
||||
if ADLoadDiag ~= nil and ADLoadDiag.install ~= nil then
|
||||
ADLoadDiag.install(adEnv)
|
||||
end
|
||||
|
||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
||||
end
|
||||
@@ -1309,6 +1313,12 @@ function ADSmartPickup:update(dt)
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
||||
end
|
||||
end
|
||||
if ADChain ~= nil then
|
||||
local okChain, errChain = pcall(ADChain.tick, getAutoDriveEnv(), dt)
|
||||
if not okChain then
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kæde fejlede: %s", tostring(errChain))
|
||||
end
|
||||
end
|
||||
if ADFlowWagons ~= nil then
|
||||
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
|
||||
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
|
||||
@@ -1328,6 +1338,16 @@ function ADSmartPickup:update(dt)
|
||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
||||
supplyState = setmetatable({}, {__mode = "k"})
|
||||
end
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.update, dt, getAutoDriveEnv()) end
|
||||
end
|
||||
|
||||
-- Statusvinduet på skærmen (adStatusHud.lua): tegner kun den cachede tekst.
|
||||
function ADSmartPickup:draw()
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.draw) end
|
||||
end
|
||||
|
||||
function ADSmartPickup:deleteMap()
|
||||
if ADStatusHud ~= nil then pcall(ADStatusHud.delete) end
|
||||
end
|
||||
|
||||
addModEventListener(ADSmartPickup)
|
||||
|
||||
@@ -585,9 +585,17 @@ function ADSources.getObjectInfoIndex(source, fillType)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Alle grupper med varen: lageret deler samme vare i flere objectInfos (fx paller med forskellig fyldning). Kun den
|
||||
-- første talt -> tallet "voksede", når en lille gruppe blev tom, og spawn-ventningen gik i stå (Sell Egg 30/9).
|
||||
function ADSources.countStored(source, fillType)
|
||||
local _, count = ADSources.getObjectInfoIndex(source, fillType)
|
||||
return count or 0
|
||||
local spec = source.placeable ~= nil and source.placeable.spec_objectStorage or nil
|
||||
local count = 0
|
||||
for _, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
|
||||
if getObjectInfoContent(objectInfo) == fillType then
|
||||
count = count + (objectInfo.numObjects or 0)
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
|
||||
@@ -636,7 +644,12 @@ function ADSources.describeAccepted(station)
|
||||
return table.concat(names, ", ")
|
||||
end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||||
-- Salgssted spilleren har slået fra (spuSellBlock, ADSellBlock).
|
||||
function ADSources.isSellBlocked(station)
|
||||
return ADSellBlock ~= nil and ADSellBlock.isBlocked ~= nil and ADSellBlock.isBlocked(station) == true
|
||||
end
|
||||
|
||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted (og ikke slået fra): { {marker, station} }.
|
||||
function ADSources.getSellMarkers(adEnv, farmId)
|
||||
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
||||
end
|
||||
@@ -645,7 +658,7 @@ function ADSources.findSellMarkers(adEnv, farmId)
|
||||
local result = {}
|
||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||
if station ~= nil and ADPrices.isSellingStation(station) then
|
||||
if station ~= nil and ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) then
|
||||
table.insert(result, {marker = marker, station = station})
|
||||
end
|
||||
end
|
||||
@@ -963,13 +976,38 @@ function ADSources.getFarmRates(adEnv, farmId, feedOnly)
|
||||
return rates
|
||||
end
|
||||
|
||||
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
|
||||
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
|
||||
function ADSources.getHusbandryConsumption(placeable)
|
||||
local rates, result = {}, {}
|
||||
if placeable == nil then return result end
|
||||
pcall(addHusbandryRates, rates, placeable)
|
||||
for fillType, rate in pairs(rates) do
|
||||
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
|
||||
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
|
||||
function ADSources.getHusbandryOutputs(placeable)
|
||||
local rates, outputs = {}, {}
|
||||
if placeable == nil then return outputs end
|
||||
pcall(addHusbandryRates, rates, placeable) -- giver kun forbrug; med for fuldstændighedens skyld
|
||||
pcall(addAnimalProduction, rates, placeable)
|
||||
for fillType, rate in pairs(rates) do
|
||||
if (rate.production or 0) > 0 then outputs[fillType] = rate.production end
|
||||
end
|
||||
return outputs
|
||||
end
|
||||
|
||||
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
|
||||
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
||||
function ADSources.getMarketQuote(fillType)
|
||||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||
local best = nil
|
||||
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
|
||||
if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then
|
||||
if ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) and ADSources.stationAccepts(station, fillType) then
|
||||
local quote = ADPrices.getQuote(station, fillType)
|
||||
if quote ~= nil and (best == nil or quote.price > best.price) then
|
||||
best = {price = quote.price, top = quote.top}
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
-- AD Status Hud
|
||||
-- Statusvinduet på skærmen (i stil med Simple Inspector): slås til/fra med input-handlingen SPU_TOGGLE_STATUS
|
||||
-- (standard numpad *). Øjebliksbilledet bygges af modens egne statusfunktioner (samme som fanerne) højst
|
||||
-- hvert sekund i update() og kun mens vinduet er tændt; draw() tegner kun de cachede linjer og bredder.
|
||||
-- Synligheden gemmes pr. bruger i modSettings/FS25_ADSmartPickup/status.xml (standard skjult).
|
||||
-- Kører kun på en klient (ikke dedikeret server). Alle spilkald er beskyttet; fejl logges én gang pr. nøgle.
|
||||
|
||||
ADStatusHud = {}
|
||||
|
||||
ADStatusHud.LOG_PREFIX = "ADSmartPickup: status: "
|
||||
ADStatusHud.REFRESH_MS = 1000
|
||||
ADStatusHud.STUCK_WARN_MS = 120000 -- stået stille så længe (uden at Smart Pickup holder den) = advarsel
|
||||
ADStatusHud.TEXT_SIZE_PX = 14
|
||||
ADStatusHud.MARGIN_PX = 8
|
||||
ADStatusHud.SCREEN_OFFSET_PX = 16 -- afstand til skærmens højre kant
|
||||
ADStatusHud.TOP_Y = 1 - 0.12 -- øverste kant (normaliseret); under spillets info-boks
|
||||
ADStatusHud.SETTINGS_DIR = "modSettings/FS25_ADSmartPickup/"
|
||||
ADStatusHud.SETTINGS_FILE = "status.xml"
|
||||
ADStatusHud.SETTINGS_ROOT = "adSmartPickupStatus"
|
||||
ADStatusHud.HEADER_COLOR = {1, 1, 1, 1}
|
||||
|
||||
ADStatusHud.visible = false
|
||||
ADStatusHud.lines = {} -- {{text, level, header, color, width}}
|
||||
ADStatusHud.layout = nil -- {x, y, width, height, textSize, marginX, marginY, padding}
|
||||
|
||||
local timerMs = 0
|
||||
local settingsLoaded = false
|
||||
local overlays = nil
|
||||
local loggedErrors = {}
|
||||
local registerToggleActionRef -- sat nederst (tastens handler til krogen)
|
||||
|
||||
-- ---- hjælpere ----
|
||||
|
||||
local function logOnce(key, err)
|
||||
if loggedErrors[key] then return end
|
||||
loggedErrors[key] = true
|
||||
if Logging ~= nil and Logging.warning ~= nil then
|
||||
Logging.warning(ADStatusHud.LOG_PREFIX .. "%s fejlede: %s", tostring(key), tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
local function guarded(key, fn, ...)
|
||||
local ok, result = pcall(fn, ...)
|
||||
if not ok then
|
||||
logOnce(key, result)
|
||||
return nil
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function textOf(key)
|
||||
if g_i18n == nil then return key end
|
||||
local ok, text = pcall(g_i18n.getText, g_i18n, key)
|
||||
return ok and text or key
|
||||
end
|
||||
|
||||
function ADStatusHud.isClient()
|
||||
local mission = g_currentMission
|
||||
if mission == nil or mission.getIsClient == nil then return false end
|
||||
local ok, isClient = pcall(mission.getIsClient, mission)
|
||||
return ok and isClient == true
|
||||
end
|
||||
|
||||
-- Fanernes egne tekst-/statusfunktioner ligger som metoder på SmartPickupFrame og bruger kun self.i18n:
|
||||
-- en let "frame" med spillets i18n genbruger dem uden at røre menuen. "Start afvist" (lastStartFailed, rødt flow)
|
||||
-- huskes på den rigtige ESC-fane (ADSmartPickup.guiFrame): tabellen deles, så vinduet viser samme farve som
|
||||
-- Flows-listen (flowLevel rydder en afvisning når flowet kører — det samme som fanen selv gør).
|
||||
local function frameProxy()
|
||||
if SmartPickupFrame == nil then return nil end
|
||||
local real = ADSmartPickup ~= nil and ADSmartPickup.guiFrame or nil
|
||||
local failed = {}
|
||||
if type(real) == "table" then
|
||||
if type(real.lastStartFailed) ~= "table" then real.lastStartFailed = {} end
|
||||
failed = real.lastStartFailed
|
||||
end
|
||||
return setmetatable({i18n = g_i18n, lastStartFailed = failed}, {__index = SmartPickupFrame})
|
||||
end
|
||||
|
||||
local function fillTitle(fillType)
|
||||
if fillType == nil or g_fillTypeManager == nil then return "?" end
|
||||
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType)
|
||||
or g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||
return tostring(title or "?")
|
||||
end
|
||||
|
||||
-- ---- øjebliksbillede ----
|
||||
|
||||
local WAGON_ACTIONS = {
|
||||
fetching = {key = "spu_status_act_fetchWagon", level = "waiting"},
|
||||
waiting = {key = "spu_status_act_waitWagon", level = "waiting"},
|
||||
emptying = {key = "spu_status_act_emptying", level = "running"},
|
||||
homing = {key = "spu_status_act_homing", level = "running"},
|
||||
}
|
||||
|
||||
-- Traktorens handling som kort tekst + niveau (ADRunsController.getVehicleActivity som Flows-fanen).
|
||||
local function vehicleAction(adEnv, vehicle)
|
||||
local wagonState = ADFlowWagons ~= nil and ADFlowWagons.statusOf ~= nil and ADFlowWagons.statusOf(vehicle) or nil
|
||||
if wagonState ~= nil and WAGON_ACTIONS[wagonState] ~= nil then
|
||||
return textOf(WAGON_ACTIONS[wagonState].key), WAGON_ACTIONS[wagonState].level
|
||||
end
|
||||
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
|
||||
local place = tostring(activity.place or "?")
|
||||
if activity.kind == "stopped" then
|
||||
return textOf("spu_activity_stopped"), "stopped"
|
||||
elseif activity.kind == "waiting" then
|
||||
return string.format(textOf("spu_status_act_waiting"), place), "waiting"
|
||||
elseif activity.kind == "holding" then
|
||||
return string.format(textOf("spu_status_act_holding"), place), "waiting"
|
||||
elseif activity.kind == "empty" and activity.fillType ~= nil then
|
||||
return string.format(textOf("spu_status_act_fetch"), fillTitle(activity.fillType), place), "running"
|
||||
end
|
||||
return string.format(textOf("spu_status_act_driving"), place), "running"
|
||||
end
|
||||
|
||||
-- Fyld i % af hele vogntoget (AutoDrives egne opgørelser, som kildevalget bruger); nil uden kapacitet.
|
||||
local function fillPercent(adEnv, vehicle)
|
||||
local autoDrive = adEnv.AutoDrive
|
||||
if autoDrive == nil or autoDrive.getAllUnits == nil or autoDrive.getAllFillLevels == nil then return nil end
|
||||
local level, capacity = autoDrive.getAllFillLevels(autoDrive.getAllUnits(vehicle))
|
||||
if type(level) ~= "number" or type(capacity) ~= "number" or capacity <= 0 then return nil end
|
||||
return math.max(0, math.min(100, level / capacity * 100))
|
||||
end
|
||||
|
||||
local function farmVehiclesById(adEnv)
|
||||
local byId = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if id ~= nil then byId[id] = vehicle end
|
||||
end
|
||||
return byId
|
||||
end
|
||||
|
||||
-- Én linje pr. traktor i et kørende flow (flows der kan stoppes: startet, eller holder vogn/henter).
|
||||
function ADStatusHud.flowEntries(adEnv, proxy)
|
||||
local entries = {}
|
||||
if ADRunsController == nil or ADRunsController.store == nil or ADFlowView == nil then return entries end
|
||||
local byId = farmVehiclesById(adEnv)
|
||||
for _, run in ipairs(ADFlowView.orderedRuns(ADRunsController.store.runs or {})) do
|
||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||
if status.canStop then
|
||||
local runLevel = proxy ~= nil and proxy.flowLevel ~= nil and guarded("flowLevel", proxy.flowLevel, proxy, adEnv, run) or nil
|
||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||
local vehicle = byId[vehicleId]
|
||||
if vehicle ~= nil then
|
||||
local action, level = vehicleAction(adEnv, vehicle)
|
||||
-- farven følger flowets niveau som Flows-listen (venter/fejl), ellers traktorens egen handling
|
||||
if runLevel == "error" or runLevel == "waiting" then level = runLevel end
|
||||
table.insert(entries, {vehicle = tostring(ADRuns.vehicleLabel(vehicle)), action = action,
|
||||
fillPercent = guarded("fillPercent", fillPercent, adEnv, vehicle), level = level})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Én linje pr. mark med aktivt markarbejde (samme statustekst og farve som Marker-fanen).
|
||||
function ADStatusHud.fieldEntries(proxy)
|
||||
local entries = {}
|
||||
if ADFields == nil or ADFieldJobs == nil or ADFieldModel == nil or proxy == nil then return entries end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
if field ~= nil and (ADFieldModel.isWorking(field) or ADFieldJobs.awaitingPlayer(info.id) ~= nil) then
|
||||
local names = {}
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
table.insert(entries, {
|
||||
fieldId = info.id,
|
||||
work = proxy:fieldStatusText(field, info, true),
|
||||
vehicle = #names > 0 and ADFlowView.joinCounted(names, ", ", nil) or nil,
|
||||
progress = SmartPickupFrame.monthCpProgress ~= nil and guarded("fieldProgress", SmartPickupFrame.monthCpProgress, info.id) or nil,
|
||||
level = proxy:fieldLevel(info.id),
|
||||
})
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Kæder med manglende led + ventende forslag + slukkede nødvendige fabrikker (samme punkter og tekster som
|
||||
-- "Kræver handling"; de to sidste står under Advarsler).
|
||||
function ADStatusHud.chainEntries(proxy)
|
||||
local chains, suggestions = {}, {}
|
||||
local result = {chains = chains, suggestions = suggestions, dependencies = {}}
|
||||
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
|
||||
return result
|
||||
end
|
||||
for _, entry in ipairs(ADStorageStats.chainEntries(ADChain.last)) do
|
||||
if entry.kind == "chainProblem" then
|
||||
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
|
||||
elseif entry.kind == "chainSuggest" then
|
||||
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
|
||||
elseif entry.kind == "dependencyOff" then
|
||||
table.insert(result.dependencies, {text = proxy:alertText(entry), level = "error"})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Solgt/slettet køretøj (ADStuckDiag kan holde det et øjeblik efter salget).
|
||||
local function isGone(vehicle)
|
||||
if vehicle == nil or vehicle.isDeleted then return true end
|
||||
if vehicle.rootNode == nil then return true end
|
||||
return entityExists ~= nil and not entityExists(vehicle.rootNode)
|
||||
end
|
||||
|
||||
-- Gårdens køretøjer der har stået stille over grænsen, uden at Smart Pickup selv holder dem (Wait/udkørsel).
|
||||
function ADStatusHud.stuckEntries()
|
||||
local entries = {}
|
||||
if ADStuckDiag == nil or ADStuckDiag.stillVehicles == nil then return entries end
|
||||
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or nil
|
||||
for _, still in ipairs(ADStuckDiag.stillVehicles(ADStatusHud.STUCK_WARN_MS)) do
|
||||
local vehicle = still.vehicle
|
||||
local owned = not isGone(vehicle) and (farmId == nil or (vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId))
|
||||
local held = owned and ((ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) ~= nil)
|
||||
or (ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) ~= nil))
|
||||
if owned and not held then
|
||||
table.insert(entries, {text = string.format(textOf("spu_status_stuck"), tostring(ADRuns.vehicleLabel(vehicle)),
|
||||
math.floor(still.ms / 60000)), level = "error"})
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function ADStatusHud.snapshot(adEnv)
|
||||
local proxy = frameProxy()
|
||||
local snapshot = {flows = {}, fields = {}, chains = {}, warnings = {}}
|
||||
if adEnv ~= nil then
|
||||
snapshot.flows = guarded("flows", ADStatusHud.flowEntries, adEnv, proxy) or {}
|
||||
end
|
||||
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
|
||||
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
|
||||
snapshot.chains = chainResult.chains or {}
|
||||
for _, entry in ipairs(chainResult.dependencies or {}) do table.insert(snapshot.warnings, entry) end
|
||||
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
|
||||
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
|
||||
return snapshot
|
||||
end
|
||||
|
||||
-- ---- layout og cache (kun i update) ----
|
||||
|
||||
local function gameInfoDisplay()
|
||||
local hud = g_currentMission ~= nil and g_currentMission.hud or nil
|
||||
return hud ~= nil and hud.gameInfoDisplay or nil
|
||||
end
|
||||
|
||||
-- Størrelser i skærmkoordinater, skaleret som spillets egen HUD (Simple Inspector-mønster).
|
||||
local function measureUnits()
|
||||
local display = gameInfoDisplay()
|
||||
if display == nil or display.scalePixelToScreenHeight == nil or display.scalePixelToScreenVector == nil then return nil end
|
||||
local textSize = display:scalePixelToScreenHeight(ADStatusHud.TEXT_SIZE_PX)
|
||||
local marginX, marginY = display:scalePixelToScreenVector({ADStatusHud.MARGIN_PX, ADStatusHud.MARGIN_PX})
|
||||
local offsetX = display:scalePixelToScreenVector({ADStatusHud.SCREEN_OFFSET_PX, 0})
|
||||
return textSize, marginX, marginY, offsetX
|
||||
end
|
||||
|
||||
-- Øverste kant: fast under info-boksen; ligger info-boksens position lavere, rykkes vinduet ned under den.
|
||||
local function topY(marginY)
|
||||
local top = ADStatusHud.TOP_Y
|
||||
local display = gameInfoDisplay()
|
||||
if display ~= nil and display.getPosition ~= nil then
|
||||
local ok, _, y = pcall(display.getPosition, display)
|
||||
if ok and type(y) == "number" and y > 0.5 and y < 1 then top = math.min(top, y - marginY) end
|
||||
end
|
||||
return top
|
||||
end
|
||||
|
||||
function ADStatusHud.rebuild(adEnv)
|
||||
local lines = ADStatusView.lines(ADStatusHud.snapshot(adEnv), textOf, ADStatusView.MAX_LINES)
|
||||
local textSize, marginX, marginY, offsetX = measureUnits()
|
||||
if textSize == nil then
|
||||
-- spillets info-boks (skalering) findes ikke: vinduet kan ikke måles og tegnes
|
||||
logOnce("measure", "gameInfoDisplay mangler eller kan ikke skalere")
|
||||
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||
return
|
||||
end
|
||||
local maxWidth = 0
|
||||
for _, line in ipairs(lines) do
|
||||
setTextBold(line.header == true)
|
||||
line.width = getTextWidth(textSize, line.text)
|
||||
line.color = line.header and ADStatusHud.HEADER_COLOR or (ADFlowView.LEVEL_COLORS[line.level] or ADStatusHud.HEADER_COLOR)
|
||||
if line.width > maxWidth then maxWidth = line.width end
|
||||
end
|
||||
setTextBold(false)
|
||||
local width = maxWidth + 2 * marginX
|
||||
local height = #lines * textSize + 2 * marginY
|
||||
local top = topY(marginY)
|
||||
ADStatusHud.lines = lines
|
||||
ADStatusHud.layout = {x = 1 - offsetX - width, y = top - height, top = top, width = width, height = height,
|
||||
textSize = textSize, marginX = marginX, marginY = marginY, padding = marginX * 0.5}
|
||||
end
|
||||
|
||||
-- ---- overlays ----
|
||||
|
||||
local function createOverlays()
|
||||
if overlays ~= nil then return overlays end
|
||||
if g_overlayManager == nil or HUD == nil or HUD.COLOR == nil or HUD.COLOR.BACKGROUND == nil then return nil end
|
||||
local r, g, b, a = unpack(HUD.COLOR.BACKGROUND)
|
||||
local created = {
|
||||
middle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0),
|
||||
left = g_overlayManager:createOverlay("gui.gameInfo_left", 0, 0, 0, 0),
|
||||
right = g_overlayManager:createOverlay("gui.gameInfo_right", 0, 0, 0, 0),
|
||||
}
|
||||
for _, overlay in pairs(created) do overlay:setColor(r, g, b, a) end
|
||||
overlays = created
|
||||
return overlays
|
||||
end
|
||||
|
||||
local function deleteOverlays()
|
||||
if overlays == nil then return end
|
||||
for _, overlay in pairs(overlays) do
|
||||
if overlay.delete ~= nil then pcall(overlay.delete, overlay) end
|
||||
end
|
||||
overlays = nil
|
||||
end
|
||||
|
||||
-- ---- indstillinger (pr. bruger, ikke i savegamet) ----
|
||||
|
||||
local function settingsPath()
|
||||
if getUserProfileAppPath == nil then return nil end
|
||||
return getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR .. ADStatusHud.SETTINGS_FILE
|
||||
end
|
||||
|
||||
function ADStatusHud.loadSettings()
|
||||
settingsLoaded = true
|
||||
local path = settingsPath()
|
||||
if path == nil or fileExists == nil or not fileExists(path) then return end
|
||||
local xmlFile = XMLFile.load("ADSmartPickupStatus", path)
|
||||
if xmlFile == nil then return end
|
||||
ADStatusHud.visible = xmlFile:getBool(ADStatusHud.SETTINGS_ROOT .. "#visible", false) == true
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
function ADStatusHud.saveSettings()
|
||||
local path = settingsPath()
|
||||
if path == nil then return end
|
||||
if createFolder ~= nil then
|
||||
createFolder(getUserProfileAppPath() .. "modSettings/")
|
||||
createFolder(getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR)
|
||||
end
|
||||
local xmlFile = XMLFile.create("ADSmartPickupStatus", path, ADStatusHud.SETTINGS_ROOT)
|
||||
if xmlFile == nil then
|
||||
logOnce("saveSettings", "kunne ikke oprette " .. path)
|
||||
return
|
||||
end
|
||||
xmlFile:setBool(ADStatusHud.SETTINGS_ROOT .. "#visible", ADStatusHud.visible == true)
|
||||
xmlFile:save()
|
||||
xmlFile:delete()
|
||||
end
|
||||
|
||||
-- ---- offentlige kald fra moden ----
|
||||
|
||||
function ADStatusHud.toggle()
|
||||
ADStatusHud.visible = not ADStatusHud.visible
|
||||
timerMs = ADStatusHud.REFRESH_MS -- byg straks ved næste update
|
||||
if not ADStatusHud.visible then ADStatusHud.lines, ADStatusHud.layout = {}, nil end
|
||||
guarded("saveSettings", ADStatusHud.saveSettings)
|
||||
end
|
||||
|
||||
-- Action-event-callback (target = ADStatusHud).
|
||||
function ADStatusHud.onToggleAction()
|
||||
ADStatusHud.toggle()
|
||||
end
|
||||
|
||||
function ADStatusHud.update(dt, adEnv)
|
||||
if not ADStatusHud.isClient() then return end
|
||||
if not settingsLoaded then
|
||||
guarded("loadSettings", ADStatusHud.loadSettings)
|
||||
timerMs = ADStatusHud.REFRESH_MS
|
||||
end
|
||||
if not ADStatusHud.visible then return end
|
||||
timerMs = timerMs + (dt or 0)
|
||||
if timerMs < ADStatusHud.REFRESH_MS then return end
|
||||
timerMs = 0
|
||||
guarded("rebuild", ADStatusHud.rebuild, adEnv)
|
||||
end
|
||||
|
||||
-- Tegnes ikke ved åben menu/dialog, skjult HUD, søvn eller åben chat.
|
||||
local function shouldSkipDraw()
|
||||
if g_gui ~= nil and g_gui:getIsGuiVisible() then return true end
|
||||
if g_noHudModeEnabled then return true end
|
||||
local mission = g_currentMission
|
||||
local hud = mission ~= nil and mission.hud or nil
|
||||
if hud == nil or hud.isVisible == false then return true end
|
||||
if g_sleepManager ~= nil and g_sleepManager.getIsSleeping ~= nil and g_sleepManager:getIsSleeping() then return true end
|
||||
if hud.chatDisplay ~= nil and hud.chatDisplay.getVisible ~= nil and hud.chatDisplay:getVisible() then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
local function render(layout, bg)
|
||||
bg.middle:setDimension(layout.width, layout.height)
|
||||
bg.middle:setPosition(layout.x, layout.y)
|
||||
bg.middle:render()
|
||||
bg.left:setDimension(layout.padding, layout.height)
|
||||
bg.left:setPosition(layout.x - layout.padding, layout.y)
|
||||
bg.left:render()
|
||||
bg.right:setDimension(layout.padding, layout.height)
|
||||
bg.right:setPosition(layout.x + layout.width, layout.y)
|
||||
bg.right:render()
|
||||
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_TOP)
|
||||
local textX, textY = layout.x + layout.marginX, layout.top - layout.marginY
|
||||
for _, line in ipairs(ADStatusHud.lines) do
|
||||
local color = line.color
|
||||
setTextBold(line.header == true)
|
||||
setTextColor(color[1], color[2], color[3], color[4])
|
||||
renderText(textX, textY, layout.textSize, line.text)
|
||||
textY = textY - layout.textSize
|
||||
end
|
||||
end
|
||||
|
||||
function ADStatusHud.draw()
|
||||
if not ADStatusHud.visible or ADStatusHud.layout == nil or #ADStatusHud.lines == 0 then return end
|
||||
if shouldSkipDraw() then return end
|
||||
local ok0, bg = pcall(createOverlays)
|
||||
if not ok0 or bg == nil then
|
||||
-- uden baggrund tegnes intet (g_overlayManager/HUD.COLOR mangler eller createOverlay fejlede)
|
||||
logOnce("overlays", ok0 and "g_overlayManager eller HUD.COLOR.BACKGROUND mangler" or bg)
|
||||
return
|
||||
end
|
||||
local ok, err = pcall(render, ADStatusHud.layout, bg)
|
||||
-- tekststate nulstilles altid, også efter en fejl (andre mods/spillet tegner videre med den)
|
||||
setTextColor(1, 1, 1, 1)
|
||||
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)
|
||||
setTextBold(false)
|
||||
if not ok then logOnce("draw", err) end
|
||||
end
|
||||
|
||||
function ADStatusHud.delete()
|
||||
deleteOverlays()
|
||||
if PlayerInputComponent ~= nil and PlayerInputComponent.spuStatusToggleHandler == registerToggleActionRef then
|
||||
PlayerInputComponent.spuStatusToggleHandler = nil
|
||||
end
|
||||
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||
settingsLoaded = false
|
||||
timerMs = 0
|
||||
end
|
||||
|
||||
-- ---- tast: global spiller-handling (til fods og i køretøjer) ----
|
||||
|
||||
-- PlayerInputComponent.registerGlobalPlayerActionEvents kaldes ved hvert kontekstskift (til fods / i køretøj);
|
||||
-- samme mønster som FS25_Courseplay (CP_OPEN_INGAME_MENU) og FS25_EasyDevControls. Hjælpeteksten skjules i F1.
|
||||
local function registerToggleAction(component)
|
||||
if not ADStatusHud.isClient() then return end
|
||||
local player = component ~= nil and component.player or nil
|
||||
if player ~= nil and player.isOwner == false then return end
|
||||
if g_inputBinding == nil or InputAction == nil or InputAction.SPU_TOGGLE_STATUS == nil then return end
|
||||
local _, eventId = g_inputBinding:registerActionEvent(InputAction.SPU_TOGGLE_STATUS, ADStatusHud, ADStatusHud.onToggleAction,
|
||||
false, true, false, true)
|
||||
if eventId ~= nil then g_inputBinding:setActionEventTextVisibility(eventId, false) end
|
||||
end
|
||||
|
||||
registerToggleActionRef = function(component) guarded("registerAction", registerToggleAction, component) end
|
||||
|
||||
-- Spillets klasse overlever en genindlæsning af savegamet, moden gør ikke: krogen lægges kun én gang (markør på
|
||||
-- klassen) og kalder den aktuelle indlæsnings handler, så en gammel ADStatusHud aldrig registrerer tasten igen.
|
||||
function ADStatusHud.installInputHook()
|
||||
if PlayerInputComponent == nil or PlayerInputComponent.registerGlobalPlayerActionEvents == nil or Utils == nil then return false end
|
||||
PlayerInputComponent.spuStatusToggleHandler = registerToggleActionRef
|
||||
if PlayerInputComponent.spuStatusToggleHooked then return true end
|
||||
PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.appendedFunction(PlayerInputComponent.registerGlobalPlayerActionEvents,
|
||||
function(component, ...)
|
||||
local handler = PlayerInputComponent.spuStatusToggleHandler
|
||||
if handler ~= nil then handler(component) end
|
||||
end)
|
||||
PlayerInputComponent.spuStatusToggleHooked = true
|
||||
return true
|
||||
end
|
||||
|
||||
if PlayerInputComponent ~= nil and Utils ~= nil then
|
||||
ADStatusHud.installInputHook()
|
||||
end
|
||||
@@ -0,0 +1,143 @@
|
||||
-- AD Status View
|
||||
-- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet
|
||||
-- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med
|
||||
-- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, "..").
|
||||
--
|
||||
-- snapshot = {
|
||||
-- flows = {{vehicle, action, fillPercent (0-100|nil), level}},
|
||||
-- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}},
|
||||
-- chains = {{text, level}}, -- standard rød
|
||||
-- warnings = {{text, level}}, -- standard gul
|
||||
-- }
|
||||
-- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header".
|
||||
|
||||
ADStatusView = {}
|
||||
|
||||
ADStatusView.MAX_LINES = 18
|
||||
ADStatusView.MAX_CHARS = 48
|
||||
ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen
|
||||
ADStatusView.MIN_FLEX_CHARS = 6
|
||||
ADStatusView.SEPARATOR = " · "
|
||||
ADStatusView.ELLIPSIS = ".."
|
||||
|
||||
local LEVELS = {running = true, waiting = true, error = true, stopped = true}
|
||||
|
||||
local SECTIONS = {
|
||||
{key = "flows", header = "spu_status_flows", defaultLevel = "running"},
|
||||
{key = "fields", header = "spu_status_fields", defaultLevel = "running"},
|
||||
{key = "chains", header = "spu_status_chains", defaultLevel = "error"},
|
||||
{key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"},
|
||||
}
|
||||
|
||||
-- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ----
|
||||
|
||||
function ADStatusView.textLength(text)
|
||||
local count = 0
|
||||
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
local function utf8Prefix(text, chars)
|
||||
local result, count = {}, 0
|
||||
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||
if count >= chars then break end
|
||||
table.insert(result, char)
|
||||
count = count + 1
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
function ADStatusView.fit(text, chars)
|
||||
text = tostring(text or "")
|
||||
if ADStatusView.textLength(text) <= chars then return text end
|
||||
local ellipsis = ADStatusView.ELLIPSIS
|
||||
return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis
|
||||
end
|
||||
|
||||
-- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares.
|
||||
local function joinFitted(parts, flexIndex)
|
||||
local sep = ADStatusView.SEPARATOR
|
||||
local fixed = ADStatusView.textLength(sep) * (#parts - 1)
|
||||
for index, part in ipairs(parts) do
|
||||
if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end
|
||||
end
|
||||
local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed)
|
||||
local fitted = {}
|
||||
for index, part in ipairs(parts) do
|
||||
fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part
|
||||
end
|
||||
return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS)
|
||||
end
|
||||
|
||||
local function percentText(value)
|
||||
return string.format("%d %%", math.floor(value + 0.5))
|
||||
end
|
||||
|
||||
-- ---- linjer pr. sektion ----
|
||||
|
||||
local function flowText(entry)
|
||||
local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")}
|
||||
if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end
|
||||
return joinFitted(parts, 2)
|
||||
end
|
||||
|
||||
local function fieldText(entry)
|
||||
local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))}
|
||||
if entry.vehicle ~= nil and entry.vehicle ~= "" then
|
||||
table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS))
|
||||
end
|
||||
if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end
|
||||
return joinFitted(parts, 1)
|
||||
end
|
||||
|
||||
local function plainText(entry)
|
||||
return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS)
|
||||
end
|
||||
|
||||
local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText}
|
||||
|
||||
local function levelOf(entry, default)
|
||||
return LEVELS[entry.level] and entry.level or default
|
||||
end
|
||||
|
||||
-- Alle linjer uden loft.
|
||||
local function allLines(snapshot, textOf)
|
||||
local lines = {}
|
||||
for _, section in ipairs(SECTIONS) do
|
||||
local entries = snapshot[section.key] or {}
|
||||
if #entries > 0 then
|
||||
table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true})
|
||||
for _, entry in ipairs(entries) do
|
||||
table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false})
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden
|
||||
-- indhold lige før afkortningen droppes.
|
||||
local function capLines(lines, maxLines, textOf)
|
||||
if #lines <= maxLines then return lines end
|
||||
local kept = {}
|
||||
for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end
|
||||
while #kept > 0 and kept[#kept].header do table.remove(kept) end
|
||||
local hidden = 0
|
||||
for index = #kept + 1, #lines do
|
||||
if not lines[index].header then hidden = hidden + 1 end
|
||||
end
|
||||
local ok, text = pcall(string.format, textOf("spu_status_more"), hidden)
|
||||
table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false})
|
||||
return kept
|
||||
end
|
||||
|
||||
function ADStatusView.lines(snapshot, textOf, maxLines)
|
||||
snapshot = snapshot or {}
|
||||
textOf = textOf or function(key) return key end
|
||||
maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES))
|
||||
local lines = allLines(snapshot, textOf)
|
||||
if #lines == 0 then
|
||||
return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}}
|
||||
end
|
||||
return capLines(lines, maxLines, textOf)
|
||||
end
|
||||
@@ -258,15 +258,114 @@ function ADStorageStats.attentionList(stats, max)
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
|
||||
end
|
||||
return ADStorageStats.rankAttention(list, max)
|
||||
end
|
||||
|
||||
-- Kæde-forslag ligger ikke på en vare (ingen stat): navnet til den stabile rækkefølge tages fra teksten.
|
||||
local function entryName(entry)
|
||||
return tostring(entry.stat ~= nil and entry.stat.title or entry.text)
|
||||
end
|
||||
|
||||
function ADStorageStats.rankAttention(list, max)
|
||||
table.sort(list, function(a, b)
|
||||
if a.score ~= b.score then return a.score > b.score end
|
||||
return tostring(a.stat.title) < tostring(b.stat.title)
|
||||
return entryName(a) < entryName(b)
|
||||
end)
|
||||
local result = {}
|
||||
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
||||
return result
|
||||
end
|
||||
|
||||
-- "Kræver handling" fra kæden (ADChain.last): ÉT gult punkt "Kæde: N forslag" (klik -> fanen Kæder), så
|
||||
-- forslagene ikke fylder listen. Rangeres under alle røde punkter (>= 3000), øverst blandt de gule, over "sælg nu".
|
||||
ADStorageStats.CHAIN_SCORE = 2000
|
||||
-- Manglende led: ALLE kædeproblemer i ÉN rød post ("Kæder: N manglende led", klik -> fanen Kæder med kæderne
|
||||
-- foldet ud), så en gård med mange kæder (fx fermenteringssiloer) ikke fylder "Kræver handling". N = unikke
|
||||
-- (kæde, vare). Score CHAIN_PROBLEM_SCORE + alvor (0-99): under "rød fuld" (3000-3100), så lagerets røde poster
|
||||
-- altid vises først, og over de gule (<= 2000). Alvor = manglende andel af behovet; kredsløb = 99.
|
||||
ADStorageStats.CHAIN_PROBLEM_SCORE = 2900
|
||||
|
||||
local function severity(problems)
|
||||
local missing, need = 0, 0
|
||||
for _, problem in ipairs(problems) do
|
||||
if problem.kind == "cycle" then return 99 end
|
||||
missing, need = missing + (problem.detail or 0), need + (problem.need or 0)
|
||||
end
|
||||
if need <= 0 then return 99 end
|
||||
return math.floor(99 * math.min(1, missing / need))
|
||||
end
|
||||
|
||||
local function chainProblemEntries(last)
|
||||
local known = {}
|
||||
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do known[root.key] = true end
|
||||
local problems, seen, rootKeys, rootSeen = {}, {}, {}, {}
|
||||
for _, problem in ipairs(last.chain ~= nil and last.chain.problems or {}) do
|
||||
if problem.rootKey ~= nil and known[problem.rootKey] then
|
||||
table.insert(problems, problem)
|
||||
seen[tostring(problem.rootKey) .. "|" .. tostring(problem.fillType)] = true
|
||||
if not rootSeen[problem.rootKey] then
|
||||
rootSeen[problem.rootKey] = true
|
||||
table.insert(rootKeys, problem.rootKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #problems == 0 then return {} end
|
||||
local count = 0
|
||||
for _ in pairs(seen) do count = count + 1 end
|
||||
table.sort(rootKeys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
return {{kind = "chainProblem", level = "red", score = ADStorageStats.CHAIN_PROBLEM_SCORE + severity(problems), count = count,
|
||||
rootKeys = rootKeys, text = string.format("Kæder: %d", count)}}
|
||||
end
|
||||
|
||||
-- "Slukket men nødvendig" (ADChain.last.dependency.offNeeded): højst MAX_DEPENDENCY_ENTRIES post (den mest hastende
|
||||
-- fabrik; more = antal skjulte, vist som "+k"). Score DEPENDENCY_SCORE + hast (0-99, færre lagerdage = højere;
|
||||
-- ukendt = 50): over "rød fuld" (3000-3100), under "rød tom" (3900-4000).
|
||||
ADStorageStats.DEPENDENCY_SCORE = 3700
|
||||
ADStorageStats.MAX_DEPENDENCY_ENTRIES = 1
|
||||
|
||||
local function urgency(days)
|
||||
if days == nil then return 50 end
|
||||
if days == math.huge then return 0 end
|
||||
return math.max(0, 99 - math.min(99, math.floor(days)))
|
||||
end
|
||||
|
||||
function ADStorageStats.dependencyEntries(last)
|
||||
local entries = {}
|
||||
for _, item in ipairs(last ~= nil and last.dependency ~= nil and last.dependency.offNeeded or {}) do
|
||||
local name = tostring(item.buildingName or item.building or "?")
|
||||
table.insert(entries, {kind = "dependencyOff", level = "red", score = ADStorageStats.DEPENDENCY_SCORE + urgency(item.days),
|
||||
building = item.building, buildingName = name, fillType = item.fillType, onlyChain = item.onlyChain == true, text = name})
|
||||
end
|
||||
entries = ADStorageStats.rankAttention(entries)
|
||||
local result = {}
|
||||
for index = 1, math.min(#entries, ADStorageStats.MAX_DEPENDENCY_ENTRIES) do result[index] = entries[index] end
|
||||
if #entries > #result then result[#result].more = #entries - #result end
|
||||
return result
|
||||
end
|
||||
|
||||
function ADStorageStats.chainEntries(last)
|
||||
if last == nil then return {} end
|
||||
local result = chainProblemEntries(last)
|
||||
for _, entry in ipairs(ADStorageStats.dependencyEntries(last)) do table.insert(result, entry) end
|
||||
local count = 0
|
||||
for _, action in ipairs(last.actions or {}) do
|
||||
if action.mode == "suggest" then count = count + 1 end
|
||||
end
|
||||
if count > 0 then
|
||||
table.insert(result, {kind = "chainSuggest", level = "yellow", score = ADStorageStats.CHAIN_SCORE, count = count,
|
||||
text = string.format("Kæde: %d", count)})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Varepunkterne (allerede rangeret) + kædens punkter, samme loft.
|
||||
function ADStorageStats.mergeAttention(attention, chainEntries, max)
|
||||
local list = {}
|
||||
for _, entry in ipairs(attention or {}) do table.insert(list, entry) end
|
||||
for _, entry in ipairs(chainEntries or {}) do table.insert(list, entry) end
|
||||
return ADStorageStats.rankAttention(list, max)
|
||||
end
|
||||
|
||||
function ADStorageStats.kpis(stats)
|
||||
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
|
||||
for _, stat in ipairs(stats or {}) do
|
||||
|
||||
@@ -53,6 +53,16 @@ function ADStuckDiag.describe(vehicle)
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
-- Kun læsning (statusvinduet): køretøjer der har holdt stille mindst minMs -> {{vehicle, ms}}, længst først.
|
||||
function ADStuckDiag.stillVehicles(minMs)
|
||||
local list = {}
|
||||
for vehicle, ms in pairs(stillMs) do
|
||||
if ms >= (minMs or ADStuckDiag.STILL_MS) then table.insert(list, {vehicle = vehicle, ms = ms}) end
|
||||
end
|
||||
table.sort(list, function(a, b) return a.ms > b.ms end)
|
||||
return list
|
||||
end
|
||||
|
||||
function ADStuckDiag.tick(dt)
|
||||
timerMs = timerMs + (dt or 0)
|
||||
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
|
||||
|
||||
@@ -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
|
||||
@@ -426,6 +426,9 @@ function ADUnloadWait.getClaims(adEnv)
|
||||
return ADWaitPool.getClaims(adEnv)
|
||||
end
|
||||
|
||||
ADUnloadWait.TIP_CHECK_MS = 2000
|
||||
local tipChecked = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADUnloadWait.install(adEnv)
|
||||
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
||||
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
|
||||
@@ -438,6 +441,15 @@ function ADUnloadWait.install(adEnv)
|
||||
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
|
||||
|
||||
task.update = function(self, dt)
|
||||
-- alle aflæsningsture (også udkørsel og tømning): tipvognen på en side AD må aflæsse fra (EDK 650, 29/9)
|
||||
if ADTipSide ~= nil and self.vehicle ~= nil then
|
||||
local now = g_time or 0
|
||||
if (tipChecked[self.vehicle] or -math.huge) + ADUnloadWait.TIP_CHECK_MS <= now then
|
||||
tipChecked[self.vehicle] = now
|
||||
local okTip, errTip = pcall(ADTipSide.ensure, self.vehicle)
|
||||
if not okTip then Logging.warning(ADUnloadWait.LOG_PREFIX .. "tip-side: %s", tostring(errTip)) end
|
||||
end
|
||||
end
|
||||
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
|
||||
if not ok then
|
||||
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
-- Lagerfanen for en bygningstype (TAB_KINDS), fx "factory" -> fanen Fabrikker.
|
||||
function SmartPickupFrame.tabOfKind(kind)
|
||||
for tab, tabKind in pairs(SmartPickupFrame.TAB_KINDS or {}) do
|
||||
if tabKind == kind then return tab end
|
||||
end
|
||||
return SmartPickupFrame.TAB_OVERVIEW
|
||||
end
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
end
|
||||
@@ -62,6 +70,10 @@ end
|
||||
|
||||
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
|
||||
function SmartPickupFrame:attentionAdvice(entry)
|
||||
-- kæde-forslag og manglende led har ingen vare (stat): klik fører til fanen Kæder
|
||||
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then return "chain", nil end
|
||||
-- slukket men nødvendig: fanen Fabrikker (Kæder når kun en kæde bruger varen)
|
||||
if entry ~= nil and entry.kind == "dependencyOff" then return entry.onlyChain and "chain" or "factories", nil end
|
||||
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
|
||||
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
|
||||
local stat = entry.stat
|
||||
@@ -83,6 +95,8 @@ function SmartPickupFrame:attentionAdvice(entry)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:attentionHint(advice, info)
|
||||
if advice == "chain" then return self.i18n:getText("spu_attention_hintChain") end
|
||||
if advice == "factories" then return self.i18n:getText("spu_attention_hintFactories") end
|
||||
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
|
||||
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
|
||||
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
|
||||
@@ -91,6 +105,19 @@ end
|
||||
|
||||
function SmartPickupFrame:onAttentionClick(entry)
|
||||
self:safe(function(frame)
|
||||
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then
|
||||
-- manglende led: kæderne med problemer foldes ud, så problemteksten står under den røde linje
|
||||
if entry.kind == "chainProblem" then
|
||||
frame.chainExpanded = frame.chainExpanded or {}
|
||||
for _, rootKey in ipairs(entry.rootKeys or {}) do frame.chainExpanded[rootKey] = true end
|
||||
end
|
||||
frame:switchTab(SmartPickupFrame.TAB_CHAINS)
|
||||
return
|
||||
end
|
||||
if entry ~= nil and entry.kind == "dependencyOff" then
|
||||
frame:switchTab(entry.onlyChain and SmartPickupFrame.TAB_CHAINS or SmartPickupFrame.tabOfKind("factory"))
|
||||
return
|
||||
end
|
||||
local adEnv = frame.getAdEnv()
|
||||
local advice, info = frame:attentionAdvice(entry)
|
||||
if adEnv == nil or advice == "none" then return end
|
||||
|
||||
@@ -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"))
|
||||
for _, tractor in ipairs(tractors) do
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
||||
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg))
|
||||
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), ADFlowView.shortVehicleName(tractor.arg))
|
||||
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", 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)
|
||||
end
|
||||
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
||||
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
||||
for _, implement in ipairs(implements) do
|
||||
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg))
|
||||
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status))
|
||||
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), ADFlowView.shortVehicleName(implement.arg))
|
||||
self:addTextRow(string.format(" %s — %s: %s", ADFlowView.shortVehicleName(implement.label), self:roleText(implement.role), status))
|
||||
end
|
||||
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
||||
end
|
||||
|
||||
@@ -232,9 +232,7 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||
return
|
||||
end
|
||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
for _, run in ipairs(group.runs) do
|
||||
local function addFlowRow(run)
|
||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||
@@ -253,6 +251,22 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
|
||||
end)
|
||||
table.insert(self.flowListRows, entry)
|
||||
end
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
local runs, idle = group.runs, {}
|
||||
-- kædeflows uden traktor (venter på en ledig traktor/vogn) foldes sammen (in-game 30/9)
|
||||
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
|
||||
for _, run in ipairs(runs) do addFlowRow(run) end
|
||||
if #idle > 0 then
|
||||
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
|
||||
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
|
||||
frame.showIdleChainFlows = not frame.showIdleChainFlows
|
||||
frame:requestRebuild()
|
||||
end)
|
||||
if self.showIdleChainFlows then
|
||||
for _, run in ipairs(idle) do addFlowRow(run) end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:refreshFlowListRows(adEnv)
|
||||
end
|
||||
@@ -266,7 +280,7 @@ function SmartPickupFrame:refreshFlowListRows(adEnv)
|
||||
local cells = self:flowCells(adEnv, run)
|
||||
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||
entry.cells[1]:setText(tostring(run.name))
|
||||
entry.cells[1]:setText(ADFlowView.listName(run))
|
||||
entry.cells[2]:setText(tostring(cells[2]))
|
||||
entry.cells[3]:setText(tostring(cells[5]))
|
||||
entry.cells[4]:setText(tostring(cells[6]))
|
||||
|
||||
@@ -51,7 +51,7 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
||||
if row.kind == "outbound" then
|
||||
local sells, stores = ADFlowView.policyUse(row.policies)
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||
if sells then table.insert(parts, row.sellAtPercent == 0 and i18n:getText("spu_flow_ruleSellAlways") or string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
||||
rule = table.concat(parts, ", ")
|
||||
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
||||
@@ -64,7 +64,9 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
||||
elseif row.reason ~= nil then
|
||||
status = row.reason
|
||||
end
|
||||
local name = (run.id == self.selectedRunId and "> " or "") .. tostring(run.name)
|
||||
-- kædeflow uden traktor: sig hvorfor det står stille i stedet for flowets regel
|
||||
if ADFlowView.isIdleChain(run) then status = i18n:getText("spu_flow_chainIdle") end
|
||||
local name = (run.id == self.selectedRunId and "> " or "") .. ADFlowView.listName(run)
|
||||
return {name, products, from, to, string.format("%d/%d", row.running, row.total), status}
|
||||
end
|
||||
|
||||
@@ -103,9 +105,7 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_overview_hint"))
|
||||
self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")})
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||
for _, run in ipairs(group.runs) do
|
||||
local function addBoardRow(run)
|
||||
local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
local runId = run.id
|
||||
@@ -113,6 +113,22 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
|
||||
end
|
||||
table.insert(self.overviewRows, {runId = run.id, cells = cells})
|
||||
end
|
||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||
self:addTextRow(i18n:getText(group.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
|
||||
if #ADRunsController.store.runs == 0 then
|
||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||
|
||||
@@ -16,8 +16,19 @@ SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_
|
||||
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
||||
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
||||
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
||||
-- Kæder-fanen (gui/SmartPickupChainTab.lua): klikbare forslag/pause, så genopbygning sjældent (som Marksiloer)
|
||||
SmartPickupFrame.TAB_CHAINS = 10
|
||||
SmartPickupFrame.CHAIN_REBUILD_TICKS = 30
|
||||
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_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"}
|
||||
|
||||
function SmartPickupFrame.getTabKind(tab)
|
||||
@@ -96,8 +107,9 @@ function SmartPickupFrame:initialize()
|
||||
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
|
||||
self.selectorPrefab:unlinkElement()
|
||||
FocusManager:removeElement(self.selectorPrefab)
|
||||
for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do
|
||||
self.subCategoryPaging:addText(tostring(index))
|
||||
for position, index in ipairs(SmartPickupFrame.TAB_DISPLAY) do
|
||||
local key = SmartPickupFrame.TAB_NAMES[index]
|
||||
self.subCategoryPaging:addText(tostring(position))
|
||||
local tab = self.selectorPrefab:clone(self.subCategoryBox)
|
||||
FocusManager:loadElementFromCustomValues(tab)
|
||||
tab:setText(self.i18n:getText(key))
|
||||
@@ -267,11 +279,11 @@ function SmartPickupFrame:switchTab(tabIndex)
|
||||
end
|
||||
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
|
||||
self.currentTab = tabIndex
|
||||
for index, tab in ipairs(self.subCategoryTabs) do
|
||||
for index, tab in pairs(self.subCategoryTabs) do
|
||||
tab:setSelected(index == tabIndex)
|
||||
end
|
||||
if self.subCategoryPaging ~= nil then
|
||||
self.subCategoryPaging:setState(tabIndex, false)
|
||||
self.subCategoryPaging:setState(SmartPickupFrame.tabPosition(tabIndex), false)
|
||||
end
|
||||
self:requestRebuild()
|
||||
self:safe(function(frame)
|
||||
@@ -279,8 +291,9 @@ function SmartPickupFrame:switchTab(tabIndex)
|
||||
end)
|
||||
end
|
||||
|
||||
-- pilene/paging giver en position i visningsrækkefølgen
|
||||
function SmartPickupFrame:onClickTab(state)
|
||||
self:switchTab(state)
|
||||
self:switchTab(SmartPickupFrame.TAB_DISPLAY[state] or state)
|
||||
end
|
||||
|
||||
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
||||
@@ -466,6 +479,18 @@ function SmartPickupFrame:rebuild()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||
if adEnv ~= nil then
|
||||
self:fillChainRows(adEnv)
|
||||
end
|
||||
self.statusRow = self:addTextRow(self.statusOverride or "")
|
||||
self.monthRebuildTicks = SmartPickupFrame.CHAIN_REBUILD_TICKS
|
||||
self:colorRows()
|
||||
self.boxLayout:invalidateLayout()
|
||||
self:refreshButtons()
|
||||
self:restoreFocusIfLost()
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
||||
if adEnv ~= nil then
|
||||
self:fillMonthRows(adEnv)
|
||||
@@ -611,7 +636,8 @@ function SmartPickupFrame:refreshButtons()
|
||||
wanted = self.overviewButtons
|
||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||
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
|
||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||
wanted = self.storageButtons
|
||||
@@ -636,7 +662,14 @@ function SmartPickupFrame:refreshLive()
|
||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||
return
|
||||
end
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
|
||||
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||
-- "forslaget gælder ikke længere" står STATUS_OVERRIDE_TICKS sekunder
|
||||
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
|
||||
self:setStatusText(self.statusOverride or "")
|
||||
end
|
||||
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
||||
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
||||
self:refreshButtons()
|
||||
|
||||
@@ -183,8 +183,33 @@ function SmartPickupFrame:addKpiRow(stats)
|
||||
return row
|
||||
end
|
||||
|
||||
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet
|
||||
-- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS,
|
||||
-- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks.
|
||||
function SmartPickupFrame:attentionFor(stats)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then
|
||||
local okEnv, adEnv = pcall(self.getAdEnv)
|
||||
if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end
|
||||
end
|
||||
if ADChain == nil or ADChain.last == nil then return attention end
|
||||
return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION)
|
||||
end
|
||||
|
||||
function SmartPickupFrame:alertText(entry)
|
||||
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
||||
if entry.kind == "chainSuggest" then
|
||||
return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0)
|
||||
end
|
||||
if entry.kind == "chainProblem" then
|
||||
return string.format(i18n:getText("spu_attention_chainProblems"), entry.count or 0)
|
||||
end
|
||||
if entry.kind == "dependencyOff" then
|
||||
if (entry.more or 0) > 0 then
|
||||
return string.format(i18n:getText("spu_attention_depOffMore"), tostring(entry.buildingName), entry.more)
|
||||
end
|
||||
return string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName))
|
||||
end
|
||||
if entry.kind == "empty" then
|
||||
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
|
||||
elseif entry.kind == "full" then
|
||||
@@ -263,7 +288,7 @@ end
|
||||
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
|
||||
local i18n = self.i18n
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
local attention = self:attentionFor(stats)
|
||||
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
|
||||
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
|
||||
self:addTextRow(i18n:getText("spu_attention"))
|
||||
@@ -307,7 +332,7 @@ function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
|
||||
local view = self.itemView
|
||||
if view == nil then return end
|
||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||
local attention = self:attentionFor(stats)
|
||||
if self:itemStructureKey(stats, attention) ~= view.key then
|
||||
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
|
||||
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
|
||||
|
||||
@@ -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_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
||||
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_tooWideShort" text="kursus 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
||||
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
||||
@@ -167,6 +167,7 @@
|
||||
<text name="spu_field_careNeed" text="trænger til %s"/>
|
||||
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
||||
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
||||
<text name="spu_field_chainFree" text="Fri til kæden (moden må skifte afgrøde)"/>
|
||||
<text name="spu_field_mulch" text="Stubbehakning efter høst"/>
|
||||
<text name="spu_field_need_fert" text="gødning"/>
|
||||
<text name="spu_field_need_lime" text="kalk"/>
|
||||
@@ -309,6 +310,7 @@
|
||||
<text name="spu_policy_sell" text="Salg"/>
|
||||
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
|
||||
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
|
||||
<text name="spu_sellAt_always" text="Altid (uanset pris)"/>
|
||||
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
|
||||
<text name="spu_storeAbove" text="Kør til lager over"/>
|
||||
@@ -349,6 +351,8 @@
|
||||
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
||||
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
||||
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
||||
<text name="spu_flow_ruleSellAlways" text="sælg altid"/>
|
||||
<text name="spu_flow_chainIdle" text="mangler traktor/vogn"/>
|
||||
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
||||
@@ -376,6 +380,9 @@
|
||||
<text name="spu_col_to" text="Til"/>
|
||||
<text name="spu_col_tractors" text="Kører"/>
|
||||
<text name="spu_col_status" text="Status"/>
|
||||
<text name="spu_board_chainIdleShow" text="%d kædeflows venter på en ledig traktor/vogn — klik for at vise"/>
|
||||
<text name="spu_board_chainIdleHide" text="%d kædeflows uden traktor — klik for at skjule"/>
|
||||
<text name="spu_board_chain" text="· Kædernes egne flows (oprettes af fanen Kæder) ·"/>
|
||||
<text name="spu_board_supply" text="· Ind til bygning ·"/>
|
||||
<text name="spu_board_outbound" text="· Ud fra bygning ·"/>
|
||||
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
|
||||
@@ -513,6 +520,7 @@
|
||||
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
||||
<text name="spu_guide_flow_sumSellAlways" text="sælg altid"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
||||
@@ -574,5 +582,80 @@
|
||||
<text name="spu_item_fullIn" text="fuld om %s"/>
|
||||
<text name="spu_item_balanced" text="i balance"/>
|
||||
<text name="spu_item_share" text="%d %% af varen"/>
|
||||
<text name="spu_tab_chains" text="Kæder"/>
|
||||
<text name="spu_chain_noFlow" text="Intet flow kører den vare endnu — kæden opretter det, når der er en ledig traktor/vogn"/>
|
||||
<text name="spu_chain_optIn" text="[Kæde til]"/>
|
||||
<text name="spu_chain_optOut" text="[Fjern]"/>
|
||||
<text name="spu_chain_notChosen" text="ingen kæde"/>
|
||||
<text name="spu_chain_candidatesShow" text="%d tændte produktioner uden kæde — klik for at vise"/>
|
||||
<text name="spu_chain_candidatesHide" text="%d tændte produktioner uden kæde — klik for at skjule"/>
|
||||
<text name="spu_chain_finalFarm" text="Gård"/>
|
||||
<text name="spu_chain_finalSell" text="Sælg"/>
|
||||
<text name="spu_chain_finalKeep" text="Behold"/>
|
||||
<text name="spu_chain_suggestHeader" text="Forslag til ny afgrøde"/>
|
||||
<text name="spu_chain_empty" text="Ingen kæder: slå et produkt til i en fabrik"/>
|
||||
<text name="spu_chain_col_item" text="Vare"/>
|
||||
<text name="spu_chain_col_source" text="Kilde"/>
|
||||
<text name="spu_chain_col_perMonth" text="l/md"/>
|
||||
<text name="spu_chain_col_status" text="Status"/>
|
||||
<text name="spu_chain_col_stock" text="Lager"/>
|
||||
<text name="spu_chain_approve" text="Forslag [Godkend]"/>
|
||||
<text name="spu_chain_dismiss" text="[Afvis]"/>
|
||||
<text name="spu_chain_auto" text="skiftet (fri mark)"/>
|
||||
<text name="spu_chain_gone" text="Forslaget gælder ikke længere"/>
|
||||
<text name="spu_chain_flowOk" text="flow kører"/>
|
||||
<text name="spu_chain_flowMissing" text="mangler traktor/vogn"/>
|
||||
<text name="spu_chain_flowNone" text="intet flow"/>
|
||||
<text name="spu_chain_flowAuto" text="fordeles af spillet"/>
|
||||
<text name="spu_chain_flowPallet" text="palle: vælg UAL-vogn"/>
|
||||
<text name="spu_chain_flowStopped" text="flow stoppet"/>
|
||||
<text name="spu_chain_bottleneck" text="flaskehals: -%s/md"/>
|
||||
<text name="spu_chain_buy" text="skal købes: %s/md"/>
|
||||
<text name="spu_chain_cycle" text="kredsløb"/>
|
||||
<text name="spu_chain_fieldShort" text="mangler %s ha"/>
|
||||
<text name="spu_chain_tight" text="lager < 3 md"/>
|
||||
<text name="spu_chain_noField" text="ingen mark"/>
|
||||
<text name="spu_chain_buySource" text="køb"/>
|
||||
<text name="spu_chain_fromFields" text="fra marker"/>
|
||||
<text name="spu_chain_notifyMore" text="+%d manglende led — se fanen Kæder"/>
|
||||
<text name="spu_chain_pause" text="[Pause]"/>
|
||||
<text name="spu_chain_resume" text="[Start]"/>
|
||||
<text name="spu_chain_rootOk" text="dækket"/>
|
||||
<text name="spu_chain_rootError" text="mangel"/>
|
||||
<text name="spu_chain_rootWaiting" text="venter"/>
|
||||
<text name="spu_chain_rootPaused" text="på pause"/>
|
||||
<text name="spu_chain_rootSummary" text="%s · %d forslag"/>
|
||||
<text name="spu_chain_free" text="Marker fri til kæden: %s"/>
|
||||
<text name="spu_chain_switches" text="Automatiske skift siden indlæsning: %d"/>
|
||||
<text name="spu_chain_scanning" text="Marker scannes endnu – kæden skifter ingen marker før alle er set"/>
|
||||
<text name="spu_attention_chainSuggests" text="Kæde: %d forslag"/>
|
||||
<text name="spu_attention_hintChain" text=" → klik: se Kæder"/>
|
||||
<text name="spu_chain_problem_buy" text="Ingen af dine fabrikker laver %s — byg en eller køb"/>
|
||||
<text name="spu_chain_problem_barn" text="Dine stalde leverer ikke nok %s — køb resten eller flere dyr"/>
|
||||
<text name="spu_chain_problem_bottleneck" text="%s kan kun lave %s af %s/md"/>
|
||||
<text name="spu_chain_problem_off" text="%s (%s) er slukket af dig — tænd den eller sæt kæden på pause"/>
|
||||
<text name="spu_chain_problem_cycle" text="Kredsløb omkring %s — kæden kan ikke beregnes"/>
|
||||
<text name="spu_attention_chainProblems" text="Kæder: %d manglende led"/>
|
||||
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: statusvindue"/>
|
||||
<text name="spu_status_flows" text="Flows"/>
|
||||
<text name="spu_status_fields" text="Marker"/>
|
||||
<text name="spu_status_chains" text="Kæder"/>
|
||||
<text name="spu_status_warnings" text="Advarsler"/>
|
||||
<text name="spu_status_more" text="+%d mere"/>
|
||||
<text name="spu_status_idle" text="Smart Pickup: intet kører lige nu"/>
|
||||
<text name="spu_status_act_fetch" text="henter %s ved %s"/>
|
||||
<text name="spu_status_act_driving" text="kører til %s"/>
|
||||
<text name="spu_status_act_waiting" text="venter ved %s"/>
|
||||
<text name="spu_status_act_holding" text="holder ved %s"/>
|
||||
<text name="spu_status_act_fetchWagon" text="henter vogn"/>
|
||||
<text name="spu_status_act_waitWagon" text="venter på vogn"/>
|
||||
<text name="spu_status_act_emptying" text="tømmer vognen"/>
|
||||
<text name="spu_status_act_homing" text="på vej hjem"/>
|
||||
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
|
||||
<text name="spu_dep_warn" text="Advarsel: du slukkede %s (%s) — %s bruger det. Lageret rækker ca. %d dage."/>
|
||||
<text name="spu_dep_warnNoDays" text="Advarsel: du slukkede %s (%s) — %s bruger det."/>
|
||||
<text name="spu_attention_depOff" text="Slukket men nødvendig: %s"/>
|
||||
<text name="spu_attention_depOffMore" text="Slukket men nødvendig: %s +%d"/>
|
||||
<text name="spu_attention_hintFactories" text=" → klik: se Fabrikker"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -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_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
||||
<text name="spu_field_rig_working" text="working on F%s"/>
|
||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
||||
<text name="spu_field_tooWideShort" text="course 50 m"/>
|
||||
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
||||
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
||||
@@ -167,6 +167,7 @@
|
||||
<text name="spu_field_careNeed" text="needs %s"/>
|
||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
||||
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
||||
<text name="spu_field_chainFree" text="Free for chains (mod may change the crop)"/>
|
||||
<text name="spu_field_mulch" text="Mulch stubble after harvest"/>
|
||||
<text name="spu_field_need_fert" text="fertilizer"/>
|
||||
<text name="spu_field_need_lime" text="lime"/>
|
||||
@@ -309,6 +310,7 @@
|
||||
<text name="spu_policy_sell" text="Sell"/>
|
||||
<text name="spu_policy_sellElseStore" text="Sell, else store"/>
|
||||
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
|
||||
<text name="spu_sellAt_always" text="Always (any price)"/>
|
||||
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
|
||||
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
|
||||
<text name="spu_storeAbove" text="Drive to storage above"/>
|
||||
@@ -349,6 +351,8 @@
|
||||
<text name="spu_flow_noSource" text="(no source)"/>
|
||||
<text name="spu_flow_noTarget" text="(no target)"/>
|
||||
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
||||
<text name="spu_flow_ruleSellAlways" text="always sell"/>
|
||||
<text name="spu_flow_chainIdle" text="needs a tractor/trailer"/>
|
||||
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
||||
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
||||
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
||||
@@ -376,6 +380,9 @@
|
||||
<text name="spu_col_to" text="To"/>
|
||||
<text name="spu_col_tractors" text="Running"/>
|
||||
<text name="spu_col_status" text="Status"/>
|
||||
<text name="spu_board_chainIdleShow" text="%d chain flows are waiting for a free tractor/trailer — click to show"/>
|
||||
<text name="spu_board_chainIdleHide" text="%d chain flows without a tractor — click to hide"/>
|
||||
<text name="spu_board_chain" text="· Flows created by chains (Chains tab) ·"/>
|
||||
<text name="spu_board_supply" text="· Into building ·"/>
|
||||
<text name="spu_board_outbound" text="· Out of building ·"/>
|
||||
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
|
||||
@@ -513,6 +520,7 @@
|
||||
<text name="spu_guide_flow_none" text="none chosen"/>
|
||||
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
||||
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
||||
<text name="spu_guide_flow_sumSellAlways" text="always sell"/>
|
||||
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
||||
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
||||
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
||||
@@ -574,5 +582,80 @@
|
||||
<text name="spu_item_fullIn" text="full in %s"/>
|
||||
<text name="spu_item_balanced" text="balanced"/>
|
||||
<text name="spu_item_share" text="%d %% of the good"/>
|
||||
<text name="spu_tab_chains" text="Chains"/>
|
||||
<text name="spu_chain_noFlow" text="No flow carries that good yet — the chain creates one when a tractor/trailer is free"/>
|
||||
<text name="spu_chain_optIn" text="[Chain on]"/>
|
||||
<text name="spu_chain_optOut" text="[Remove]"/>
|
||||
<text name="spu_chain_notChosen" text="no chain"/>
|
||||
<text name="spu_chain_candidatesShow" text="%d productions on without a chain — click to show"/>
|
||||
<text name="spu_chain_candidatesHide" text="%d productions on without a chain — click to hide"/>
|
||||
<text name="spu_chain_finalFarm" text="Farm"/>
|
||||
<text name="spu_chain_finalSell" text="Sell"/>
|
||||
<text name="spu_chain_finalKeep" text="Keep"/>
|
||||
<text name="spu_chain_suggestHeader" text="Crop change suggestions"/>
|
||||
<text name="spu_chain_empty" text="No chains: enable a product in a factory"/>
|
||||
<text name="spu_chain_col_item" text="Goods"/>
|
||||
<text name="spu_chain_col_source" text="Source"/>
|
||||
<text name="spu_chain_col_perMonth" text="l/mo"/>
|
||||
<text name="spu_chain_col_status" text="Status"/>
|
||||
<text name="spu_chain_col_stock" text="Stock"/>
|
||||
<text name="spu_chain_approve" text="Suggest [Approve]"/>
|
||||
<text name="spu_chain_dismiss" text="[Reject]"/>
|
||||
<text name="spu_chain_auto" text="switched (free field)"/>
|
||||
<text name="spu_chain_gone" text="The suggestion no longer applies"/>
|
||||
<text name="spu_chain_flowOk" text="flow running"/>
|
||||
<text name="spu_chain_flowMissing" text="no tractor/trailer"/>
|
||||
<text name="spu_chain_flowNone" text="no flow"/>
|
||||
<text name="spu_chain_flowAuto" text="distributed by game"/>
|
||||
<text name="spu_chain_flowPallet" text="pallet: UAL trailer"/>
|
||||
<text name="spu_chain_flowStopped" text="flow stopped"/>
|
||||
<text name="spu_chain_bottleneck" text="bottleneck: -%s/mo"/>
|
||||
<text name="spu_chain_buy" text="must buy: %s/mo"/>
|
||||
<text name="spu_chain_cycle" text="loop"/>
|
||||
<text name="spu_chain_fieldShort" text="short %s ha"/>
|
||||
<text name="spu_chain_tight" text="stock < 3 mo"/>
|
||||
<text name="spu_chain_noField" text="no field"/>
|
||||
<text name="spu_chain_buySource" text="buy"/>
|
||||
<text name="spu_chain_fromFields" text="from fields"/>
|
||||
<text name="spu_chain_notifyMore" text="+%d more missing links — see the Chains tab"/>
|
||||
<text name="spu_chain_pause" text="[Pause]"/>
|
||||
<text name="spu_chain_resume" text="[Resume]"/>
|
||||
<text name="spu_chain_rootOk" text="covered"/>
|
||||
<text name="spu_chain_rootError" text="short"/>
|
||||
<text name="spu_chain_rootWaiting" text="waiting"/>
|
||||
<text name="spu_chain_rootPaused" text="paused"/>
|
||||
<text name="spu_chain_rootSummary" text="%s · %d sugg."/>
|
||||
<text name="spu_chain_free" text="Fields free for the chain: %s"/>
|
||||
<text name="spu_chain_switches" text="Automatic switches since load: %d"/>
|
||||
<text name="spu_chain_scanning" text="Fields still being scanned – no field changes until all are seen"/>
|
||||
<text name="spu_attention_chainSuggests" text="Chain: %d suggestions"/>
|
||||
<text name="spu_attention_hintChain" text=" → click: see Chains"/>
|
||||
<text name="spu_chain_problem_buy" text="None of your factories makes %s — build one or buy it"/>
|
||||
<text name="spu_chain_problem_barn" text="Barns make too little %s — buy the rest or more animals"/>
|
||||
<text name="spu_chain_problem_bottleneck" text="%s can only make %s of %s/mo"/>
|
||||
<text name="spu_chain_problem_off" text="%s (%s) was turned off by you — turn it on or pause the chain"/>
|
||||
<text name="spu_chain_problem_cycle" text="Loop around %s — the chain cannot be calculated"/>
|
||||
<text name="spu_attention_chainProblems" text="Chains: %d missing links"/>
|
||||
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: status window"/>
|
||||
<text name="spu_status_flows" text="Flows"/>
|
||||
<text name="spu_status_fields" text="Fields"/>
|
||||
<text name="spu_status_chains" text="Chains"/>
|
||||
<text name="spu_status_warnings" text="Warnings"/>
|
||||
<text name="spu_status_more" text="+%d more"/>
|
||||
<text name="spu_status_idle" text="Smart Pickup: nothing running right now"/>
|
||||
<text name="spu_status_act_fetch" text="fetching %s at %s"/>
|
||||
<text name="spu_status_act_driving" text="driving to %s"/>
|
||||
<text name="spu_status_act_waiting" text="waiting at %s"/>
|
||||
<text name="spu_status_act_holding" text="holding at %s"/>
|
||||
<text name="spu_status_act_fetchWagon" text="fetching wagon"/>
|
||||
<text name="spu_status_act_waitWagon" text="waiting for wagon"/>
|
||||
<text name="spu_status_act_emptying" text="emptying the wagon"/>
|
||||
<text name="spu_status_act_homing" text="heading home"/>
|
||||
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
|
||||
<text name="spu_dep_warn" text="Warning: you switched off %s (%s) — %s uses it. Stock lasts about %d days."/>
|
||||
<text name="spu_dep_warnNoDays" text="Warning: you switched off %s (%s) — %s uses it."/>
|
||||
<text name="spu_attention_depOff" text="Switched off but needed: %s"/>
|
||||
<text name="spu_attention_depOffMore" text="Switched off but needed: %s +%d"/>
|
||||
<text name="spu_attention_hintFactories" text=" → click: see Factories"/>
|
||||
</texts>
|
||||
</l10n>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.60.14.0</version>
|
||||
<version>1.62.7.0</version>
|
||||
<title>
|
||||
<en>AD Smart Pickup</en>
|
||||
</title>
|
||||
@@ -31,6 +31,9 @@
|
||||
<sourceFile filename="adSources.lua"/>
|
||||
<sourceFile filename="adBuildings.lua"/>
|
||||
<sourceFile filename="adOutbound.lua"/>
|
||||
<sourceFile filename="adTipSide.lua"/>
|
||||
<sourceFile filename="adLoadDiag.lua"/>
|
||||
<sourceFile filename="adSellBlock.lua"/>
|
||||
<sourceFile filename="adUnloadWait.lua"/>
|
||||
<sourceFile filename="adFieldModel.lua"/>
|
||||
<sourceFile filename="adFieldFleet.lua"/>
|
||||
@@ -54,6 +57,25 @@
|
||||
<sourceFile filename="adFieldJobs.lua"/>
|
||||
<sourceFile filename="adMonthView.lua"/>
|
||||
<sourceFile filename="adProfiler.lua"/>
|
||||
<sourceFile filename="adChainModel.lua"/>
|
||||
<sourceFile filename="adCropPlan.lua"/>
|
||||
<sourceFile filename="adChainActions.lua"/>
|
||||
<sourceFile filename="adDependency.lua"/>
|
||||
<sourceFile filename="adChainData.lua"/>
|
||||
<sourceFile filename="adChain.lua"/>
|
||||
<sourceFile filename="adChainRun.lua"/>
|
||||
<sourceFile filename="adChainView.lua"/>
|
||||
<sourceFile filename="adStatusView.lua"/>
|
||||
<sourceFile filename="adStatusHud.lua"/>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
<actions>
|
||||
<action name="SPU_TOGGLE_STATUS"/>
|
||||
</actions>
|
||||
<inputBinding>
|
||||
<actionBinding action="SPU_TOGGLE_STATUS">
|
||||
<!-- numpad * (Insert bruges af FS25_UniversalAutoload) -->
|
||||
<binding device="KB_MOUSE_DEFAULT" input="KEY_KP_multiply"/>
|
||||
</actionBinding>
|
||||
</inputBinding>
|
||||
</modDesc>
|
||||
|
||||
@@ -16,11 +16,16 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
|
||||
startes eller stoppes samlet fra en farvet liste.
|
||||
- **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og
|
||||
sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje.
|
||||
- **Produktionskæder:** slå et produkt til i en fabrik, og moden holder det kørende tilbage til marken. Fanen Kæder viser træet, marker markeret "Fri til kæden" skifter afgrøde automatisk, og andre marker får forslag, du godkender eller afviser. Mellemled og flows oprettes efter behov.
|
||||
- **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen,
|
||||
og Courseplay arbejder på marken.
|
||||
- **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre.
|
||||
- **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter
|
||||
det flow, der løser problemet.
|
||||
- **Statusvindue:** numpad \* viser et lille vindue øverst til højre med flow-traktorernes handling og fyld,
|
||||
markarbejde i gang, kæder med manglende led og advarsler.
|
||||
- **Advarsel når en nødvendig fabrik slukkes:** slukker du en produktion, som et flow, en stald eller en kæde
|
||||
bruger, advarer moden og viser hvor længe lageret rækker.
|
||||
- **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes
|
||||
med savegamet.
|
||||
|
||||
@@ -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.
|
||||
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
|
||||
marker and park it again. Detached implements and free tractors form a shared pool.
|
||||
- **Production chains:** switch on a product in a factory and the mod keeps it running back to the field. The Chains tab shows the tree, fields marked "Free for chains" change crop automatically, and other fields get suggestions you approve or reject. Intermediate goods and flows are set up as needed.
|
||||
- **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive
|
||||
drives the roads and Courseplay works the field.
|
||||
- **Field silos:** goods in field silos are brought home to the farm's storages automatically.
|
||||
- **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list
|
||||
creates the flow that fixes the problem.
|
||||
- **Status window:** numpad \* shows a small window in the top right with the flow tractors' action and
|
||||
fill, field work in progress, chains with missing links and warnings.
|
||||
- **Warning when a needed factory is switched off:** switch off a production that a flow, a barn or a chain
|
||||
uses, and the mod warns you and shows how long the stock lasts.
|
||||
- **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved
|
||||
with the savegame.
|
||||
|
||||
**Install:** download `FS25_ADSmartPickup.zip` from
|
||||
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
|
||||
in `Documents/My Games/FarmingSimulator2025/mods/`.
|
||||
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
|
||||
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
|
||||
`ADSmartPickup`.
|
||||
|
||||
**Development:** the mod code is in `FS25_ADSmartPickup/`. Build the zip with
|
||||
`cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||
|
||||
+187
-5
@@ -1,6 +1,6 @@
|
||||
# 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)*
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et **fast sæt** er et
|
||||
redskab, der ikke har en Connect-markør. Det bliver siddende og udlånes aldrig.
|
||||
**Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et redskab eller en
|
||||
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
|
||||
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,
|
||||
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>"
|
||||
|
||||
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å.
|
||||
- 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"
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
@@ -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
|
||||
|
||||
**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) |
|
||||
| *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 |
|
||||
| *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:
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
---
|
||||
@@ -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å |
|
||||
| **Afkobl-markør** | Markør, hvor redskaber bag traktoren kobles af |
|
||||
| **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 |
|
||||
| **Markflåde** | De maskiner, markarbejdet må bruge |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **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
@@ -1,6 +1,6 @@
|
||||
# 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)*
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Set.** A tractor with an implement or trailer attached, or a self-propelled machine. A **fixed set** is
|
||||
an implement that has no Connect marker. It stays attached and is never lent out.
|
||||
**Set.** A tractor with an implement or trailer attached, or a self-propelled machine. An implement or
|
||||
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
|
||||
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
|
||||
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>"
|
||||
|
||||
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 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"
|
||||
|
||||
@@ -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
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
- **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
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
**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) |
|
||||
| *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 |
|
||||
| *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:
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
---
|
||||
@@ -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 |
|
||||
| **Afkobl marker** | Marker where implements behind the tractor are detached |
|
||||
| **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 |
|
||||
| **Field fleet** | The machines field work is allowed to use |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **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.
@@ -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])
|
||||
@@ -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})")
|
||||
@@ -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))
|
||||
@@ -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)}")
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user