diff --git a/FS25_ADSmartPickup/adBuildings.lua b/FS25_ADSmartPickup/adBuildings.lua new file mode 100644 index 0000000..d914711 --- /dev/null +++ b/FS25_ADSmartPickup/adBuildings.lua @@ -0,0 +1,229 @@ +-- AD Buildings +-- Gårdens bygninger som brugeren vælger imellem (i stedet for markører): fabrik, silo, stald, +-- objektlager og salgssted, hver med den AD-markør der står nærmest bygningens læsse- og +-- aflæsningstrigger (≤ MAX_MARKER_DISTANCE; Wait-markører aldrig; siloers læssemarkør via det +-- strenge opslag, så tætte rækker ikke blandes). Spil-adapter med 1 s cache. + +ADBuildings = {} + +ADBuildings.LOG_PREFIX = "ADSmartPickup: " +ADBuildings.MAX_MARKER_DISTANCE = 40 +ADBuildings.CACHE_MS = 1000 + +local cache = {list = nil, farmId = nil, time = -math.huge} + +local function now() + if g_currentMission ~= nil and type(g_currentMission.time) == "number" then + return g_currentMission.time + end + return os.clock() * 1000 +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 getName(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 + +function ADBuildings.getId(placeable) + if placeable.uniqueId ~= nil then + return tostring(placeable.uniqueId) + end + local x, z = getNodePosition(placeable.rootNode) + if x == nil then + return nil + end + return string.format("pos:%d:%d", math.floor(x + 0.5), math.floor(z + 0.5)) +end + +local function getProductionPoint(placeable) + return placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil +end + +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 getKind(placeable) + if getProductionPoint(placeable) ~= nil then return "factory" end + if placeable.spec_silo ~= nil then return "silo" end + if placeable.getHusbandryFillLevel ~= nil then return "husbandry" end + if placeable.spec_objectStorage ~= nil then return "objectStorage" end + return nil +end + +-- Trigger-noder pr. bygning: læsse (loadTriggers.triggerNode, pallespawner, objektlager) og +-- aflæsning (unloadTriggers exactFillRootNode/triggerNode/aiNode). +local function collectNodes(placeable, loadStations, unloadStations) + local loadNodes, unloadNodes = {}, {} + for _, station in pairs(loadStations) do + if station.owningPlaceable == placeable then + for _, trigger in pairs(station.loadTriggers or {}) do + if trigger.triggerNode ~= nil then table.insert(loadNodes, trigger.triggerNode) end + end + end + end + for _, station in pairs(unloadStations) do + if station.owningPlaceable == placeable then + for _, trigger in pairs(station.unloadTriggers or {}) do + for _, field in ipairs({"exactFillRootNode", "triggerNode", "aiNode"}) do + if trigger[field] ~= nil then table.insert(unloadNodes, trigger[field]) end + end + end + end + end + local productionPoint = getProductionPoint(placeable) + local spawner = productionPoint ~= nil and productionPoint.palletSpawner or nil + if spawner ~= nil then + for _, place in ipairs(spawner.spawnPlaces or {}) do + if place.startNode ~= nil then table.insert(loadNodes, place.startNode) end + end + end + if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then + table.insert(loadNodes, placeable.rootNode) + table.insert(unloadNodes, placeable.rootNode) + end + return loadNodes, unloadNodes +end + +-- Nærmeste ikke-Wait-markør til en af noderne, ≤ MAX_MARKER_DISTANCE. +local function nearestMarker(adEnv, nodes) + local best, bestDistance = nil, ADBuildings.MAX_MARKER_DISTANCE + if #nodes == 0 then + return nil + end + local positions = {} + for _, node in ipairs(nodes) do + local x, z = getNodePosition(node) + if x ~= nil then table.insert(positions, {x = x, z = z}) end + end + for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do + if not ADSupplyPlanner.isWaitMarker(marker) then + local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id) + if wayPoint ~= nil then + for _, position in ipairs(positions) do + local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z + local distance = math.sqrt(dx * dx + dz * dz) + if distance < bestDistance then + best, bestDistance = marker, distance + end + end + end + end + end + if best == nil then + return nil + end + return {id = best.id, name = tostring(best.name), distance = math.floor(bestDistance + 0.5)} +end + +-- Silo: den markør hvis STRENGE opslag (trigger bag næsen) rammer siloens læssestation. +local function siloLoadMarker(adEnv, farmId, placeable, loadStations) + local own = {} + for _, station in pairs(loadStations) do + if station.owningPlaceable == placeable then own[station] = true end + end + local best = nil + for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do + if not ADSupplyPlanner.isWaitMarker(marker) then + local station = ADSmartPickup.getLoadStationAtWayPointId(adEnv, marker.id, farmId) + if station ~= nil and own[station] then + best = {id = marker.id, name = tostring(marker.name), distance = 0} + break + end + end + end + return best +end + +local function isSellPlaceable(placeable, unloadStations) + for _, station in pairs(unloadStations) do + if station.owningPlaceable == placeable and ADPrices.isSellingStation(station) then + return true + end + end + return false +end + +local ORDER = {factory = 1, husbandry = 2, objectStorage = 3, silo = 4, sell = 5} + +local function build(adEnv, farmId) + local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {} + local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil + local loadStations = storage ~= nil and storage:getLoadingStations() or {} + local unloadStations = storage ~= nil and storage:getUnloadingStations() or {} + local result = {} + for _, placeable in pairs(placeables) do + local kind = getKind(placeable) + local isSell = kind == nil and isSellPlaceable(placeable, unloadStations) + if isSell then kind = "sell" end + if kind ~= nil and (kind == "sell" or isOwnedBy(placeable, farmId)) then + local id = ADBuildings.getId(placeable) + if id ~= nil then + local loadNodes, unloadNodes = collectNodes(placeable, loadStations, unloadStations) + local loadMarker + if kind == "silo" then + loadMarker = siloLoadMarker(adEnv, farmId, placeable, loadStations) + else + loadMarker = nearestMarker(adEnv, loadNodes) + end + table.insert(result, { + id = id, name = getName(placeable, id), kind = kind, placeable = placeable, + loadMarker = loadMarker, + unloadMarker = nearestMarker(adEnv, unloadNodes), + canBeSource = kind ~= "sell", + canBeTarget = kind == "husbandry" or kind == "factory", + canSell = kind == "sell", + }) + end + end + end + 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 ADBuildings.invalidate() + cache.list, cache.time = nil, -math.huge +end + +function ADBuildings.list(adEnv, farmId) + local time = now() + if cache.list ~= nil and cache.farmId == farmId and (time - cache.time) < ADBuildings.CACHE_MS then + return cache.list + end + cache.list, cache.farmId, cache.time = build(adEnv, farmId), farmId, time + return cache.list +end + +function ADBuildings.find(adEnv, farmId, id) + for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do + if building.id == id then return building end + end + return nil +end + +-- role "load": bygningen hvis læssemarkør er wayPointId; "unload": aflæsningsmarkør. +function ADBuildings.byMarker(adEnv, farmId, wayPointId, role) + for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do + local marker = role == "unload" and building.unloadMarker or building.loadMarker + if marker ~= nil and marker.id == wayPointId then return building end + end + return nil +end diff --git a/tests/test_adBuildings.lua b/tests/test_adBuildings.lua new file mode 100644 index 0000000..ee37b8f --- /dev/null +++ b/tests/test_adBuildings.lua @@ -0,0 +1,129 @@ +-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden) +-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket, +-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache. + +local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7 +local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}} +g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end} +Logging = {info = function() end, warning = function() end} +getWorldTranslation = function(node) return node.x, 0, node.z end +SellingStation = {} +dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") +dofile("FS25_ADSmartPickup/adPrices.lua") +dofile("FS25_ADSmartPickup/adBuildings.lua") + +local failures = 0 +local function check(name, actual, expected) + if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end +end + +local markers, wayPoints = {}, {} +local function addMarker(id, name, group, x, z) + wayPoints[id] = {id = id, x = x, z = z} + table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group}) +end +local loadStations, unloadStations = {}, {} +local adEnv = {ADGraphManager = { + getWayPointById = function(_, id) return wayPoints[id] end, + getMapMarkers = function() return markers end, + getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end, +}} +local strictLoadAt = {} +ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end} +g_currentMission = { + placeableSystem = {placeables = {}}, + storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end}, + accessHandler = {canFarmAccess = function() return true end}, + getFarmId = function() return 1 end, +} +local function placeable(uid, name, x, z, extra) + local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end} + for k, v in pairs(extra or {}) do p[k] = v end + table.insert(g_currentMission.placeableSystem.placeables, p) + return p +end + +-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted +local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end}) +local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}}} +local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}} +table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload) +local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}}) +local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true} +table.insert(unloadStations, millUnload) +local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}}) +local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}}) +local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}} +local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}} +table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station) +table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}}) +table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}}) +local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}}) +local sellPoint = placeable("sell", "Mejeri", 3000, 0, {}) +local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true} +table.insert(unloadStations, sellStation) +local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end}) + +addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren +addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren +addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren) +addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren +addMarker(5, "sugar offload", "production", 470, 0) +addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1 +addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m) +strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station +addMarker(8, "Storage pickup", "Pickup", 2010, 0) +addMarker(9, "Mejeri", "selling", 3000, 20) +addMarker(10, "Langt væk", "All", 9000, 9000) + +-- B1: liste, kind og markører +local list = ADBuildings.list(adEnv, 1) +local byId = {} +for _, b in ipairs(list) do byId[b.id] = b end +check("B1 antal (naboens silo udeladt)", #list, 6) +check("B1 stald", byId.barn1.kind, "husbandry") +check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1) +check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2) +check("B1 stald afstand", byId.barn1.loadMarker.distance, 5) +check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4) +check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5) +check("B1 fabrik er ikke salgssted", byId.mill.canSell, false) +check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8) +check("B1 salgssted", byId.sell.kind, "sell") +check("B1 salgssted markør", byId.sell.unloadMarker.id, 9) +check("B1 salgssted canSell", byId.sell.canSell, true) +check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true) +check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true) + +-- B2: siloer i række: strengt opslag giver hver sin markør +check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6) +check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7) +strictLoadAt[7] = nil +ADBuildings.invalidate() +check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil) +strictLoadAt[7] = silo2Station +ADBuildings.invalidate() + +-- B3: byMarker (omvendt opslag) +check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1") +check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1") +check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil) +check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil) + +-- B4: id-fallback uden uniqueId +check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3") +check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1") + +-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate +local first = ADBuildings.list(adEnv, 1) +check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true) +ADBuildings.invalidate() +check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true) + +-- B6: sortering: fabrik, stald, objektlager, silo, salg; navn inden for slags +local order = {} +for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.kind) end +check("B6 rækkefølge", table.concat(order, ","), "factory,husbandry,objectStorage,silo,silo,sell") + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)