979b7f72d5
Slukker spilleren en produktion, hvis vare et tændt flow kører (manuel vare eller målet tager den i auto), en stald æder eller en aktiv kæde bruger, kommer én besked pr. slukning med brugere og lagerdage, og en rød post 'Slukket men nødvendig' i Kræver handling og statusvinduet så længe den er slukket og nødvendig. Ingen advarsel når en anden tændt produktion dækker, lageret rækker over 60 dage, kæden selv slukkede den eller den var slukket ved indlæsning. Tjekket kører i kædens tick også uden kæder; ren logik i adDependency.lua.
1162 lines
57 KiB
Lua
1162 lines
57 KiB
Lua
-- AD Sources
|
||
-- Kilder til udkørsel: hvad står der ved en AD-markør (fabrik, silo eller objektlager),
|
||
-- hvilke varer rummer/producerer den, hvor meget, og hvordan tages paller ud af et
|
||
-- objektlager. Spil-adapter — de rene beslutninger ligger i adOutboundPlanner.
|
||
|
||
ADSources = {}
|
||
|
||
-- ADCache-opslag der også virker uden adCache (tests)
|
||
local function cacheGet(bucket, key, ttlMs, fn)
|
||
if ADCacheGet == nil then return fn() end
|
||
return ADCacheGet(bucket, key, ttlMs, fn)
|
||
end
|
||
|
||
|
||
ADSources.LOG_PREFIX = "ADSmartPickup: "
|
||
-- Objektlager tættere end dette på markøren regnes som markørens (samme som triggere).
|
||
ADSources.MAX_DISTANCE = 40
|
||
-- Paller der står inden for denne afstand af fabrikkens pallespawner tæller som fabrikkens output.
|
||
ADSources.PALLET_RADIUS = 25
|
||
|
||
-- Staldens egne pallespawnere (hønsehus: æg, fårestald: uld): {[fillType] = spawner}.
|
||
local function getHusbandryPalletSpawners(placeable)
|
||
local spec = placeable ~= nil and placeable.spec_husbandryPallets or nil
|
||
return spec ~= nil and spec.fillTypeIndexToPalletSpawner or nil
|
||
end
|
||
|
||
|
||
local function distance2(ax, az, bx, bz)
|
||
local dx, dz = ax - bx, az - bz
|
||
return dx * dx + dz * dz
|
||
end
|
||
|
||
local function getNodePosition(node)
|
||
if node == nil or (entityExists ~= nil and not entityExists(node)) then
|
||
return nil
|
||
end
|
||
local x, _, z = getWorldTranslation(node)
|
||
return x, z
|
||
end
|
||
|
||
local function getPlaceableName(placeable, fallback)
|
||
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
|
||
if own ~= nil then return own end
|
||
if placeable ~= nil and placeable.getName ~= nil then
|
||
local ok, name = pcall(placeable.getName, placeable)
|
||
if ok and name ~= nil then
|
||
return tostring(name)
|
||
end
|
||
end
|
||
return fallback
|
||
end
|
||
|
||
local function getProductionPoint(placeable)
|
||
return placeable ~= nil and placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
|
||
end
|
||
|
||
-- Varer der KAN stå på paller (spillets fillType-definition: isPalletType eller pallefil).
|
||
-- Sukker og mel er palletyper, men læsses som bulk ved en fabrik med læssestation — så om
|
||
-- varen er palle eller bulk afgøres pr. kilde i isPalletAt, ikke her.
|
||
function ADSources.isPalletFillType(fillType)
|
||
local desc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
||
return desc ~= nil and (desc.isPalletType == true or desc.palletFilename ~= nil)
|
||
end
|
||
|
||
local function stationSupports(station, fillType)
|
||
if station == nil then
|
||
return false
|
||
end
|
||
if station.supportedFillTypes ~= nil then
|
||
return station.supportedFillTypes[fillType] == true
|
||
end
|
||
if station.getIsFillTypeSupported ~= nil then
|
||
local ok, value = pcall(station.getIsFillTypeSupported, station, fillType)
|
||
return ok and value == true
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Palle eller bulk ved DENNE kilde: silo = bulk; objektlager = palle; fabrik = bulk hvis
|
||
-- markørens læssestation udleverer varen, ellers palle hvis varen er en palletype.
|
||
function ADSources.isPalletAt(source, fillType)
|
||
if source.kind == "husbandry" then
|
||
local spawners = getHusbandryPalletSpawners(source.placeable)
|
||
return spawners ~= nil and spawners[fillType] ~= nil
|
||
elseif source.kind == "silo" then
|
||
return false
|
||
elseif source.kind == "objectStorage" then
|
||
return true
|
||
end
|
||
local productionPoint = getProductionPoint(source.placeable)
|
||
local loadingStation = productionPoint ~= nil and productionPoint.loadingStation or nil
|
||
if stationSupports(source.station, fillType) or stationSupports(loadingStation, fillType) then
|
||
return false
|
||
end
|
||
return ADSources.isPalletFillType(fillType)
|
||
end
|
||
|
||
-- Placeable'ns "nærmeste punkt" for kildegenkendelse: pallespawnerens første plads (fabrik,
|
||
-- stald med paller) eller rodnoden.
|
||
local function getPlaceableAnchor(placeable)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil
|
||
if spawner == nil then
|
||
for _, husbandrySpawner in pairs(getHusbandryPalletSpawners(placeable) or {}) do spawner = husbandrySpawner break end
|
||
end
|
||
local place = spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] or nil
|
||
local node = place ~= nil and place.startNode or placeable.rootNode
|
||
local x, z = getNodePosition(node)
|
||
if x == nil and node ~= placeable.rootNode then
|
||
x, z = getNodePosition(placeable.rootNode)
|
||
end
|
||
return x, z
|
||
end
|
||
|
||
-- Nærmeste placeable inden for MAX_DISTANCE af waypointet som farmen må bruge og predicate godkender.
|
||
local function findPlaceableNear(wayPoint, farmId, predicate)
|
||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||
local access = g_currentMission ~= nil and g_currentMission.accessHandler or nil
|
||
local best, bestDistance = nil, ADSources.MAX_DISTANCE * ADSources.MAX_DISTANCE
|
||
for _, placeable in pairs(placeables) do
|
||
if predicate(placeable) and (access == nil or access:canFarmAccess(farmId, placeable)) then
|
||
local x, z = getPlaceableAnchor(placeable)
|
||
if x ~= nil then
|
||
local d = distance2(wayPoint.x, wayPoint.z, x, z)
|
||
if d < bestDistance then
|
||
best, bestDistance = placeable, d
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- (afstand i meter som 2. værdi)
|
||
return best, best ~= nil and math.sqrt(bestDistance) or nil
|
||
end
|
||
|
||
local function isObjectStorage(placeable)
|
||
return placeable.spec_objectStorage ~= nil
|
||
end
|
||
|
||
local function isProductionPoint(placeable)
|
||
return getProductionPoint(placeable) ~= nil
|
||
end
|
||
|
||
-- {kind = "factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} eller nil.
|
||
function ADSources.identify(adEnv, wayPointId, farmId)
|
||
return cacheGet("identify", tostring(wayPointId) .. ":" .. tostring(farmId), 10000, function()
|
||
return ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||
end)
|
||
end
|
||
|
||
function ADSources.identifyUncached(adEnv, wayPointId, farmId)
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||
if wayPoint == nil then
|
||
return nil
|
||
end
|
||
local marker = adEnv.ADGraphManager.getMapMarkerByWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(wayPointId) or nil
|
||
-- Wait-punkter er ventepladser, aldrig kilder eller lagermål — også når de står tæt på et lager
|
||
if marker ~= nil and ADSupplyPlanner ~= nil and ADSupplyPlanner.isWaitMarker ~= nil and ADSupplyPlanner.isWaitMarker(marker) then
|
||
return nil
|
||
end
|
||
local markerName = marker ~= nil and tostring(marker.name) or tostring(wayPointId)
|
||
local station = ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||
local placeable = station ~= nil and station.owningPlaceable or nil
|
||
if placeable ~= nil and getProductionPoint(placeable) ~= nil then
|
||
return {kind = "factory", placeable = placeable, station = station, wayPointId = wayPointId, name = getPlaceableName(placeable, markerName)}
|
||
end
|
||
if placeable ~= nil and placeable.spec_silo ~= nil then
|
||
return {kind = "silo", placeable = placeable, station = station, wayPointId = wayPointId, name = getPlaceableName(placeable, markerName)}
|
||
end
|
||
-- stald: mælketank, gyllebeholder m.m. (læssestation på en husbandry-placeable)
|
||
if placeable ~= nil and placeable.getHusbandryFillLevel ~= nil then
|
||
return {kind = "husbandry", placeable = placeable, station = station, wayPointId = wayPointId, name = getPlaceableName(placeable, markerName)}
|
||
end
|
||
-- generelt lager: anden bygning med læssestation (fx separatorhal med digestat); varer fra stationen
|
||
if placeable ~= nil and placeable.spec_objectStorage == nil then
|
||
return {kind = "storage", placeable = placeable, station = station, wayPointId = wayPointId, name = getPlaceableName(placeable, markerName)}
|
||
end
|
||
-- Det strenge opslag (trigger bag næsen) passer til silorækker; en mælketank eller fabrik ligger
|
||
-- ofte ved siden af markøren. Løst opslag inden for 40 m — men aldrig for siloer.
|
||
-- stald med egne pallespawnere (hønsehus: æg) — markøren står ved spawneren, ingen læssestation
|
||
local palletBarn, palletBarnDistance = findPlaceableNear(wayPoint, farmId, function(placeable) return placeable.getHusbandryFillLevel ~= nil and getHusbandryPalletSpawners(placeable) ~= nil end)
|
||
if placeable == nil and ADSmartPickup.getLoadStationNearWayPointId ~= nil then
|
||
local loose, looseDistance = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE)
|
||
local loosePlaceable = loose ~= nil and loose.owningPlaceable or nil
|
||
-- et nærmere hønsehus vinder over en fjernere læssestation (gyllestation 23 m fra "Æg Pickup 1")
|
||
if palletBarn ~= nil and looseDistance ~= nil and palletBarnDistance ~= nil and palletBarnDistance < looseDistance then
|
||
loosePlaceable = nil
|
||
end
|
||
if loosePlaceable ~= nil and loosePlaceable.spec_silo == nil then
|
||
if getProductionPoint(loosePlaceable) ~= nil then
|
||
return {kind = "factory", placeable = loosePlaceable, station = loose, wayPointId = wayPointId, name = getPlaceableName(loosePlaceable, markerName)}
|
||
elseif loosePlaceable.getHusbandryFillLevel ~= nil then
|
||
return {kind = "husbandry", placeable = loosePlaceable, station = loose, wayPointId = wayPointId, name = getPlaceableName(loosePlaceable, markerName)}
|
||
end
|
||
end
|
||
end
|
||
if palletBarn ~= nil then
|
||
return {kind = "husbandry", placeable = palletBarn, station = nil, wayPointId = wayPointId, name = getPlaceableName(palletBarn, markerName)}
|
||
end
|
||
-- Rene pallefabrikker (halmvarer, bageri ...) har ingen læssestation: find dem på afstand
|
||
-- til pallespawneren; dernæst objektlagre.
|
||
local factory = findPlaceableNear(wayPoint, farmId, isProductionPoint)
|
||
if factory ~= nil then
|
||
-- produktionspunktets egen læssestation (bulk-outputs), også når markøren ikke rammer triggeren
|
||
local loadingStation = getProductionPoint(factory).loadingStation
|
||
return {kind = "factory", placeable = factory, station = loadingStation, wayPointId = wayPointId, name = getPlaceableName(factory, markerName)}
|
||
end
|
||
local storage = findPlaceableNear(wayPoint, farmId, isObjectStorage)
|
||
if storage ~= nil then
|
||
return {kind = "objectStorage", placeable = storage, station = nil, wayPointId = wayPointId, name = getPlaceableName(storage, markerName)}
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- Til advarslen "kilde-markør X er ikke …": hvad opslaget faktisk ser ved markøren.
|
||
function ADSources.explainMarker(adEnv, wayPointId, farmId)
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||
if wayPoint == nil then
|
||
return "markøren har intet waypoint (slettet?)"
|
||
end
|
||
local station = ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
|
||
if station == nil then
|
||
local loose = ADSmartPickup.getLoadStationNearWayPointId ~= nil and ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE) or nil
|
||
if loose ~= nil and loose.owningPlaceable ~= nil and loose.owningPlaceable.spec_silo ~= nil then
|
||
return string.format("nærmeste læssestation inden for 40 m er en silo ('%s'), som kræver at markøren står i køresporet med triggeren bag næsen", getPlaceableName(loose.owningPlaceable, "?"))
|
||
end
|
||
local factory = findPlaceableNear(wayPoint, farmId, isProductionPoint)
|
||
local storage = findPlaceableNear(wayPoint, farmId, isObjectStorage)
|
||
if factory == nil and storage == nil then
|
||
return "ingen læssestation ved markøren, og ingen fabrik/objektlager inden for 40 m"
|
||
end
|
||
return string.format("ingen læssestation ved markøren; nærmeste bygning: %s", getPlaceableName(factory or storage, "?"))
|
||
end
|
||
local placeable = station.owningPlaceable
|
||
if placeable == nil then
|
||
return "læssestation ved markøren uden bygning (owningPlaceable = nil)"
|
||
end
|
||
local kind = "ukendt bygningstype"
|
||
if getProductionPoint(placeable) ~= nil then kind = "fabrik"
|
||
elseif placeable.spec_silo ~= nil then kind = "silo"
|
||
elseif placeable.getHusbandryFillLevel ~= nil then kind = "stald"
|
||
elseif placeable.spec_objectStorage ~= nil then kind = "objektlager" end
|
||
return string.format("læssestation ved markøren hører til '%s' (%s)", getPlaceableName(placeable, "?"), kind)
|
||
end
|
||
|
||
local function isPalletVehicle(vehicle)
|
||
return vehicle ~= nil and (vehicle.spec_pallet ~= nil or vehicle.isPallet == true)
|
||
end
|
||
|
||
local function getPalletContent(vehicle)
|
||
if vehicle.getFillUnitFillType ~= nil and vehicle.getFillUnitFillLevel ~= nil then
|
||
return vehicle:getFillUnitFillType(1), vehicle:getFillUnitFillLevel(1) or 0
|
||
end
|
||
local units = vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits or nil
|
||
if units ~= nil and units[1] ~= nil then
|
||
return units[1].fillType, units[1].fillLevel or 0
|
||
end
|
||
return nil, 0
|
||
end
|
||
|
||
-- Spawnerens område som segmenter {ax, az, bx, bz}: hver spawnPlace fra start til slut (startNode/endNode,
|
||
-- eller PlacementUtil-felterne startX/startZ + dirX/dirZ*width). Et hønsehus har to rækker; måles kun
|
||
-- til første række, ligger den anden række tættere på naboens start end husets egen.
|
||
local function spawnerSegments(spawner)
|
||
local segments = {}
|
||
for _, place in ipairs(spawner ~= nil and spawner.spawnPlaces or {}) do
|
||
local ax, az = getNodePosition(place.startNode)
|
||
local bx, bz = getNodePosition(place.endNode)
|
||
if ax == nil and place.startX ~= nil and place.startZ ~= nil then
|
||
ax, az = place.startX, place.startZ
|
||
if place.dirX ~= nil and place.dirZ ~= nil and place.width ~= nil then
|
||
bx, bz = ax + place.dirX * place.width, az + place.dirZ * place.width
|
||
end
|
||
end
|
||
if ax ~= nil then table.insert(segments, {ax, az, bx or ax, bz or az}) end
|
||
end
|
||
return segments
|
||
end
|
||
|
||
-- Kvadreret afstand fra (x, z) til nærmeste punkt på et af segmenterne.
|
||
local function segmentsDistance2(segments, x, z)
|
||
local best = math.huge
|
||
for _, s in ipairs(segments) do
|
||
local ax, az, bx, bz = s[1], s[2], s[3], s[4]
|
||
local dx, dz = bx - ax, bz - az
|
||
local len2 = dx * dx + dz * dz
|
||
local t = len2 > 0 and math.max(0, math.min(1, ((x - ax) * dx + (z - az) * dz) / len2)) or 0
|
||
local d = distance2(x, z, ax + dx * t, az + dz * t)
|
||
if d < best then best = d end
|
||
end
|
||
return best
|
||
end
|
||
|
||
-- Alle pallespawnere der kan lave varen (stalde med spawner for fillType, fabrikkers pallespawner) som
|
||
-- {placeable, segments}. Står bygningerne tættere end PALLET_RADIUS (hønsehuse på række), tilhører en
|
||
-- palle den spawner hvis område er nærmest — ellers tæller et tomt hus naboernes paller med.
|
||
local function findRivalSpawners(fillType)
|
||
local rivals = {}
|
||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||
for _, placeable in pairs(placeables) do
|
||
local husbandrySpawners = getHusbandryPalletSpawners(placeable)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
local spawner = husbandrySpawners ~= nil and husbandrySpawners[fillType] or (productionPoint ~= nil and productionPoint.palletSpawner or nil)
|
||
local segments = spawnerSegments(spawner)
|
||
if #segments > 0 then table.insert(rivals, {placeable = placeable, segments = segments}) end
|
||
end
|
||
return rivals
|
||
end
|
||
|
||
local function isNearestSpawner(rivals, placeable, own, x, z)
|
||
for _, rival in ipairs(rivals) do
|
||
if rival.placeable ~= placeable and segmentsDistance2(rival.segments, x, z) < own then return false end
|
||
end
|
||
return true
|
||
end
|
||
|
||
ADSources.lastPalletNote = {}
|
||
|
||
-- Liter af varen på paller ved spawneren (pallespawneren tømmer lageret ud på jorden løbende, så
|
||
-- det er dér varen fysisk er). spawner = staldens spawner for varen; nil = fabrikkens pallespawner.
|
||
local function getPalletsNearSpawner(placeable, fillType, farmId, spawner)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
local segments = spawnerSegments(spawner or (productionPoint ~= nil and productionPoint.palletSpawner or nil))
|
||
if #segments == 0 then
|
||
local rx, rz = getNodePosition(placeable.rootNode)
|
||
if rx == nil then return 0, 0 end
|
||
segments = {{rx, rz, rx, rz}}
|
||
end
|
||
local rivals = cacheGet("palletSpawners", fillType, 10000, function() return findRivalSpawners(fillType) end)
|
||
local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {}
|
||
local total, count, lost = 0, 0, 0
|
||
local radius2 = ADSources.PALLET_RADIUS * ADSources.PALLET_RADIUS
|
||
for _, vehicle in pairs(vehicles) do
|
||
if isPalletVehicle(vehicle) and (vehicle.getOwnerFarmId == nil or vehicle:getOwnerFarmId() == farmId) then
|
||
local palletFillType, level = getPalletContent(vehicle)
|
||
local x, z = getNodePosition(vehicle.rootNode)
|
||
local own = (palletFillType == fillType and x ~= nil) and segmentsDistance2(segments, x, z) or math.huge
|
||
if own <= radius2 then
|
||
if isNearestSpawner(rivals, placeable, own, x, z) then
|
||
total = total + level
|
||
count = count + 1
|
||
else
|
||
lost = lost + 1
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- diagnose: paller inden for rækkevidde, men alle tildelt en nabo (højst én gang pr. minut pr. bygning)
|
||
local now = g_currentMission ~= nil and g_currentMission.time or nil
|
||
if count == 0 and lost > 0 and now ~= nil and (ADSources.lastPalletNote[placeable] or -math.huge) + 60000 < now then
|
||
ADSources.lastPalletNote[placeable] = now
|
||
Logging.info(ADSources.LOG_PREFIX .. "%s: %d paller inden for %d m tilhører nabo-spawnere (0 egne)", getPlaceableName(placeable, "?"), lost, ADSources.PALLET_RADIUS)
|
||
end
|
||
return total, count
|
||
end
|
||
|
||
-- canPallet: varen er en palletype (kan stå på paller); palletLiters: liter på paller ved
|
||
-- kilden når varen ELLERS læsses som bulk (fabrik med både læssestation og pallespawner).
|
||
local function newProduct(fillType, available, level, capacity, isPallet)
|
||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||
local title = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
|
||
return {fillType = fillType, name = name, title = title or name, available = available, level = level, capacity = capacity, isPallet = isPallet, canPallet = isPallet or ADSources.isPalletFillType(fillType), palletLiters = 0, palletCount = 0}
|
||
end
|
||
|
||
local function getFactoryProducts(source, farmId)
|
||
local productionPoint = getProductionPoint(source.placeable)
|
||
local storage = productionPoint ~= nil and productionPoint.storage or nil
|
||
local seen, products = {}, {}
|
||
for _, production in ipairs(productionPoint ~= nil and productionPoint.productions or {}) do
|
||
for _, output in ipairs(production.outputs or {}) do
|
||
local fillType = output.type
|
||
if fillType ~= nil and not seen[fillType] then
|
||
seen[fillType] = true
|
||
local level = storage ~= nil and storage.getFillLevel ~= nil and (storage:getFillLevel(fillType) or 0) or 0
|
||
local capacity = storage ~= nil and storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0
|
||
local isPallet = ADSources.isPalletAt(source, fillType)
|
||
local palletLiters, palletCount = 0, 0
|
||
if ADSources.isPalletFillType(fillType) then
|
||
palletLiters, palletCount = getPalletsNearSpawner(source.placeable, fillType, farmId)
|
||
end
|
||
local available = level + (isPallet and palletLiters or 0)
|
||
local product = newProduct(fillType, available, level, capacity, isPallet)
|
||
product.palletLiters, product.palletCount = palletLiters, palletCount
|
||
table.insert(products, product)
|
||
end
|
||
end
|
||
end
|
||
return products
|
||
end
|
||
|
||
local function getSiloProducts(source)
|
||
local storages = source.placeable.spec_silo ~= nil and source.placeable.spec_silo.storages or {}
|
||
local levels, capacities = {}, {}
|
||
for _, storage in pairs(storages) do
|
||
for fillType, level in pairs(storage.fillLevels or {}) do
|
||
if (level or 0) > 0 then
|
||
levels[fillType] = (levels[fillType] or 0) + level
|
||
capacities[fillType] = (capacities[fillType] or 0) + (storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0)
|
||
end
|
||
end
|
||
end
|
||
-- Udvidelses-lagre (fx mælketanke med isExtension) deles af alle læssestationer inden for
|
||
-- storageRadius: tanken selv kan være tom mens puljen er fuld. Læssestationens tal er det man kan hente.
|
||
local station = source.station
|
||
if station ~= nil and station.getAllFillLevels ~= nil then
|
||
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||
local ok, pooled, pooledCapacity = pcall(station.getAllFillLevels, station, farmId)
|
||
if ok and type(pooled) == "table" then
|
||
for fillType, level in pairs(pooled) do
|
||
if (level or 0) > (levels[fillType] or 0) then
|
||
levels[fillType] = level
|
||
capacities[fillType] = math.max(capacities[fillType] or 0, tonumber(pooledCapacity) or 0, level)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
local products = {}
|
||
for fillType, level in pairs(levels) do
|
||
table.insert(products, newProduct(fillType, level, level, capacities[fillType] or 0, false))
|
||
end
|
||
table.sort(products, function(a, b) return a.fillType < b.fillType end)
|
||
return products
|
||
end
|
||
|
||
-- Et objektlagers post: {fillType, fillLevel pr. objekt} for paller og baller.
|
||
local function getObjectInfoContent(objectInfo)
|
||
local object = objectInfo.objects ~= nil and objectInfo.objects[1] or nil
|
||
if object == nil then
|
||
return nil, 0
|
||
end
|
||
local attributes = object.palletAttributes or object.baleAttributes
|
||
if attributes ~= nil then
|
||
return attributes.fillType, attributes.fillLevel or 0
|
||
end
|
||
return object.fillType, object.fillLevel or 0
|
||
end
|
||
|
||
local function getObjectStorageProducts(source)
|
||
local spec = source.placeable.spec_objectStorage
|
||
local products = {}
|
||
for _, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
|
||
local fillType, perObject = getObjectInfoContent(objectInfo)
|
||
local count = objectInfo.numObjects or 0
|
||
if fillType ~= nil and count > 0 then
|
||
local product = newProduct(fillType, count * perObject, spec.numStoredObjects or count, spec.capacity or 0, true)
|
||
product.palletCount = count
|
||
table.insert(products, product)
|
||
end
|
||
end
|
||
return products
|
||
end
|
||
|
||
-- Stald: det læssestationen udleverer (mælk, gylle ...) med staldens eget niveau/kapacitet.
|
||
-- Varerne det læssepunkt udleverer som står nærmest markøren (en stald kan have mælk i den ene ende
|
||
-- og gylle i den anden på samme station). nil når punktet ikke opgiver sine varer.
|
||
local function getNearestTriggerFillTypes(adEnv, source)
|
||
local station = source.station
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(source.wayPointId)
|
||
if station == nil or wayPoint == nil or station.loadTriggers == nil then
|
||
return nil
|
||
end
|
||
local best, bestDistance = nil, math.huge
|
||
for _, trigger in pairs(station.loadTriggers) do
|
||
local x, z = getNodePosition(trigger.triggerNode)
|
||
if x ~= nil then
|
||
local d = distance2(wayPoint.x, wayPoint.z, x, z)
|
||
if d < bestDistance then best, bestDistance = trigger, d end
|
||
end
|
||
end
|
||
if best == nil or type(best.fillTypes) ~= "table" or next(best.fillTypes) == nil then
|
||
return nil
|
||
end
|
||
local list = {}
|
||
for key, value in pairs(best.fillTypes) do
|
||
-- FS25 gemmer som sæt {[ft] = true}; nogle mods som liste {ft, ...}
|
||
if value == true then table.insert(list, key) elseif type(value) == "number" then table.insert(list, value) end
|
||
end
|
||
return #list > 0 and list or nil
|
||
end
|
||
|
||
local function getHusbandryPalletProducts(source, farmId)
|
||
local products = {}
|
||
local spec = source.placeable.spec_husbandryPallets
|
||
for fillType, spawner in pairs(getHusbandryPalletSpawners(source.placeable) or {}) do
|
||
local liters, count = getPalletsNearSpawner(source.placeable, fillType, farmId, spawner)
|
||
local capacity = spec ~= nil and spec.capacities ~= nil and spec.capacities[fillType] or 0
|
||
local product = newProduct(fillType, liters, liters, math.max(capacity or 0, liters), true)
|
||
product.palletLiters, product.palletCount = liters, count
|
||
table.insert(products, product)
|
||
end
|
||
table.sort(products, function(a, b) return a.fillType < b.fillType end)
|
||
return products
|
||
end
|
||
|
||
local function getHusbandryProducts(adEnv, source)
|
||
local placeable, station = source.placeable, source.station
|
||
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||
if station == nil then
|
||
-- kilde via staldens pallespawner (hønsehus: æg): kun pallevarerne
|
||
return getHusbandryPalletProducts(source, farmId)
|
||
end
|
||
local fillTypes = getNearestTriggerFillTypes(adEnv, source) or {}
|
||
if #fillTypes == 0 then
|
||
for fillType, supported in pairs(station ~= nil and station.supportedFillTypes or {}) do
|
||
if supported == true then table.insert(fillTypes, fillType) end
|
||
end
|
||
end
|
||
if #fillTypes == 0 and station ~= nil and station.getIsFillTypeSupported ~= nil and g_fillTypeManager ~= nil then
|
||
for _, name in ipairs({"MILK", "GOATMILK", "BUFFALOMILK", "SHEEPMILK", "LIQUIDMANURE", "MANURE", "DIGESTATE"}) do
|
||
local fillType = g_fillTypeManager.getFillTypeIndexByName ~= nil and g_fillTypeManager:getFillTypeIndexByName(name) or nil
|
||
if fillType ~= nil then
|
||
local ok, value = pcall(station.getIsFillTypeSupported, station, fillType)
|
||
if ok and value == true then table.insert(fillTypes, fillType) end
|
||
end
|
||
end
|
||
end
|
||
table.sort(fillTypes)
|
||
-- Læssestationen trækker fra staldens beholder OG tilknyttede udvidelser (fx Milk Tank Extension,
|
||
-- en siloExtension inden for storageRadius). getHusbandryFillLevel ser kun staldens egen beholder.
|
||
local stationLevels = nil
|
||
if station ~= nil and station.getAllFillLevels ~= nil then
|
||
local ok, levels = pcall(station.getAllFillLevels, station, g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1)
|
||
if ok and type(levels) == "table" then stationLevels = levels end
|
||
end
|
||
local products = {}
|
||
for _, fillType in ipairs(fillTypes) do
|
||
local level = math.max(placeable:getHusbandryFillLevel(fillType) or 0, stationLevels ~= nil and stationLevels[fillType] or 0)
|
||
local capacity = placeable.getHusbandryCapacity ~= nil and (placeable:getHusbandryCapacity(fillType) or 0) or 0
|
||
if capacity <= 0 and placeable.getHusbandryFreeCapacity ~= nil then
|
||
capacity = level + (placeable:getHusbandryFreeCapacity(fillType) or 0)
|
||
end
|
||
table.insert(products, newProduct(fillType, level, level, math.max(capacity, level), false))
|
||
end
|
||
return products
|
||
end
|
||
|
||
-- Generelt lager: det læssestationen har af de varer den udleverer (supportedFillTypes/triggeres fillTypes;
|
||
-- ukendt = alt med niveau). Kapacitet = stationens samlede.
|
||
local function getStationProducts(source)
|
||
local station = source.station
|
||
if station == nil or station.getAllFillLevels == nil then
|
||
return {}
|
||
end
|
||
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||
local ok, levels, capacity = pcall(station.getAllFillLevels, station, farmId)
|
||
if not ok or type(levels) ~= "table" then
|
||
return {}
|
||
end
|
||
local supported = station.supportedFillTypes
|
||
local products = {}
|
||
for fillType, level in pairs(levels) do
|
||
local isSupported = supported == nil or next(supported) == nil or supported[fillType] == true
|
||
if isSupported and (level or 0) > 0 then
|
||
table.insert(products, newProduct(fillType, level, level, math.max(tonumber(capacity) or 0, level), false))
|
||
end
|
||
end
|
||
table.sort(products, function(a, b) return a.fillType < b.fillType end)
|
||
return products
|
||
end
|
||
|
||
-- Varer ved kilden med tilgængelig mængde: { {fillType, name, title, available, level, capacity, isPallet} }.
|
||
function ADSources.getProducts(adEnv, source, farmId)
|
||
if source.kind == "factory" then
|
||
return getFactoryProducts(source, farmId)
|
||
elseif source.kind == "silo" then
|
||
return getSiloProducts(source)
|
||
elseif source.kind == "husbandry" then
|
||
return getHusbandryProducts(adEnv, source)
|
||
elseif source.kind == "storage" then
|
||
return getStationProducts(source)
|
||
elseif source.kind == "objectStorage" then
|
||
return getObjectStorageProducts(source)
|
||
end
|
||
return {}
|
||
end
|
||
|
||
-- Objektlager: objectInfo-indeks + antal for varen; nil når lageret ikke har den.
|
||
function ADSources.getObjectInfoIndex(source, fillType)
|
||
local spec = source.placeable ~= nil and source.placeable.spec_objectStorage or nil
|
||
for index, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
|
||
local infoFillType = getObjectInfoContent(objectInfo)
|
||
if infoFillType == fillType and (objectInfo.numObjects or 0) > 0 then
|
||
return index, objectInfo.numObjects
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
function ADSources.countStored(source, fillType)
|
||
local _, count = ADSources.getObjectInfoIndex(source, fillType)
|
||
return count or 0
|
||
end
|
||
|
||
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
|
||
-- AutomaticBaleStorage-moddens kø (der kun fanger klient-kald) ikke blander sig.
|
||
function ADSources.spawnOne(source, objectInfoIndex)
|
||
local placeable = source.placeable
|
||
if placeable == nil or placeable.removeAbstractObjectsFromStorage == nil then
|
||
return false
|
||
end
|
||
local ok = pcall(placeable.removeAbstractObjectsFromStorage, placeable, objectInfoIndex, 1, nil)
|
||
return ok == true
|
||
end
|
||
|
||
-- acceptedFillTypes rummer kun bulk-triggerens varer; palletriggerens varer ligger i
|
||
-- supportedFillTypes (det AutoDrive selv slår op i). Begge tæller.
|
||
function ADSources.stationAccepts(station, fillType)
|
||
if station.acceptedFillTypes ~= nil and station.acceptedFillTypes[fillType] == true then
|
||
return true
|
||
end
|
||
if station.supportedFillTypes ~= nil and station.supportedFillTypes[fillType] == true then
|
||
return true
|
||
end
|
||
if station.getIsFillTypeSupported ~= nil then
|
||
local ok, value = pcall(station.getIsFillTypeSupported, station, fillType)
|
||
return ok and value == true
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Til loggen: navnene på de varer stationen tager (sorteret), fx "SUGAR, STRAW_BASKET".
|
||
function ADSources.describeAccepted(station)
|
||
local set = {}
|
||
for _, field in ipairs({"acceptedFillTypes", "supportedFillTypes"}) do
|
||
for fillType, accepted in pairs(station[field] or {}) do
|
||
if accepted == true then set[fillType] = true end
|
||
end
|
||
end
|
||
local names = {}
|
||
for fillType in pairs(set) do
|
||
table.insert(names, g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType))
|
||
end
|
||
table.sort(names, function(a, b)
|
||
-- stabil rækkefølge efter indeks hvor det kendes, ellers navn
|
||
return tostring(a) < tostring(b)
|
||
end)
|
||
return table.concat(names, ", ")
|
||
end
|
||
|
||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
||
function ADSources.getSellMarkers(adEnv, farmId)
|
||
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
||
end
|
||
|
||
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
|
||
table.insert(result, {marker = marker, station = station})
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
|
||
-- ---- paller til lager: objektlager (palleskur) som mål ----
|
||
|
||
function ADSources.objectStorageAccepts(placeable, fillType)
|
||
local spec = placeable ~= nil and placeable.spec_objectStorage or nil
|
||
if spec == nil or spec.supportsPallets == false then
|
||
return false
|
||
end
|
||
if placeable.getObjectStorageSupportsFillType ~= nil then
|
||
local ok, value = pcall(placeable.getObjectStorageSupportsFillType, placeable, fillType)
|
||
return ok and value == true
|
||
end
|
||
return true
|
||
end
|
||
|
||
-- Ledige pladser (objekter); uendeligt når lageret ingen kapacitet opgiver.
|
||
function ADSources.objectStorageFreeSlots(placeable)
|
||
local spec = placeable ~= nil and placeable.spec_objectStorage or nil
|
||
if spec == nil then
|
||
return 0
|
||
end
|
||
local capacity = spec.capacity or 0
|
||
if capacity <= 0 then
|
||
return math.huge
|
||
end
|
||
local stored = spec.storedObjects ~= nil and #spec.storedObjects or (spec.numStoredObjects or 0)
|
||
return math.max(0, capacity - stored)
|
||
end
|
||
|
||
-- Nærmeste markør ved et objektlager der tager varen og har plads, med rute fra traktoren;
|
||
-- kilderne selv udelukkes. {wayPointId, name, free = pladser} eller nil.
|
||
function ADSources.findPalletStore(adEnv, vehicle, fillType, excludedPlaceables, isReachable, farmId)
|
||
local x, z = getNodePosition(vehicle.rootNode)
|
||
if x == nil then
|
||
return nil
|
||
end
|
||
local candidates = {}
|
||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||
local source = ADSources.identify(adEnv, marker.id, farmId)
|
||
if source ~= nil and source.kind == "objectStorage" and not (excludedPlaceables ~= nil and excludedPlaceables[source.placeable])
|
||
and ADSources.objectStorageAccepts(source.placeable, fillType) and ADSources.objectStorageFreeSlots(source.placeable) >= 1 then
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||
table.insert(candidates, {marker = marker, source = source, distance = distance2(x, z, wayPoint.x, wayPoint.z)})
|
||
end
|
||
end
|
||
table.sort(candidates, function(a, b) return a.distance < b.distance end)
|
||
for _, candidate in ipairs(candidates) do
|
||
if isReachable == nil or isReachable(candidate.marker.id) then
|
||
return {wayPointId = candidate.marker.id, name = tostring(candidate.marker.name), free = ADSources.objectStorageFreeSlots(candidate.source.placeable)}
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- ---- reserve: hvad gårdens egne fabrikker og stalde skal bruge det næste døgn ----
|
||
|
||
ADSources.RESERVE_HOURS = 24
|
||
|
||
-- Fælles ejerregel (ADPrices): egen ejer eller jord-bundet kortanlæg på gårdens jord.
|
||
local function isOwnedBy(placeable, farmId)
|
||
if ADPrices ~= nil and ADPrices.isOwnedBy ~= nil then
|
||
return ADPrices.isOwnedBy(placeable, farmId)
|
||
end
|
||
if placeable.getOwnerFarmId ~= nil then
|
||
local ok, owner = pcall(placeable.getOwnerFarmId, placeable)
|
||
return ok and owner == farmId
|
||
end
|
||
return placeable.ownerFarmId == farmId
|
||
end
|
||
|
||
local function isProductionWanted(productionPoint, production)
|
||
local isEnabled = false
|
||
if productionPoint.getIsProductionEnabled ~= nil then
|
||
isEnabled = productionPoint:getIsProductionEnabled(production.id) == true
|
||
else
|
||
for _, active in ipairs(productionPoint.activeProductions or {}) do
|
||
if active == production then isEnabled = true end
|
||
end
|
||
end
|
||
local missingInputs = ProductionPoint ~= nil and ProductionPoint.PROD_STATUS ~= nil and ProductionPoint.PROD_STATUS.MISSING_INPUTS or nil
|
||
return isEnabled or (missingInputs ~= nil and production.status == missingInputs)
|
||
end
|
||
|
||
-- Fabrik: døgnforbrug af varen i ønskede produktioner minus det den har, højst fri plads.
|
||
local function getProductionReserve(placeable, fillType)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
if productionPoint == nil or productionPoint.storage == nil then
|
||
return 0
|
||
end
|
||
local perMinute = 0
|
||
for _, production in ipairs(productionPoint.productions or {}) do
|
||
if isProductionWanted(productionPoint, production) then
|
||
for _, input in ipairs(production.inputs or {}) do
|
||
if input.type == fillType then
|
||
local cyclesPerMinute = production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)
|
||
perMinute = perMinute + (input.amount or 0) * cyclesPerMinute
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if perMinute <= 0 then
|
||
return 0
|
||
end
|
||
local storage = productionPoint.storage
|
||
local level = storage.getFillLevel ~= nil and (storage:getFillLevel(fillType) or 0) or ((storage.fillLevels or {})[fillType] or 0)
|
||
local free = storage.getFreeCapacity ~= nil and storage:getFreeCapacity(fillType) or nil
|
||
if free == nil then
|
||
local capacity = storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0
|
||
free = math.max(0, capacity - level)
|
||
end
|
||
local need = perMinute * 60 * ADSources.RESERVE_HOURS - level
|
||
return math.max(0, math.min(need, free))
|
||
end
|
||
|
||
-- Stald: foder/halm/vand-rate × timer minus beholdning, højst fri plads (spec-felter som
|
||
-- Distribution Redux læser dem); automatisk vandforsyning = intet behov.
|
||
-- Foder = varer i krybben ELLER i dyretypens foderdefinition (tom krybbe har ingen niveauer).
|
||
-- Mælk/gylle er staldens output og understøttes kun som lager.
|
||
local function isFoodFillType(placeable, fillType)
|
||
local food = placeable.spec_husbandryFood
|
||
if food == nil then return false end
|
||
if food.fillLevels ~= nil and food.fillLevels[fillType] ~= nil then return true end
|
||
local system = g_currentMission ~= nil and g_currentMission.animalFoodSystem or nil
|
||
local animalFood = system ~= nil and system.getAnimalFood ~= nil and system:getAnimalFood(food.animalTypeIndex) or nil
|
||
for _, group in pairs(animalFood ~= nil and animalFood.groups or {}) do
|
||
for _, candidate in pairs(group.fillTypes or {}) do
|
||
if candidate == fillType then return true end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function getHusbandryReserve(placeable, fillType)
|
||
if placeable.getHusbandryIsFillTypeSupported == nil or placeable.getHusbandryFillLevel == nil then
|
||
return 0
|
||
end
|
||
local okSupported, supported = pcall(placeable.getHusbandryIsFillTypeSupported, placeable, fillType)
|
||
if not okSupported or supported ~= true then
|
||
return 0
|
||
end
|
||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or ""
|
||
local rate, level = 0, placeable:getHusbandryFillLevel(fillType) or 0
|
||
local food, straw, water = placeable.spec_husbandryFood, placeable.spec_husbandryStraw, placeable.spec_husbandryWater
|
||
if name == "WATER" then
|
||
if water == nil or water.automaticWaterSupply then return 0 end
|
||
rate = water.litersPerHour or 0
|
||
elseif name == "STRAW" and straw ~= nil then
|
||
rate = straw.inputLitersPerHour or 0
|
||
elseif food ~= nil and isFoodFillType(placeable, fillType) then
|
||
-- kun varer i foderkrybben er foder; mælk/gylle er staldens OUTPUT (understøttet som lager)
|
||
rate = food.litersPerHour or 0
|
||
level = 0
|
||
for _, value in pairs(food.fillLevels or {}) do level = level + (value or 0) end
|
||
end
|
||
if rate <= 0 then
|
||
return 0
|
||
end
|
||
local free = placeable.getHusbandryFreeCapacity ~= nil and (placeable:getHusbandryFreeCapacity(fillType) or 0) or math.huge
|
||
return math.max(0, math.min(rate * ADSources.RESERVE_HOURS - level, free))
|
||
end
|
||
|
||
-- {liters, consumers = {navne}} — gårdens samlede behov for varen; udelukkede placeables (kilderne) tæller ikke.
|
||
function ADSources.getFarmDemand(adEnv, fillType, farmId, excludedPlaceables)
|
||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||
local total, consumers = 0, {}
|
||
for _, placeable in pairs(placeables) do
|
||
if isOwnedBy(placeable, farmId) and not (excludedPlaceables ~= nil and excludedPlaceables[placeable]) then
|
||
local reserve = getProductionReserve(placeable, fillType) + getHusbandryReserve(placeable, fillType)
|
||
if reserve > 0 then
|
||
total = total + reserve
|
||
table.insert(consumers, getPlaceableName(placeable, "?"))
|
||
end
|
||
end
|
||
end
|
||
return {liters = total, consumers = consumers}
|
||
end
|
||
|
||
-- ---- forbrug og produktion pr. døgn (fanen Lagre) ----
|
||
|
||
local function addRate(rates, fillType, field, liters)
|
||
if fillType == nil or liters == nil or liters <= 0 then return end
|
||
rates[fillType] = rates[fillType] or {consumption = 0, production = 0}
|
||
rates[fillType][field] = rates[fillType][field] + liters
|
||
end
|
||
|
||
-- Statistik: kun produktioner der kører NU. "Ønsket" (tændt eller mangler input) bruges til reserve og
|
||
-- forsyning, men ville her tælle stillestående fabrikker med fuld hastighed — fx fermenteringssiloer
|
||
-- med 200.000 cyklusser i timen gav hundreder af millioner liter pr. døgn.
|
||
local function isProductionRunning(productionPoint, production)
|
||
local status = ProductionPoint ~= nil and ProductionPoint.PROD_STATUS ~= nil and ProductionPoint.PROD_STATUS.RUNNING or nil
|
||
if status ~= nil and production.status ~= nil then
|
||
return production.status == status
|
||
end
|
||
return isProductionWanted(productionPoint, production)
|
||
end
|
||
|
||
-- Spillets rater gælder pr. måned (én 24-timers dag pr. måned); faktisk pr. døgn = rate × timeAdjustment
|
||
-- (1 / dage pr. periode).
|
||
local function getTimeAdjustment()
|
||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||
local value = environment ~= nil and tonumber(environment.timeAdjustment) or nil
|
||
return value ~= nil and value > 0 and value or 1
|
||
end
|
||
|
||
local function addFactoryRates(rates, placeable)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
for _, production in ipairs(productionPoint ~= nil and productionPoint.productions or {}) do
|
||
if isProductionRunning(productionPoint, production) then
|
||
local perDay = (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
|
||
for _, input in ipairs(production.inputs or {}) do addRate(rates, input.type, "consumption", (input.amount or 0) * perDay) end
|
||
for _, output in ipairs(production.outputs or {}) do addRate(rates, output.type, "production", (output.amount or 0) * perDay) end
|
||
end
|
||
end
|
||
end
|
||
|
||
local function getFoodWeights(placeable)
|
||
local foodSpec = placeable.spec_husbandryFood
|
||
local system = g_currentMission ~= nil and g_currentMission.animalFoodSystem or nil
|
||
local weights = {}
|
||
local food = system ~= nil and system.getAnimalFood ~= nil and foodSpec ~= nil and system:getAnimalFood(foodSpec.animalTypeIndex) or nil
|
||
for _, group in pairs(food ~= nil and food.groups or {}) do
|
||
local weight = tonumber(group.productionWeight) or tonumber(group.eatWeight) or 1
|
||
for _, fillType in pairs(group.fillTypes or {}) do weights[fillType] = math.max(weights[fillType] or 0, weight) end
|
||
end
|
||
return weights
|
||
end
|
||
|
||
local function addHusbandryRates(rates, placeable)
|
||
local food, straw, water = placeable.spec_husbandryFood, placeable.spec_husbandryStraw, placeable.spec_husbandryWater
|
||
local byName = function(name) return g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil and g_fillTypeManager:getFillTypeIndexByName(name) or nil end
|
||
local nameToIndex = function(name)
|
||
local index = byName(name)
|
||
if index == nil and g_fillTypeManager ~= nil then
|
||
for i = 1, 256 do if g_fillTypeManager:getFillTypeNameByIndex(i) == name then return i end end
|
||
end
|
||
return index
|
||
end
|
||
if food ~= nil and (food.litersPerHour or 0) > 0 then
|
||
for fillType, liters in pairs(ADStorageStats.splitTroughConsumption(food.litersPerHour * 24, food.fillLevels, getFoodWeights(placeable))) do
|
||
addRate(rates, fillType, "consumption", liters)
|
||
end
|
||
end
|
||
if straw ~= nil and (straw.inputLitersPerHour or 0) > 0 then
|
||
addRate(rates, nameToIndex("STRAW"), "consumption", straw.inputLitersPerHour * 24)
|
||
end
|
||
if water ~= nil and not water.automaticWaterSupply and (water.litersPerHour or 0) > 0 then
|
||
addRate(rates, nameToIndex("WATER"), "consumption", water.litersPerHour * 24)
|
||
end
|
||
end
|
||
|
||
-- Staldens produktion: dyrenes output-kurver (liter pr. dyr pr. døgn efter alder) × antal dyr pr.
|
||
-- klynge. Output-navne uden fillType: milk -> MILK, manure -> MANURE, liquidManure -> LIQUIDMANURE.
|
||
local OUTPUT_DEFAULTS = {milk = "MILK", manure = "MANURE", liquidManure = "LIQUIDMANURE"}
|
||
|
||
local function resolveFillType(value, outputName)
|
||
if type(value) == "number" then return value end
|
||
local name = type(value) == "string" and value or OUTPUT_DEFAULTS[outputName]
|
||
if name == nil or g_fillTypeManager == nil or g_fillTypeManager.getFillTypeIndexByName == nil then return nil end
|
||
return g_fillTypeManager:getFillTypeIndexByName(name)
|
||
end
|
||
|
||
local function curveValue(def, age)
|
||
local curve = type(def) == "table" and def.curve or def
|
||
if curve == nil or curve.get == nil then return nil end
|
||
local ok, value = pcall(curve.get, curve, age)
|
||
return ok and type(value) == "number" and value or nil
|
||
end
|
||
|
||
local function addAnimalProduction(rates, placeable)
|
||
local animalSystem = g_currentMission ~= nil and g_currentMission.animalSystem or nil
|
||
if placeable.getClusters == nil or animalSystem == nil or animalSystem.getSubTypeByIndex == nil then return end
|
||
local ok, clusters = pcall(placeable.getClusters, placeable)
|
||
if not ok or type(clusters) ~= "table" then return end
|
||
for _, cluster in ipairs(clusters) do
|
||
local subTypeIndex = cluster.getSubTypeIndex ~= nil and cluster:getSubTypeIndex() or cluster.subTypeIndex
|
||
local subType = subTypeIndex ~= nil and animalSystem:getSubTypeByIndex(subTypeIndex) or nil
|
||
local age = cluster.getAge ~= nil and cluster:getAge() or cluster.age
|
||
local count = cluster.getNumAnimals ~= nil and cluster:getNumAnimals() or cluster.numAnimals or 0
|
||
if subType ~= nil and type(subType.output) == "table" and age ~= nil and count > 0 then
|
||
for outputName, def in pairs(subType.output) do
|
||
local perAnimal = curveValue(def, age)
|
||
local fillType = resolveFillType(type(def) == "table" and (def.fillType or def.fillTypeIndex) or nil, outputName)
|
||
if perAnimal ~= nil and perAnimal > 0 then
|
||
addRate(rates, fillType, "production", perAnimal * count)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- {[fillType] = {consumption, production}} i liter pr. døgn for gårdens fabrikker og stalde.
|
||
-- feedOnly: kun staldenes forbrug (foder, halm, vand) — marksilo-flowets foderreserve.
|
||
function ADSources.getFarmRates(adEnv, farmId, feedOnly)
|
||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||
local rates = {}
|
||
for _, placeable in pairs(placeables) do
|
||
if isOwnedBy(placeable, farmId) then
|
||
if getProductionPoint(placeable) ~= nil then
|
||
if not feedOnly then addFactoryRates(rates, placeable) end
|
||
elseif placeable.getHusbandryFillLevel ~= nil then
|
||
addHusbandryRates(rates, placeable)
|
||
if not feedOnly then pcall(addAnimalProduction, rates, placeable) end
|
||
end
|
||
end
|
||
end
|
||
local adjustment = getTimeAdjustment()
|
||
for _, rate in pairs(rates) do
|
||
rate.consumption, rate.production = rate.consumption * adjustment, rate.production * adjustment
|
||
end
|
||
return rates
|
||
end
|
||
|
||
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
|
||
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
|
||
function ADSources.getHusbandryConsumption(placeable)
|
||
local rates, result = {}, {}
|
||
if placeable == nil then return result end
|
||
pcall(addHusbandryRates, rates, placeable)
|
||
for fillType, rate in pairs(rates) do
|
||
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
|
||
end
|
||
return result
|
||
end
|
||
|
||
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
|
||
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
|
||
function ADSources.getHusbandryOutputs(placeable)
|
||
local rates, outputs = {}, {}
|
||
if placeable == nil then return outputs end
|
||
pcall(addHusbandryRates, rates, placeable) -- giver kun forbrug; med for fuldstændighedens skyld
|
||
pcall(addAnimalProduction, rates, placeable)
|
||
for fillType, rate in pairs(rates) do
|
||
if (rate.production or 0) > 0 then outputs[fillType] = rate.production end
|
||
end
|
||
return outputs
|
||
end
|
||
|
||
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
|
||
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
||
function ADSources.getMarketQuote(fillType)
|
||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||
local best = nil
|
||
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
|
||
if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then
|
||
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}
|
||
end
|
||
end
|
||
end
|
||
return best
|
||
end
|
||
|
||
-- ---- inventar: alt gården har liggende, pr. bygning ----
|
||
|
||
local function addEntry(entries, fillType, level, capacity, extra)
|
||
local entry = newProduct(fillType, level, level, capacity, false)
|
||
entry.isOutput = extra ~= nil and extra.isOutput or false
|
||
entry.count = extra ~= nil and extra.count or nil
|
||
entry.slots = extra ~= nil and extra.slots or nil
|
||
table.insert(entries, entry)
|
||
end
|
||
|
||
local function factoryEntries(placeable)
|
||
local productionPoint = getProductionPoint(placeable)
|
||
local storage = productionPoint ~= nil and productionPoint.storage or nil
|
||
local entries = {}
|
||
if storage == nil then return entries end
|
||
local outputs = {}
|
||
for _, production in ipairs(productionPoint.productions or {}) do
|
||
for _, output in ipairs(production.outputs or {}) do outputs[output.type] = true end
|
||
end
|
||
for fillType, level in pairs(storage.fillLevels or {}) do
|
||
if (level or 0) > 0 then
|
||
local capacity = storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0
|
||
addEntry(entries, fillType, level, capacity, {isOutput = outputs[fillType] == true})
|
||
end
|
||
end
|
||
return entries
|
||
end
|
||
|
||
local function siloEntries(placeable)
|
||
local entries, levels, capacities = {}, {}, {}
|
||
for _, storage in pairs(placeable.spec_silo.storages or {}) do
|
||
for fillType, level in pairs(storage.fillLevels or {}) do
|
||
if (level or 0) > 0 then
|
||
levels[fillType] = (levels[fillType] or 0) + level
|
||
capacities[fillType] = (capacities[fillType] or 0) + (storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0)
|
||
end
|
||
end
|
||
end
|
||
for fillType, level in pairs(levels) do addEntry(entries, fillType, level, capacities[fillType] or 0) end
|
||
return entries
|
||
end
|
||
|
||
local function objectStorageEntries(placeable)
|
||
local entries = {}
|
||
local spec = placeable.spec_objectStorage
|
||
for _, objectInfo in ipairs(spec.objectInfos or {}) do
|
||
local fillType, perObject = getObjectInfoContent(objectInfo)
|
||
local count = objectInfo.numObjects or 0
|
||
if fillType ~= nil and count > 0 then
|
||
-- spec.capacity er PLADSER (delt mellem alle varer); i liter: (egne + ledige) paller x indhold
|
||
local free = ADSources.objectStorageFreeSlots(placeable)
|
||
local slots = free ~= math.huge and count + free or nil -- skur uden grænse: ingen procent
|
||
addEntry(entries, fillType, count * perObject, slots ~= nil and slots * perObject or 0, {count = count, slots = slots})
|
||
end
|
||
end
|
||
return entries
|
||
end
|
||
|
||
-- Stald + udvidelsestanke: højeste niveau pr. vare blandt staldens læssestationer (de deler beholdere).
|
||
local function getHusbandryStationLevels(placeable)
|
||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||
local result = {}
|
||
if storage == nil or storage.getLoadingStations == nil then return result end
|
||
for _, station in pairs(storage:getLoadingStations()) do
|
||
if station.owningPlaceable == placeable and station.getAllFillLevels ~= nil then
|
||
local ok, levels = pcall(station.getAllFillLevels, station, g_currentMission:getFarmId())
|
||
for fillType, level in pairs(ok and type(levels) == "table" and levels or {}) do
|
||
result[fillType] = math.max(result[fillType] or 0, level or 0)
|
||
end
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
|
||
local function husbandryEntries(placeable)
|
||
local entries, seen = {}, {}
|
||
local stationLevels = getHusbandryStationLevels(placeable)
|
||
local food = placeable.spec_husbandryFood
|
||
if food ~= nil then
|
||
local total = 0
|
||
for _, level in pairs(food.fillLevels or {}) do total = total + (level or 0) end
|
||
for fillType, level in pairs(food.fillLevels or {}) do
|
||
if (level or 0) > 0 then
|
||
seen[fillType] = true
|
||
addEntry(entries, fillType, level, level + math.max(0, (food.capacity or 0) - total))
|
||
end
|
||
end
|
||
end
|
||
for _, name in ipairs({"STRAW", "WATER", "MILK", "LIQUIDMANURE", "MANURE"}) do
|
||
local fillType = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil and g_fillTypeManager:getFillTypeIndexByName(name) or nil
|
||
if fillType == nil and g_fillTypeManager ~= nil then
|
||
-- test-fakes uden getFillTypeIndexByName: slå navnet op baglæns
|
||
for index = 1, 64 do
|
||
if g_fillTypeManager:getFillTypeNameByIndex(index) == name then fillType = index break end
|
||
end
|
||
end
|
||
if fillType ~= nil and not seen[fillType] and placeable.getHusbandryIsFillTypeSupported ~= nil and placeable:getHusbandryIsFillTypeSupported(fillType) then
|
||
local own = placeable.getHusbandryFillLevel ~= nil and (placeable:getHusbandryFillLevel(fillType) or 0) or 0
|
||
local level = math.max(own, stationLevels[fillType] or 0)
|
||
if level > 0 then
|
||
local free = placeable.getHusbandryFreeCapacity ~= nil and (placeable:getHusbandryFreeCapacity(fillType) or 0) or 0
|
||
addEntry(entries, fillType, level, math.max(level, own + free))
|
||
end
|
||
end
|
||
end
|
||
return entries
|
||
end
|
||
|
||
-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
|
||
-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
|
||
function ADSources.getFarmInventory(adEnv, farmId)
|
||
return cacheGet("inventory", tostring(farmId), 2000, function() return ADSources.buildFarmInventory(adEnv, farmId) end)
|
||
end
|
||
|
||
function ADSources.buildFarmInventory(adEnv, farmId)
|
||
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||
local result = {}
|
||
for _, placeable in pairs(placeables) do
|
||
if isOwnedBy(placeable, farmId) then
|
||
local kind, entries = nil, nil
|
||
if getProductionPoint(placeable) ~= nil then
|
||
kind, entries = "factory", factoryEntries(placeable)
|
||
elseif placeable.spec_silo ~= nil then
|
||
kind, entries = "silo", siloEntries(placeable)
|
||
elseif placeable.spec_objectStorage ~= nil then
|
||
kind, entries = "objectStorage", objectStorageEntries(placeable)
|
||
elseif placeable.getHusbandryFillLevel ~= nil then
|
||
kind, entries = "husbandry", husbandryEntries(placeable)
|
||
end
|
||
if entries ~= nil and #entries > 0 then
|
||
table.sort(entries, function(a, b)
|
||
if a.isOutput ~= b.isOutput then return a.isOutput end
|
||
return tostring(a.title) < tostring(b.title)
|
||
end)
|
||
table.insert(result, {name = getPlaceableName(placeable, "?"), kind = kind, entries = entries, placeable = placeable})
|
||
end
|
||
end
|
||
end
|
||
local order = {factory = 1, husbandry = 2, objectStorage = 3, silo = 4}
|
||
table.sort(result, function(a, b)
|
||
if order[a.kind] ~= order[b.kind] then return order[a.kind] < order[b.kind] end
|
||
return a.name < b.name
|
||
end)
|
||
return result
|
||
end
|
||
|
||
function ADSources.getSourcePlaceables(sources)
|
||
local set = {}
|
||
for _, source in ipairs(sources or {}) do
|
||
if source.placeable ~= nil then
|
||
set[source.placeable] = true
|
||
end
|
||
end
|
||
return set
|
||
end
|