feat: fabriksstyret forsyning — målets tændte produktioner styrer ingredienslisten (v1.6.0.0)

Er aflæsningsmålet et produktionspunkt, erstattes ingredienslisten af inputs fra
ønskede produktioner (tændte, eller auto-slukkede af kortet med status
MISSING_INPUTS — Dakotas ExtendedProductionPoint), begrænset til det vognen kan
bære. Brugerens multi-valg er kun tændknappen og gendannes ved stop som hidtil.

Prioritet: tid til tom = lager / (Σ amount × cyclesPerMinute); varer med kendt
forbrug scorer i ]1,2] og går altid forud for relativ tomhed [0,1]. Loggen viser
forbrug og minutter; fabrik uden ønskede produktioner logges én gang.

API-navne verificeret mod EasyDevControls + Dakota16x ExtendedProductionPoint.
This commit is contained in:
2026-09-22 20:04:08 +02:00
parent 8c76790ece
commit 795c03663c
6 changed files with 257 additions and 14 deletions
+65
View File
@@ -290,6 +290,71 @@ bareStation.owningPlaceable = {spec_productionPoint = {productionPoint = {storag
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE})
check("S10 lager læses fra produktionspunktet", manager:getNextPickup(feeder, false), 3)
-- ==== Fabriksstyret forsyning (aflæsningsmålet er et produktionspunkt) ====
ProductionPoint = {PROD_STATUS = {INACTIVE = 0, RUNNING = 1, MISSING_INPUTS = 2, NO_OUTPUT_SPACE = 3}}
local WATER = 40
FILL_TYPE_NAMES[WATER] = "WATER"
FS25_AutoDrive.AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end
-- Fabrik med TMR-opskrift: sluger ensilage, nipper til hø. Vand kan vognen ikke bære.
local function buildFactoryWorld(tmrEnabled, tmrStatus)
buildFeedWorld()
local pp = {
enabled = {tmr = tmrEnabled},
productions = {
{id = "tmr", cyclesPerMinute = 1, status = tmrStatus, inputs = {{type = SILAGE, amount = 250}, {type = HAY, amount = 10}, {type = WATER, amount = 1000}}},
{id = "off", cyclesPerMinute = 1, status = ProductionPoint.PROD_STATUS.INACTIVE, inputs = {{type = GRASS, amount = 500}}},
},
getIsProductionEnabled = function(self, id) return self.enabled[id] == true end,
}
local factory = {spec_productionPoint = {productionPoint = pp}, getName = function() return "AIO" end}
unloadStations = {makeUnloadStation(110, 5000, 10, blanderLevels, {[HAY] = 100000, [GRASS] = 100000, [SILAGE] = 100000, [WATER] = 100000}, factory)}
local driver = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS})
driver.carriable = {HAY, GRASS, SILAGE}
return driver, pp
end
-- F1: tændt produktion styrer listen: ensilage (200 min til tom) vinder over hø (8000 min)
-- og over græs, som er relativt tommest men ikke et input i fabrikken
local factoryDriver = buildFactoryWorld(true, ProductionPoint.PROD_STATUS.RUNNING)
check("F1 fabrikkens input med kortest tid til tom", manager:getNextPickup(factoryDriver, false), 4)
check("F1 AD's vare skiftet til ensilage (ikke i brugerens valg)", factoryDriver.ad.stateModule.fillType, SILAGE)
check("F1 log nævner minutter til tom", logLines[#logLines]:find("min til tom", 1, true) ~= nil, true)
-- F2: næste tur — listen genudledes fra fabrikken, ikke fra brugerens valg
blanderLevels[SILAGE] = 99500
check("F2 fuld ensilage -> hø (næste fabriks-input)", manager:getNextPickup(factoryDriver, false), 1)
-- F3: AD stopper -> brugerens OPRINDELIGE valg gendannes, ikke fabrikkens liste
factoryDriver.ad.stateModule.active = false
listener:update(16)
check("F3 brugerens valg gendannet ved stop", #factoryDriver.ad.stateModule.selectedFillTypes, 2)
-- F4: manuelt slukket produktion -> fallback til brugerens valg (græs er tommest)
factoryDriver = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.INACTIVE)
local function countLogsSince(startIndex, needle)
local count = 0
for i = startIndex + 1, #logLines do
if logLines[i]:find(needle, 1, true) then count = count + 1 end
end
return count
end
local logMark = #logLines
check("F4 slukket fabrik -> brugerens valg", manager:getNextPickup(factoryDriver, false), 3)
check("F4 loggen forklarer fallback", countLogsSince(logMark, "ingen ønskede produktioner"), 1)
manager:getNextPickup(factoryDriver, false)
check("F4 forklaringen logges kun én gang", countLogsSince(logMark, "ingen ønskede produktioner"), 1)
-- F5: auto-slukket af kortet pga. manglende input -> stadig fabrikkens liste
factoryDriver = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.MISSING_INPUTS)
check("F5 auto-slukket fabrik fodres stadig", manager:getNextPickup(factoryDriver, false), 4)
-- F6: produktionspunkt uden getIsProductionEnabled -> activeProductions-listen bruges
local fallbackDriver, fallbackPp = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.RUNNING)
fallbackPp.getIsProductionEnabled = nil
fallbackPp.activeProductions = {fallbackPp.productions[1]}
check("F6 activeProductions som fallback", manager:getNextPickup(fallbackDriver, false), 4)
-- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven)
resetWorld()
local loadLevels = {[SUGARBEET] = 5000}
+46
View File
@@ -62,5 +62,51 @@ check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSou
check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true)
check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true)
-- Fabriksstyret forsyning: hvilke produktioner "ønsker" input?
local MISSING_INPUTS, INACTIVE = 2, 0
check("tændt produktion ønskes", ADSupplyPlanner.isProductionWanted(true, INACTIVE, MISSING_INPUTS), true)
check("auto-slukket pga. mangel ønskes stadig", ADSupplyPlanner.isProductionWanted(false, MISSING_INPUTS, MISSING_INPUTS), true)
check("manuelt slukket ønskes ikke", ADSupplyPlanner.isProductionWanted(false, INACTIVE, MISSING_INPUTS), false)
check("ukendt status uden tænd -> ikke ønsket", ADSupplyPlanner.isProductionWanted(false, nil, MISSING_INPUTS), false)
-- collectProductionDemand: liter/min pr. vare fra ønskede produktioner, kun det vognen kan bære
local STRAW, MINERAL, WATER = 23, 24, 25
local productions = {
{id = "tmr", cyclesPerMinute = 2, inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}, {type = MINERAL, amount = 1}}},
{id = "mix", cyclesPerMinute = 1, inputs = {{type = SILAGE, amount = 50}, {type = WATER, amount = 500}}},
{id = "off", cyclesPerMinute = 5, inputs = {{type = HAY, amount = 100}}},
}
local wanted = {tmr = true, mix = true}
local canCarry = {[SILAGE] = true, [STRAW] = true, [MINERAL] = true, [HAY] = true}
local demand = ADSupplyPlanner.collectProductionDemand(productions, function(p) return wanted[p.id] == true end, canCarry)
check("forbrug summeres på tværs af produktioner", demand[SILAGE], 250)
check("forbrug ganges med cyklusser/min", demand[STRAW], 20)
check("uønsket produktion tæller ikke", demand[HAY], nil)
check("vare vognen ikke kan bære udelades", demand[WATER], nil)
check("tom produktionsliste -> tomt behov", next(ADSupplyPlanner.collectProductionDemand({}, function() return true end, canCarry)), nil)
check("produktion uden inputs tåles", next(ADSupplyPlanner.collectProductionDemand({{id = "x", cyclesPerMinute = 1}}, function() return true end, canCarry)), nil)
-- minutesToEmpty
check("minutter til tom", ADSupplyPlanner.minutesToEmpty(1000, 250), 4)
check("intet forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, 0), nil)
check("ukendt forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, nil), nil)
-- rankNeeds med forbrug: den der løber tør først vinder, selv om den er relativt fyldest
local timed = ADSupplyPlanner.rankNeeds({
{fillType = MINERAL, level = 500, capacity = 10000, litersPerMinute = 2}, -- 250 min, 95 % tom
{fillType = SILAGE, level = 50000, capacity = 100000, litersPerMinute = 250}, -- 200 min, 50 % tom
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, -- 4500 min
})
check("kortest tid til tom først", timed[1].fillType, SILAGE)
check("dernæst næstkortest", timed[2].fillType, MINERAL)
check("minutesToEmpty følger med", timed[1].minutesToEmpty, 200)
check("vare med forbrug slår vare uden", ADSupplyPlanner.rankNeeds({
{fillType = HAY, level = 0, capacity = 100000},
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20},
})[1].fillType, STRAW)
check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20},
}), 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)