Compare commits
22 Commits
a3d234a3a2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb287a8154 | |||
| e229a27624 | |||
| ba98cf83d3 | |||
| a6662d6da1 | |||
| bf3b99aca4 | |||
| cbc9b2c8e3 | |||
| 1bda82a270 | |||
| 21b48d7664 | |||
| 88ddb3d6cd | |||
| 37cf84c101 | |||
| 991252848f | |||
| 6b533e62eb | |||
| c92c870297 | |||
| 18b6c82871 | |||
| 47afdebf64 | |||
| 2290bdde95 | |||
| 0c21ba49a0 | |||
| e1f7b19fef | |||
| 74537f7ec2 | |||
| d2970a1bda | |||
| bd106a2176 | |||
| acf94c8e88 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
end
|
||||
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
|
||||
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
|
||||
local function flowTitle(data, fillTypes)
|
||||
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
|
||||
if #fillTypes == 1 then return first end
|
||||
return string.format("%s +%d", tostring(first), #fillTypes - 1)
|
||||
end
|
||||
|
||||
local function startWithPair(adEnv, run, runId, pair, used)
|
||||
used[pair.tractor.id], used[pair.implement.id] = true, true
|
||||
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
|
||||
run = ADRuns.getRun(ADRunsController.store, runId)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -339,7 +339,12 @@ local function rowsRules(guide)
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local rows = {}
|
||||
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
||||
if sells then
|
||||
-- "Altid" (0) øverst: sælg uanset pris (in-game 30/9)
|
||||
local options = {{label = L("spu_sellAt_always"), value = 0}}
|
||||
for _, option in ipairs(percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5)) do table.insert(options, option) end
|
||||
table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), options, draft.sellAtPercent))
|
||||
end
|
||||
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||
@@ -489,7 +494,7 @@ local SUMMARY = {
|
||||
if isOut(guide) then
|
||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||
local parts = {}
|
||||
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if sells then table.insert(parts, draft.sellAtPercent == 0 and L("spu_guide_flow_sumSellAlways") or L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||
|
||||
@@ -52,20 +52,61 @@ end
|
||||
|
||||
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
|
||||
-- {{kind, runs}, ...}; tomme grupper udelades.
|
||||
-- Spillerens egne flows først (forsyning, udkørsel), så kædernes egne flows (run.chain) samlet til sidst
|
||||
-- (in-game 30/9: "Kæde: …"-flows blandede sig alfabetisk ind mellem de flows spilleren selv har sat op).
|
||||
function ADFlowView.boardGroups(runs)
|
||||
local byKind = {supply = {}, outbound = {}}
|
||||
local chainRuns = {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
table.insert(chainRuns, run)
|
||||
else
|
||||
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
||||
end
|
||||
end
|
||||
local groups = {}
|
||||
for _, kind in ipairs({"supply", "outbound"}) do
|
||||
if #byKind[kind] > 0 then
|
||||
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")})
|
||||
table.insert(groups, {kind = kind, chain = false, runs = sortedBy(byKind[kind], "name")})
|
||||
end
|
||||
end
|
||||
if #chainRuns > 0 then table.insert(groups, {kind = "supply", chain = true, runs = sortedBy(chainRuns, "name")}) end
|
||||
return groups
|
||||
end
|
||||
|
||||
-- Kort maskinnavn til lister: " Unrealistic" og parenteser som "(+ Custom Crops)" fjernes (lange navne blev ombrudt
|
||||
-- med traktorens nummer alene på næste linje, in-game 30/9). Bliver intet tilbage, bruges det fulde navn.
|
||||
function ADFlowView.shortVehicleName(name)
|
||||
local full = tostring(name or "")
|
||||
local short = full:gsub("%s*%b()", ""):gsub("%s+[Uu]nrealistic", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if short == "" then return full end
|
||||
return short
|
||||
end
|
||||
|
||||
-- Kædeflow uden traktor (venter på en ledig traktor/vogn).
|
||||
function ADFlowView.isIdleChain(run)
|
||||
return run.chain ~= nil and run.chain ~= "" and #(run.vehicleIds or {}) == 0
|
||||
end
|
||||
|
||||
-- Kædeflows uden traktor foldes sammen i listen (de venter på en ledig traktor/vogn). -> active, idle
|
||||
function ADFlowView.splitIdle(runs)
|
||||
local active, idle = {}, {}
|
||||
for _, run in ipairs(runs or {}) do
|
||||
table.insert(#(run.vehicleIds or {}) > 0 and active or idle, run)
|
||||
end
|
||||
return active, idle
|
||||
end
|
||||
|
||||
-- Navn i listen: kædeflows står under overskriften "Kædernes egne flows", så "Kæde: " fjernes (ellers skjuler
|
||||
-- afkortningen hvilken bygning det gælder).
|
||||
function ADFlowView.listName(run)
|
||||
local name = tostring(run.name or "")
|
||||
if run.chain ~= nil and run.chain ~= "" then
|
||||
name = name:gsub("^Kæde:%s*", ""):gsub("^Chain:%s*", "")
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
|
||||
function ADFlowView.orderedRuns(runs)
|
||||
local result = {}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
-- AD Load Diag
|
||||
-- Diagnose når AutoDrive står ved et LOAD-mål uden at fylde (in-game 30/9: Fendt + MEGA-sprøjte ved "Market Price
|
||||
-- Liquid Fill Station"). AutoDrive fylder kun, når en fyldenhed står i triggerens boks (trigger.fillableObjects) og
|
||||
-- triggeren har varen. Efter REPORT_MS ved målet uden fremgang skrives én linje: markør, vare, og for hver vogn den
|
||||
-- nærmeste AD-læssetrigger (afstand, har varen?, står vognen i boksen?). Kun når en trigger med varen er inden for
|
||||
-- NEAR_M (holder ved Wait-punkter logges ikke). Ingen indgreb — kun log.
|
||||
|
||||
ADLoadDiag = {}
|
||||
ADLoadDiag.LOG_PREFIX = "ADSmartPickup: pålæsning: "
|
||||
ADLoadDiag.REPORT_MS = 30000
|
||||
ADLoadDiag.NEAR_M = 20 -- m: trigger med varen herinden for = køretøjet står ved stationen (ellers Wait/mark: ingen log)
|
||||
|
||||
-- ---- ren logik ----
|
||||
|
||||
-- state (pr. opgave), dt, reached (ved målet), level (liter på vognene) -> true når der skal rapporteres (én gang pr.
|
||||
-- ankomst). Fremgang eller kørsel nulstiller.
|
||||
function ADLoadDiag.tick(state, dt, reached, level)
|
||||
if not reached then
|
||||
state.ms, state.reported, state.level = 0, false, nil
|
||||
return false
|
||||
end
|
||||
if state.level == nil or level > state.level then
|
||||
state.level, state.ms = level, 0
|
||||
return false
|
||||
end
|
||||
state.ms = (state.ms or 0) + (dt or 0)
|
||||
if state.reported or state.ms < ADLoadDiag.REPORT_MS then return false end
|
||||
state.reported = true
|
||||
return true
|
||||
end
|
||||
|
||||
-- distances = afstande fra vognene til nærmeste AD-trigger med varen -> står ved stationen?
|
||||
function ADLoadDiag.atStation(distances)
|
||||
for _, d in ipairs(distances or {}) do
|
||||
if d <= ADLoadDiag.NEAR_M then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ---- spil ----
|
||||
|
||||
local function fillTypeName(fillType)
|
||||
return g_fillTypeManager ~= nil and fillType ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
end
|
||||
|
||||
local function inBox(trigger, trailer)
|
||||
for key, entry in pairs(trigger.fillableObjects or {}) do
|
||||
if key == trailer or (type(entry) == "table" and entry.object == trailer) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function describeTrailer(adEnv, trailer, fillType)
|
||||
local node = trailer.components ~= nil and trailer.components[1] ~= nil and trailer.components[1].node or trailer.rootNode
|
||||
if node == nil then return nil end
|
||||
local tx, _, tz = getWorldTranslation(node)
|
||||
local nearest, nearestOk = nil, nil
|
||||
for _, trigger in pairs(adEnv.ADTriggerManager.getLoadTriggers() or {}) do
|
||||
local x, _, z = adEnv.ADTriggerManager.getTriggerPos(trigger)
|
||||
if x ~= nil and (x ~= 0 or z ~= 0) then
|
||||
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
|
||||
local ok = adEnv.ADTriggerManager.triggerSupportsFillType(trigger, fillType)
|
||||
if nearest == nil or d < nearest.d then nearest = {d = d, ok = ok, trigger = trigger} end
|
||||
if ok and (nearestOk == nil or d < nearestOk.d) then nearestOk = {d = d, trigger = trigger} end
|
||||
end
|
||||
end
|
||||
local name = trailer.getName ~= nil and trailer:getName() or "?"
|
||||
local parts = {}
|
||||
if nearest ~= nil then
|
||||
table.insert(parts, string.format("nærmeste AD-trigger %.1f m (%s varen, %s)", nearest.d, nearest.ok and "har" or "UDEN",
|
||||
inBox(nearest.trigger, trailer) and "vognen i boksen" or "vognen IKKE i boksen"))
|
||||
end
|
||||
if nearestOk ~= nil and (nearest == nil or nearestOk.trigger ~= nearest.trigger) then
|
||||
table.insert(parts, string.format("nærmeste med varen %.1f m (%s)", nearestOk.d,
|
||||
inBox(nearestOk.trigger, trailer) and "i boksen" or "ikke i boksen"))
|
||||
end
|
||||
if nearestOk == nil then table.insert(parts, "INGEN AD-læssetrigger med varen") end
|
||||
return string.format("'%s': %s", tostring(name), table.concat(parts, ", ")), nearestOk ~= nil and nearestOk.d or nil
|
||||
end
|
||||
|
||||
local function report(adEnv, task)
|
||||
local vehicle = task.vehicle
|
||||
local fillType = vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
|
||||
local marker = task.destinationID ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(task.destinationID) or nil
|
||||
local lines, distances = {}, {}
|
||||
for _, trailer in ipairs(task.trailers or {}) do
|
||||
if trailer.getFillUnits ~= nil and #trailer:getFillUnits() > 0 then
|
||||
local ok, line, distance = pcall(describeTrailer, adEnv, trailer, fillType)
|
||||
if ok and line ~= nil then table.insert(lines, line) end
|
||||
if ok and distance ~= nil then table.insert(distances, distance) end
|
||||
end
|
||||
end
|
||||
-- holder ved Wait/mark (ingen trigger med varen tæt på): det er ikke en fejlet pålæsning
|
||||
if not ADLoadDiag.atStation(distances) then return end
|
||||
local maxDistance = adEnv.AutoDrive.getMaxTriggerDistance ~= nil and adEnv.AutoDrive.getMaxTriggerDistance(vehicle) or nil
|
||||
Logging.warning(ADLoadDiag.LOG_PREFIX .. "'%s' står ved '%s' uden at fylde %s i %d s (AD-triggerafstand %s m): %s",
|
||||
tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker ~= nil and marker.name or task.destinationID), fillTypeName(fillType),
|
||||
math.floor(ADLoadDiag.REPORT_MS / 1000), tostring(maxDistance), #lines > 0 and table.concat(lines, "; ") or "ingen vogne med fyldenheder")
|
||||
end
|
||||
|
||||
local states = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function ADLoadDiag.after(adEnv, task, dt)
|
||||
local vehicle = task.vehicle
|
||||
if vehicle == nil or vehicle.ad == nil or vehicle.ad.drivePathModule == nil then return end
|
||||
local state = states[task]
|
||||
if state == nil then
|
||||
state = {}
|
||||
states[task] = state
|
||||
end
|
||||
local reached = vehicle.ad.drivePathModule:isTargetReached() and not vehicle.ad.trailerModule:isActiveAtTrigger()
|
||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||
if ADLoadDiag.tick(state, dt, reached, level) then report(adEnv, task) end
|
||||
end
|
||||
|
||||
local isInstalled = false
|
||||
|
||||
function ADLoadDiag.install(adEnv)
|
||||
local task = adEnv ~= nil and adEnv.LoadAtDestinationTask or nil
|
||||
if isInstalled or task == nil or task.update == nil or adEnv.ADTriggerManager == nil then return end
|
||||
isInstalled = true
|
||||
local original = task.update
|
||||
task.update = function(self, dt)
|
||||
local result = original(self, dt)
|
||||
local ok, err = pcall(ADLoadDiag.after, adEnv, self, dt)
|
||||
if not ok and not self.spuDiagFailed then
|
||||
self.spuDiagFailed = true
|
||||
Logging.warning(ADLoadDiag.LOG_PREFIX .. "diagnose fejlede: %s", tostring(err))
|
||||
end
|
||||
return result
|
||||
end
|
||||
end
|
||||
@@ -660,6 +660,13 @@ end
|
||||
|
||||
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
||||
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
||||
-- Palleskur: moden tager ét objekt ud ad gangen og afslutter selv pålæsningen (fuld / tomt lager / 30 s uden
|
||||
-- udlevering). Imens holdes AD's egen pålæsning, ellers kører AD, så snart triggeren et øjeblik er tom (in-game 30/9:
|
||||
-- Sell Egg kørte halvfuld). Først når AD har aktiveret UAL (det gør AD selv ved ankomst).
|
||||
function ADOutbound.holdForSpawn(sourceKind, done, ualActive)
|
||||
return sourceKind == "objectStorage" and not done and ualActive == true
|
||||
end
|
||||
|
||||
local function updateSpawn(adEnv, state, task, source, dt)
|
||||
local vehicle = task.vehicle
|
||||
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
||||
@@ -676,7 +683,7 @@ local function updateSpawn(adEnv, state, task, source, dt)
|
||||
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
|
||||
spawn.pending = nil
|
||||
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType)))
|
||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift: lager %d -> %d, vogn %d -> %d objekter); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType), spawn.pending.stored, stored, spawn.pending.loaded, loaded))
|
||||
state.done = true
|
||||
return
|
||||
else
|
||||
@@ -763,6 +770,7 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||||
task:finished()
|
||||
return true
|
||||
end
|
||||
if ADOutbound.holdForSpawn(source.kind, state.done, task.activatedUALLoading) then return true end
|
||||
end
|
||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||
if state.lastFill == nil or level > state.lastFill then
|
||||
|
||||
@@ -67,7 +67,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
||||
storagePercent = storagePercent,
|
||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||
percent = percent,
|
||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
||||
-- sælg fra 0 % = "Altid": uanset pris (også uden prisdata)
|
||||
priceOk = input.sellAtPercent == 0 or (percent ~= nil and percent >= (input.sellAtPercent or 90)),
|
||||
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ function ADPool.roleOf(specs)
|
||||
return role
|
||||
end
|
||||
|
||||
-- Ekstra roller for et redskab: sprøjtens roller (gødning/kalk/ukrudt) — men ikke for en såmaskine med
|
||||
-- gødningstank (Kinze 4905), den er kun såmaskine (in-game 30/9: hentet til ren gødning).
|
||||
function ADPool.rolesOf(role, sprayRoles)
|
||||
if role ~= "seeder" and sprayRoles ~= nil then return sprayRoles end
|
||||
return ADFieldFleet.rolesFor(role)
|
||||
end
|
||||
|
||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||
function ADPool.numberOf(value)
|
||||
if type(value) == "number" then return value end
|
||||
@@ -402,7 +409,7 @@ function ADPool.collect(adEnv, isBusy)
|
||||
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||
table.insert(implements, {carries = carries, capacity = capacity,
|
||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
role = role, roles = ADPool.rolesOf(role, specs.sprayRoles), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user