diff --git a/FS25_ADSmartPickup/adChainData.lua b/FS25_ADSmartPickup/adChainData.lua
new file mode 100644
index 0000000..c361297
--- /dev/null
+++ b/FS25_ADSmartPickup/adChainData.lua
@@ -0,0 +1,214 @@
+-- AD Chain Data (læse-adapter)
+-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
+-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
+-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
+-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
+
+ADChainData = {}
+ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
+ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
+ADChainData.warned = {}
+
+-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
+function ADChainData.isPermanent(fruitName)
+ if type(fruitName) ~= "string" or fruitName == "" then return false end
+ return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
+end
+
+function ADChainData.recipeKey(placeableUid, productionId)
+ return tostring(placeableUid) .. ":" .. tostring(productionId)
+end
+
+-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
+function ADChainData.cyclesPerMonth(production)
+ return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
+end
+
+function ADChainData.warnOnce(key, fmt, ...)
+ if ADChainData.warned[key] then return end
+ ADChainData.warned[key] = true
+ if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
+end
+
+-- ---- hjælpere ----
+
+-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
+local function nameResolver(data)
+ return function(index)
+ if index == nil or g_fillTypeManager == nil then return nil end
+ local name = g_fillTypeManager:getFillTypeNameByIndex(index)
+ if name == nil then return nil end
+ if data.fillIndex[name] == nil then
+ data.fillIndex[name] = index
+ local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
+ data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
+ end
+ return name
+ end
+end
+
+-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
+local function amountsByName(list, nameOf)
+ local result = {}
+ for _, entry in ipairs(list or {}) do
+ local name = nameOf(entry.type)
+ if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
+ end
+ return result
+end
+
+local function placeableId(placeable)
+ if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
+ return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
+end
+
+-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
+local function placeableName(placeable, fallback)
+ local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
+ if own ~= nil then return own end
+ if 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 isEnabled(point, production)
+ if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
+ for _, active in ipairs(point.activeProductions or {}) do
+ if active == production then return true end
+ end
+ return false
+end
+
+-- ---- læsning pr. del ----
+
+-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
+local function readFactory(placeable, point, byChain, nameOf)
+ local uid = placeableId(placeable)
+ if uid == nil then return {}, {} end
+ local buildingName = placeableName(placeable, uid)
+ local recipes, points = {}, {}
+ for _, production in ipairs(point.productions or {}) do
+ local key = ADChainData.recipeKey(uid, production.id)
+ table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
+ buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
+ cyclesPerMonth = ADChainData.cyclesPerMonth(production),
+ inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf)})
+ points[key] = {point = point, productionId = production.id, placeable = placeable}
+ end
+ return recipes, points
+end
+
+local function readHerd(placeable, nameOf)
+ local uid = placeableId(placeable)
+ local outputs = {}
+ for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
+ local name = nameOf(index)
+ if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
+ end
+ if uid == nil or next(outputs) == nil then return nil end
+ return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
+end
+
+local function readPlaceables(data, farmId, byChain, nameOf)
+ local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
+ for index, placeable in pairs(placeables) do
+ local ok, err = pcall(function()
+ if not ADPrices.isOwnedBy(placeable, farmId) then return end
+ local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
+ if point ~= nil then
+ local recipes, points = readFactory(placeable, point, byChain, nameOf)
+ for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
+ for key, entry in pairs(points) do data.points[key] = entry end
+ elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
+ local herd = readHerd(placeable, nameOf)
+ if herd ~= nil then table.insert(data.husbandry, herd) end
+ end
+ end)
+ if not ok then
+ local key = tostring(placeable ~= nil and placeable.uniqueId or index)
+ ADChainData.warnOnce("placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
+ end
+ end
+ table.sort(data.recipes, function(a, b) return a.key < b.key end)
+end
+
+local function readCrops(data, nameOf)
+ local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
+ for _, fruit in ipairs(fruits) do
+ if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
+ local ok, err = pcall(function()
+ local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
+ if fillName == nil then return end
+ if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
+ data.crops[fillName] = fruit.name
+ data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
+ local quote = fruit.fillType.index ~= nil and ADSources.getMarketQuote(fruit.fillType.index) or nil
+ data.prices[fruit.name] = quote ~= nil and quote.price or 0
+ end)
+ if not ok then ADChainData.warnOnce("fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
+ end
+ end
+end
+
+local function readStock(data, adEnv, farmId, nameOf)
+ local ok, err = pcall(function()
+ for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
+ local fruit = data.crops[nameOf(index) or ""]
+ if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
+ end
+ end)
+ if not ok then ADChainData.warnOnce("stock", "lager kunne ikke læses: %s", tostring(err)) end
+end
+
+-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald.
+local function readUsedFruits(data, adEnv, farmId, nameOf)
+ for _, recipe in ipairs(data.recipes) do
+ if recipe.enabled then
+ for name in pairs(recipe.inputs) do
+ if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
+ end
+ end
+ end
+ local ok, err = pcall(function()
+ for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId) or {}) do
+ local fruit = data.crops[nameOf(index) or ""]
+ if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
+ end
+ end)
+ if not ok then ADChainData.warnOnce("rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
+end
+
+local function readFields(data)
+ for _, info in ipairs(ADFields.list() or {}) do
+ local ok, entry = pcall(function()
+ local field = ADFieldJobs.getField(info.id)
+ local plan = field.plan or {}
+ local fruit = info.growing and info.fruitName or field.lastFruit
+ return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
+ targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
+ permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
+ end)
+ if ok then
+ table.insert(data.fields, entry)
+ else
+ ADChainData.warnOnce("field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
+ end
+ end
+end
+
+-- ---- samlet ----
+
+-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
+function ADChainData.collect(adEnv, farmId, byChain)
+ local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, prices = {},
+ usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}}
+ local nameOf = nameResolver(data)
+ readPlaceables(data, farmId, byChain, nameOf)
+ readCrops(data, nameOf)
+ readStock(data, adEnv, farmId, nameOf)
+ readUsedFruits(data, adEnv, farmId, nameOf)
+ readFields(data)
+ return data
+end
diff --git a/FS25_ADSmartPickup/adFields.lua b/FS25_ADSmartPickup/adFields.lua
index 307ffcf..f5e7450 100644
--- a/FS25_ADSmartPickup/adFields.lua
+++ b/FS25_ADSmartPickup/adFields.lua
@@ -291,6 +291,17 @@ function ADFields.shapeSignature(verts)
roundTo(cx, 1), roundTo(cz, 1))
end
+-- Markens areal i hektar (shoelace på {x,z}-polygonen, /10000); 0 uden en flade (under 3 hjørner).
+function ADFields.polygonAreaHa(verts)
+ if type(verts) ~= "table" or #verts < 3 then return 0 end
+ local n, twiceArea = #verts, 0
+ for index = 1, n do
+ local a, b = verts[index], verts[index % n + 1]
+ twiceArea = twiceArea + a.x * b.z - b.x * a.z
+ end
+ return math.abs(twiceArea) / 2 / 10000
+end
+
-- Hjørnernes gennemsnit (nil uden hjørner).
function ADFields.polygonCenter(verts)
if verts == nil or #verts == 0 then return nil end
diff --git a/FS25_ADSmartPickup/adSources.lua b/FS25_ADSmartPickup/adSources.lua
index 8cf9476..ad88581 100644
--- a/FS25_ADSmartPickup/adSources.lua
+++ b/FS25_ADSmartPickup/adSources.lua
@@ -963,6 +963,19 @@ function ADSources.getFarmRates(adEnv, farmId, feedOnly)
return rates
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)
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index e6f60ba..60e1c04 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -58,6 +58,7 @@
+