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 @@ +