feat: adBuildings — gårdens bygninger med automatisk markør ved læsse-/aflæsningstrigger

This commit is contained in:
2026-09-23 18:14:00 +02:00
parent 6dbeaab3f7
commit 4137442b50
2 changed files with 358 additions and 0 deletions
+229
View File
@@ -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
+129
View File
@@ -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)