22 Commits

Author SHA1 Message Date
masterdraco bb287a8154 feat: spuSellBlock slår salgssteder fra/til pr. savegame 2026-09-30 23:35:01 +02:00
masterdraco e229a27624 fix: pålæsningsdiagnose kun ved stationen (ikke Wait/mark) 2026-09-30 23:28:58 +02:00
masterdraco ba98cf83d3 docs: parkering og hentning kun ved at bakke 2026-09-30 23:28:08 +02:00
masterdraco a6662d6da1 feat: diagnose når AD står ved LOAD-mål uden at fylde 2026-09-30 22:54:28 +02:00
masterdraco bf3b99aca4 fix: kald ikke AutoDrives getMarkerAtStation (opretter ekstra stationsmarkører) 2026-09-30 22:18:16 +02:00
masterdraco cbc9b2c8e3 fix: påfyldningsmarkør ved stationens egne læssetriggere (langt sæt fik anden stations markør) 2026-09-30 22:15:32 +02:00
masterdraco 1bda82a270 feat: kæder respekterer fabrikkens udgang (Fordel = intet flow, Sælg på mellemvare -> Behold) 2026-09-30 22:01:42 +02:00
masterdraco 21b48d7664 fix: objektlager tæller alle grupper af varen (Sell Egg stoppede halvfuld) 2026-09-30 21:55:01 +02:00
masterdraco 88ddb3d6cd feat(kæde): kæder slås selv til ([Kæde til]/[Fjern]); tomme kædeflows slettes; runsView med traktorer (v1.62.0.0) 2026-09-30 21:43:30 +02:00
masterdraco 37cf84c101 fix(udkørsel): hold AD's pålæsning mens palleskuret udleverer paller (v1.61.13.0) 2026-09-30 21:35:06 +02:00
masterdraco 991252848f fix(oversigt): kædeflows uden traktor foldet sammen, 'mangler traktor/vogn' som status (v1.61.12.0) 2026-09-30 21:30:45 +02:00
masterdraco 6b533e62eb fix(bygninger): markørnavn afgør mellem tætte markører (Pickup til pålæsning, Unload til aflæsning) (v1.61.11.0) 2026-09-30 18:19:31 +02:00
masterdraco c92c870297 fix(flåde): korte maskinnavne i puljelisten (v1.61.10.0) 2026-09-30 18:16:19 +02:00
masterdraco 18b6c82871 feat(udkørsel): 'Altid (uanset pris)' i Sælg fra (v1.61.9.0) 2026-09-30 18:06:51 +02:00
masterdraco 47afdebf64 feat(kæde): læs i køretøjer tæller som lager, ingen skift under høst, varelinje åbner flow (v1.61.8.0) 2026-09-30 18:03:56 +02:00
masterdraco 2290bdde95 fix(kæde): 'Gård' ved slutprodukter gården selv bruger; Sælg/Behold kun ved salgbare (v1.61.7.0) 2026-09-30 17:55:10 +02:00
masterdraco 0c21ba49a0 fix(kæde): slutprodukter gården selv bruger sælges aldrig straks; tidligere salg sættes tilbage (v1.61.6.0) 2026-09-30 17:53:56 +02:00
masterdraco e1f7b19fef feat(marker): redskaber over 50 m bruges, CP-kursus begrænses til 50 m (v1.61.5.0) 2026-09-30 17:47:34 +02:00
masterdraco 74537f7ec2 feat(kæde): ét flow pr. bygning, slutprodukter sælges straks (Sælg/Behold), tomme kædeflows foldes sammen; såmaskine ikke gødningsspreder (v1.61.4.0) 2026-09-30 17:44:06 +02:00
masterdraco d2970a1bda feat(kæde): forslag samlet øverst uden dubletter; kæder sorteret efter status, bygning, produkt (v1.61.3.0) 2026-09-30 17:16:05 +02:00
masterdraco bd106a2176 fix(flows): egne flows øverst, kædeflows samlet nederst uden 'Kæde: ' (v1.61.2.0) 2026-09-30 17:14:08 +02:00
masterdraco acf94c8e88 fix(gui): fanen Kæder efter Marker; kædelistens navne afkortes ikke (v1.61.1.0) 2026-09-30 16:32:29 +02:00
37 changed files with 1249 additions and 116 deletions
+25
View File
@@ -6,8 +6,33 @@ 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
+44 -2
View File
@@ -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
+84 -14
View File
@@ -17,7 +17,7 @@ ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
ADChain.DISMISS_CHANGE = 0.25
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}}
ADChain.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
@@ -53,12 +53,16 @@ end
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
function ADChain.newState()
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil}
-- 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}
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)
@@ -75,6 +79,9 @@ function ADChain.stateFromRecords(records)
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
@@ -119,12 +126,15 @@ end
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
function ADChain.modelRecipes(recipes, paused, userOff)
-- 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
@@ -234,12 +244,13 @@ function ADChain.stillNeeded(runView, edges)
return nil
end
-- {[forbruger|vare] = "ok"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
-- {[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
local status = "missing"
for _, run in ipairs(runs 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]
@@ -259,7 +270,9 @@ function ADChain.runsView(runs)
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})
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
@@ -289,12 +302,12 @@ local function farmId()
end
local function plan(data, state)
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff)
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), data.husbandry, data.crops, data.fieldGoods).needRecipes
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
@@ -302,11 +315,11 @@ local function plan(data, state)
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))
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)}
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
@@ -378,7 +391,8 @@ function ADChain.evaluate(adEnv, act)
for key in pairs(state.paused) do paused[key] = true end
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
paused = paused, userOff = copySet(state.userOff), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
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
@@ -491,6 +505,44 @@ function ADChain.dismiss(fieldId)
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
@@ -520,6 +572,15 @@ function ADChain.save()
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
@@ -547,7 +608,16 @@ function ADChain.load()
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 = {}}
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)
+168 -5
View File
@@ -17,14 +17,53 @@ end
local SUPPLIED = {production = true, husbandry = true, field = true}
function ADChainActions.edges(chain)
local list, seen = {}, {}
-- 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
table.insert(list, {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey})
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
@@ -51,7 +90,24 @@ end
function ADChainActions.missingFlows(edges, runs)
local list = {}
for _, edge in ipairs(edges or {}) do
if not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
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
@@ -59,9 +115,116 @@ end
function ADChainActions.flowsToStop(runs, activeRootKeys)
local list = {}
for _, run in ipairs(runs or {}) do
if run.chain ~= nil and run.chain ~= "" and not run.stopped and not (activeRootKeys or {})[run.chain] then
-- 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
+61 -3
View File
@@ -92,6 +92,28 @@ 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
@@ -102,8 +124,9 @@ local function readFactory(placeable, point, byChain, nameOf)
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf)})
points[key] = {point = point, productionId = production.id, placeable = placeable}
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
@@ -267,6 +290,38 @@ local function readFieldGoods(data, nameOf)
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
@@ -324,6 +379,8 @@ local function readFields(data)
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"}
@@ -439,13 +496,14 @@ end
-- 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 = {}}
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)
+1 -1
View File
@@ -59,7 +59,7 @@ function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
for _, recipe in ipairs(recipes or {}) do
capacity[recipe.key] = recipe.cyclesPerMonth or 0
if recipe.enabled and not recipe.byChain then
if not recipe.paused then table.insert(roots, recipe) end
if not recipe.paused and not recipe.notChosen then table.insert(roots, recipe) end
standalone[recipe.key] = true
end
end
+168 -23
View File
@@ -125,26 +125,15 @@ local function createStopped(run, kind, reason, ...)
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
end
local function createFlow(adEnv, data, edge, pool, used)
local title = data.fillTitles[edge.fillType] or edge.fillType
local created = ADRunsController.createRun(ADChain.flowName(title, edge.consumerName or edge.consumer), "supply")
local runId = created.id
ADRunsController.updateRun(runId, {targetBuildingIds = {edge.consumer}, ingredientMode = "manual", ingredients = {edge.fillType},
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, edge.consumer), chain = edge.rootKey})
-- updateRun erstatter store'en: slå altid run'et op igen
local run = ADRuns.getRun(ADRunsController.store, runId)
local fillIndex = data.fillIndex[edge.fillType]
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
return
end
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
if pair == nil then
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
return
-- É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)
@@ -159,18 +148,158 @@ local function createFlow(adEnv, data, edge, pool, used)
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 missing = ADChainActions.missingFlows(edges, ADChain.runsView(ADRunsController.store.runs))
local runs = ADChain.runsView(ADRunsController.store.runs)
local missing = ADChainActions.missingFlows(edges, runs)
local pool, used, count = {}, {}, 0
for _, edge in ipairs(missing) do
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
local ok, err = pcall(createFlow, adEnv, data, edge, pool, used)
if not ok then warn("flow til %s (%s) kunne ikke oprettes: %s", tostring(edge.consumerName), tostring(edge.fillType), tostring(err)) end
end
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)
@@ -182,6 +311,15 @@ function ADChainRun.stopFlows(adEnv, chain, edges)
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
@@ -202,7 +340,14 @@ function ADChainRun.apply(adEnv, data, result)
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
+76 -15
View File
@@ -13,23 +13,23 @@ ADChainView = {}
ADChainView.INDENT = " "
-- Tegnbudget pr. celle i flowListPrefab (gui/SmartPickupFrame.xml: textMaxWidth 192/162/62/244/92 px).
-- Antaget ~11,5 px pr. tegn (spuInfoText: 800 px ~ 70 tegn), fed ~12,5 px; c3 har kun tal (~10 px pr. tegn).
ADChainView.CELL_CHARS = {15, 14, 6, 21, 8}
ADChainView.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_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
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_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
"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"}
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
@@ -129,7 +129,7 @@ function ADChainView.rootLevel(root, last)
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
if (status ~= nil and status ~= "ok") or isTight(node, last, short) then hasWaiting = true end
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
@@ -218,6 +218,8 @@ local function nodeStatus(node, last, fmt, textOf, short)
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
@@ -270,6 +272,39 @@ local function actionsFor(last, fruit, modes)
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
@@ -280,13 +315,16 @@ local function rootRows(rows, root, last, ctx, isExpanded)
local status = ctx.textOf(ROOT_STATUS[level])
if #suggestions > 0 then status = string.format(ctx.textOf("spu_chain_rootSummary"), status, #suggestions) end
local marker = (#(root.children or {}) > 0) and (isExpanded and "– " or "+ ") or ""
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""), "", status,
ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")},
{[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}, root.key))
if not isExpanded then
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 1, ctx.titleOf, ctx.textOf)) end
return
end
-- 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
@@ -307,11 +345,13 @@ local function rootRows(rows, root, last, ctx, isExpanded)
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, {suggest = true, auto = true})) do
for _, action in ipairs(actionsFor(last, node.fruit, {auto = true})) do
table.insert(rows, suggestRow(action, depth + 1, ctx.titleOf, ctx.textOf))
end
end
@@ -334,12 +374,27 @@ local function pausedRows(rows, last, ctx)
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_rootPaused"), ctx.textOf("spu_chain_resume")}, {[5] = {kind = "resume", rootKey = key}}))
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")
@@ -385,8 +440,14 @@ function ADChainView.rows(last, titleOf, fmt, textOf, expanded)
textOf("spu_chain_col_perMonth"), textOf("spu_chain_col_status"), textOf("spu_chain_col_stock")}))
end
local ctx = {titleOf = titleOf, fmt = fmt, textOf = textOf, short = shortageByFruit(last)}
for _, root in ipairs(roots) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
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
+19
View File
@@ -519,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)
@@ -619,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)
+3 -2
View File
@@ -46,7 +46,8 @@ local function isCandidate(field, used, dismissedFruit, fruit)
end
-- prices: {[afgrøde] = pris} eller en funktion afgrøde -> pris (slås kun op for kandidaternes afgrøder).
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed)
-- 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
@@ -57,7 +58,7 @@ function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismis
return priceCache[fruit]
end
for _, shortage in ipairs(shortages or {}) do
local remaining = shortage.missingHa
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
+8 -3
View File
@@ -380,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
+63 -5
View File
@@ -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)
+7 -2
View File
@@ -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
+42 -1
View File
@@ -52,20 +52,61 @@ end
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
-- {{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 = {}
+132
View File
@@ -0,0 +1,132 @@
-- AD Load Diag
-- Diagnose når AutoDrive står ved et LOAD-mål uden at fylde (in-game 30/9: Fendt + MEGA-sprøjte ved "Market Price
-- Liquid Fill Station"). AutoDrive fylder kun, når en fyldenhed står i triggerens boks (trigger.fillableObjects) og
-- triggeren har varen. Efter REPORT_MS ved målet uden fremgang skrives én linje: markør, vare, og for hver vogn den
-- nærmeste AD-læssetrigger (afstand, har varen?, står vognen i boksen?). Kun når en trigger med varen er inden for
-- NEAR_M (holder ved Wait-punkter logges ikke). Ingen indgreb — kun log.
ADLoadDiag = {}
ADLoadDiag.LOG_PREFIX = "ADSmartPickup: pålæsning: "
ADLoadDiag.REPORT_MS = 30000
ADLoadDiag.NEAR_M = 20 -- m: trigger med varen herinden for = køretøjet står ved stationen (ellers Wait/mark: ingen log)
-- ---- ren logik ----
-- state (pr. opgave), dt, reached (ved målet), level (liter på vognene) -> true når der skal rapporteres (én gang pr.
-- ankomst). Fremgang eller kørsel nulstiller.
function ADLoadDiag.tick(state, dt, reached, level)
if not reached then
state.ms, state.reported, state.level = 0, false, nil
return false
end
if state.level == nil or level > state.level then
state.level, state.ms = level, 0
return false
end
state.ms = (state.ms or 0) + (dt or 0)
if state.reported or state.ms < ADLoadDiag.REPORT_MS then return false end
state.reported = true
return true
end
-- distances = afstande fra vognene til nærmeste AD-trigger med varen -> står ved stationen?
function ADLoadDiag.atStation(distances)
for _, d in ipairs(distances or {}) do
if d <= ADLoadDiag.NEAR_M then return true end
end
return false
end
-- ---- spil ----
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and fillType ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
local function inBox(trigger, trailer)
for key, entry in pairs(trigger.fillableObjects or {}) do
if key == trailer or (type(entry) == "table" and entry.object == trailer) then return true end
end
return false
end
local function describeTrailer(adEnv, trailer, fillType)
local node = trailer.components ~= nil and trailer.components[1] ~= nil and trailer.components[1].node or trailer.rootNode
if node == nil then return nil end
local tx, _, tz = getWorldTranslation(node)
local nearest, nearestOk = nil, nil
for _, trigger in pairs(adEnv.ADTriggerManager.getLoadTriggers() or {}) do
local x, _, z = adEnv.ADTriggerManager.getTriggerPos(trigger)
if x ~= nil and (x ~= 0 or z ~= 0) then
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
local ok = adEnv.ADTriggerManager.triggerSupportsFillType(trigger, fillType)
if nearest == nil or d < nearest.d then nearest = {d = d, ok = ok, trigger = trigger} end
if ok and (nearestOk == nil or d < nearestOk.d) then nearestOk = {d = d, trigger = trigger} end
end
end
local name = trailer.getName ~= nil and trailer:getName() or "?"
local parts = {}
if nearest ~= nil then
table.insert(parts, string.format("nærmeste AD-trigger %.1f m (%s varen, %s)", nearest.d, nearest.ok and "har" or "UDEN",
inBox(nearest.trigger, trailer) and "vognen i boksen" or "vognen IKKE i boksen"))
end
if nearestOk ~= nil and (nearest == nil or nearestOk.trigger ~= nearest.trigger) then
table.insert(parts, string.format("nærmeste med varen %.1f m (%s)", nearestOk.d,
inBox(nearestOk.trigger, trailer) and "i boksen" or "ikke i boksen"))
end
if nearestOk == nil then table.insert(parts, "INGEN AD-læssetrigger med varen") end
return string.format("'%s': %s", tostring(name), table.concat(parts, ", ")), nearestOk ~= nil and nearestOk.d or nil
end
local function report(adEnv, task)
local vehicle = task.vehicle
local fillType = vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
local marker = task.destinationID ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(task.destinationID) or nil
local lines, distances = {}, {}
for _, trailer in ipairs(task.trailers or {}) do
if trailer.getFillUnits ~= nil and #trailer:getFillUnits() > 0 then
local ok, line, distance = pcall(describeTrailer, adEnv, trailer, fillType)
if ok and line ~= nil then table.insert(lines, line) end
if ok and distance ~= nil then table.insert(distances, distance) end
end
end
-- holder ved Wait/mark (ingen trigger med varen tæt på): det er ikke en fejlet pålæsning
if not ADLoadDiag.atStation(distances) then return end
local maxDistance = adEnv.AutoDrive.getMaxTriggerDistance ~= nil and adEnv.AutoDrive.getMaxTriggerDistance(vehicle) or nil
Logging.warning(ADLoadDiag.LOG_PREFIX .. "'%s' står ved '%s' uden at fylde %s i %d s (AD-triggerafstand %s m): %s",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker ~= nil and marker.name or task.destinationID), fillTypeName(fillType),
math.floor(ADLoadDiag.REPORT_MS / 1000), tostring(maxDistance), #lines > 0 and table.concat(lines, "; ") or "ingen vogne med fyldenheder")
end
local states = setmetatable({}, {__mode = "k"})
function ADLoadDiag.after(adEnv, task, dt)
local vehicle = task.vehicle
if vehicle == nil or vehicle.ad == nil or vehicle.ad.drivePathModule == nil then return end
local state = states[task]
if state == nil then
state = {}
states[task] = state
end
local reached = vehicle.ad.drivePathModule:isTargetReached() and not vehicle.ad.trailerModule:isActiveAtTrigger()
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
if ADLoadDiag.tick(state, dt, reached, level) then report(adEnv, task) end
end
local isInstalled = false
function ADLoadDiag.install(adEnv)
local task = adEnv ~= nil and adEnv.LoadAtDestinationTask or nil
if isInstalled or task == nil or task.update == nil or adEnv.ADTriggerManager == nil then return end
isInstalled = true
local original = task.update
task.update = function(self, dt)
local result = original(self, dt)
local ok, err = pcall(ADLoadDiag.after, adEnv, self, dt)
if not ok and not self.spuDiagFailed then
self.spuDiagFailed = true
Logging.warning(ADLoadDiag.LOG_PREFIX .. "diagnose fejlede: %s", tostring(err))
end
return result
end
end
+9 -1
View File
@@ -660,6 +660,13 @@ end
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
-- 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
+2 -1
View File
@@ -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,
}
+8 -1
View File
@@ -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
+3 -2
View File
@@ -202,7 +202,8 @@ 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
@@ -484,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,
+2
View File
@@ -1478,6 +1478,7 @@ function ADRunsController.save()
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
@@ -1498,6 +1499,7 @@ function ADRunsController.load(adEnv)
pcall(ADRunsController.migrateBuildings, adEnv)
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
if ADChain ~= nil and ADChain.load ~= nil then pcall(ADChain.load) end
if 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
+154
View File
@@ -0,0 +1,154 @@
-- AD Sell Block
-- Salgssteder spilleren har slået fra (in-game 30/9: "Market Price Liquid Fill Station" fra FarmFillStations gik i stå
-- ved aflæsning af gylle). Konsol: spuSellBlock viser salgsstederne; spuSellBlock <del af navn> slår dem fra/til.
-- Udkørsel (ADSources.findSellMarkers) og priser (ADSources.getMarketQuote) springer udelukkede over. Gemmes pr.
-- savegame i ADSmartPickup_sellblock.xml (skrives fra ADRunsController.save, som de andre filer).
ADSellBlock = {}
ADSellBlock.FILE_NAME = "ADSmartPickup_sellblock.xml"
ADSellBlock.ROOT = "sellBlock"
ADSellBlock.blocked = {} -- {[stationId] = visningsnavn}
-- ---- ren logik ----
-- stations = {{id, names = {...}}}, query = del af et navn (små/store bogstaver ligegyldige) -> matchende stationer
function ADSellBlock.match(stations, query)
local wanted = string.lower(tostring(query or ""))
local hits = {}
if wanted == "" then return hits end
for _, station in ipairs(stations or {}) do
for _, name in ipairs(station.names or {}) do
if string.find(string.lower(tostring(name)), wanted, 1, true) ~= nil then
table.insert(hits, station)
break
end
end
end
return hits
end
-- Ny tabel: hver station skiftes (fra -> til, til -> fra). Første navn huskes til visning.
function ADSellBlock.toggle(blocked, stations)
local result = {}
for id, name in pairs(blocked or {}) do result[id] = name end
for _, station in ipairs(stations or {}) do
if result[station.id] ~= nil then
result[station.id] = nil
else
result[station.id] = tostring((station.names or {})[1] or station.id)
end
end
return result
end
-- ---- spil ----
local function stationId(station)
local placeable = station ~= nil and station.owningPlaceable or nil
if placeable ~= nil and placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
return nil
end
function ADSellBlock.isBlocked(station)
local id = stationId(station)
return id ~= nil and ADSellBlock.blocked[id] ~= nil
end
local function stationNames(station)
local names = {}
local placeable = station.owningPlaceable
if placeable ~= nil and placeable.getName ~= nil then
local ok, name = pcall(placeable.getName, placeable)
if ok and name ~= nil then table.insert(names, name) end
end
if station.getName ~= nil then
local ok, name = pcall(station.getName, station)
if ok and name ~= nil then table.insert(names, name) end
end
return names
end
-- Alle salgssteder med AD-markør -> {{id, names}} (markørens navn først: det spilleren ser i AutoDrive).
local function sellStations(adEnv)
local list, byId = {}, {}
local farmId = g_currentMission:getFarmId()
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
local id = station ~= nil and ADPrices.isSellingStation(station) and stationId(station) or nil
if id ~= nil then
if byId[id] == nil then
byId[id] = {id = id, names = {}}
table.insert(list, byId[id])
for _, name in ipairs(stationNames(station)) do table.insert(byId[id].names, name) end
end
table.insert(byId[id].names, 1, marker.name)
end
end
table.sort(list, function(a, b) return tostring(a.names[1]) < tostring(b.names[1]) end)
return list
end
function ADSellBlock:consoleSellBlock(...)
local query = table.concat({...}, " ")
local adEnv = FS25_AutoDrive
if adEnv == nil or adEnv.ADGraphManager == nil then return "AutoDrive ikke fundet" end
local stations = sellStations(adEnv)
if query == "" then
local lines = {"Salgssteder (spuSellBlock <del af navn> slår fra/til):"}
for _, station in ipairs(stations) do
table.insert(lines, string.format(" %s %s", ADSellBlock.blocked[station.id] ~= nil and "[FRA]" or "[til]", table.concat(station.names, " / ")))
end
return table.concat(lines, "\n")
end
local hits = ADSellBlock.match(stations, query)
if #hits == 0 then return string.format("Intet salgssted matcher '%s' (spuSellBlock uden navn viser listen)", query) end
ADSellBlock.blocked = ADSellBlock.toggle(ADSellBlock.blocked, hits)
local lines = {}
for _, station in ipairs(hits) do
local text = string.format("%s: %s", station.names[1], ADSellBlock.blocked[station.id] ~= nil and "slået FRA som salgssted" or "bruges igen")
table.insert(lines, text)
Logging.info("ADSmartPickup: salgssted " .. text)
end
if ADRunsController ~= nil and ADRunsController.save ~= nil then pcall(ADRunsController.save) end
return table.concat(lines, "\n")
end
local function getPath()
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
if path == nil then return nil end
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADSellBlock.FILE_NAME))
end
function ADSellBlock.save()
local path = getPath()
if path == nil then return false end
local xmlFile = XMLFile.create("ADSmartPickupSellBlock", path, ADSellBlock.ROOT)
if xmlFile == nil then return false end
local index = 0
for id, name in pairs(ADSellBlock.blocked) do
local key = string.format("%s.station(%d)", ADSellBlock.ROOT, index)
xmlFile:setString(key .. "#id", id)
xmlFile:setString(key .. "#name", name)
index = index + 1
end
xmlFile:save()
xmlFile:delete()
return true
end
function ADSellBlock.load()
ADSellBlock.blocked = {}
local path = getPath()
if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupSellBlock", path)
if xmlFile == nil then return end
xmlFile:iterate(ADSellBlock.ROOT .. ".station", function(_, key)
local id = xmlFile:getString(key .. "#id")
if id ~= nil then ADSellBlock.blocked[id] = xmlFile:getString(key .. "#name") or id end
end)
xmlFile:delete()
end
if addConsoleCommand ~= nil then
addConsoleCommand("spuSellBlock", "Salgssteder: uden navn = liste; spuSellBlock <del af navn> slår fra/til", "consoleSellBlock", ADSellBlock)
end
+3
View File
@@ -1152,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
+18 -5
View File
@@ -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
@@ -994,7 +1007,7 @@ 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}
+28 -1
View File
@@ -27,6 +27,10 @@ function SmartPickupFrame:onChainAction(action)
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
@@ -40,6 +44,27 @@ function SmartPickupFrame:onChainAction(action)
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 {}
@@ -74,7 +99,9 @@ function SmartPickupFrame:addChainRow(entry)
cells[4]:setTextColor(unpackFn(color))
end
local actions, toggle = entry.actions or {}, entry.toggle
if next(actions) ~= nil or toggle ~= nil then
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
@@ -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]))
+21 -5
View File
@@ -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"))
+15 -5
View File
@@ -21,6 +21,14 @@ 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", "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)
@@ -99,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))
@@ -270,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)
@@ -282,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/
+19 -1
View File
@@ -151,7 +151,7 @@
<text name="spu_field_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
<text name="spu_field_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"/>
@@ -310,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"/>
@@ -350,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 %%"/>
@@ -377,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)"/>
@@ -514,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 %%"/>
@@ -576,6 +583,16 @@
<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"/>
@@ -589,6 +606,7 @@
<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"/>
+19 -1
View File
@@ -151,7 +151,7 @@
<text name="spu_field_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
<text name="spu_field_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"/>
@@ -310,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"/>
@@ -350,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 %%"/>
@@ -377,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)"/>
@@ -514,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 %%"/>
@@ -576,6 +583,16 @@
<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"/>
@@ -589,6 +606,7 @@
<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"/>
+3 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.61.0.0</version>
<version>1.62.7.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -32,6 +32,8 @@
<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"/>
+7
View File
@@ -131,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).
@@ -1200,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 |
---
+7
View File
@@ -131,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).
@@ -1238,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 |
---
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.