-- 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 = {} 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 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) 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 == "silo" or source.kind == "husbandry" 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) -- eller rodnoden. local function getPlaceableAnchor(placeable) local productionPoint = getProductionPoint(placeable) local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil 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 return best 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) 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 -- 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. if placeable == nil and ADSmartPickup.getLoadStationNearWayPointId ~= nil then local loose = ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, ADSources.MAX_DISTANCE) local loosePlaceable = loose ~= nil and loose.owningPlaceable or nil 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 -- 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 -- Liter af varen på paller der står ved fabrikkens spawner (pallespawneren tømmer lageret -- ud på jorden løbende, så det er dér varen fysisk er). local function getPalletsNearSpawner(placeable, fillType, farmId) local productionPoint = getProductionPoint(placeable) local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil local node = spawner ~= nil and spawner.spawnPlaces ~= nil and spawner.spawnPlaces[1] ~= nil and spawner.spawnPlaces[1].startNode or placeable.rootNode local sx, sz = getNodePosition(node) if sx == nil then return 0 end local vehicles = g_currentMission ~= nil and g_currentMission.vehicleSystem ~= nil and g_currentMission.vehicleSystem.vehicles or {} local total, count = 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) if palletFillType == fillType and x ~= nil and distance2(sx, sz, x, z) <= radius2 then total = total + level count = count + 1 end end 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 getHusbandryProducts(adEnv, source) local placeable, station = source.placeable, source.station 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 -- 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 == "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) 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 local function isOwnedBy(placeable, farmId) 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. function ADSources.getFarmRates(adEnv, farmId) 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 addFactoryRates(rates, placeable) elseif placeable.getHusbandryFillLevel ~= nil then addHusbandryRates(rates, placeable) pcall(addAnimalProduction, rates, placeable) 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 -- 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 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 addEntry(entries, fillType, count * perObject, spec.capacity or 0, {count = count}) 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) 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