Files

541 lines
25 KiB
Lua

-- 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
-- en separat drop-markør (≠ bygningens læssemarkør) må højst stå så langt fra aflæsningstriggeren
ADBuildings.MAX_DROP_DISTANCE = 25
-- bygninger ændrer sig sjældent; genopbygningen (bygninger x markører) er dyr — 1 s gav et hak hvert sekund
ADBuildings.CACHE_MS = 30000
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)
local own = ADNames ~= nil and ADNames.forPlaceable(placeable) or nil
if own ~= nil then return own end
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
-- Fælles ejerregel (ADPrices): egen ejer eller jord-bundet kortanlæg på gårdens jord.
local function isOwnedBy(placeable, farmId)
if ADPrices ~= nil and ADPrices.isOwnedBy ~= nil then
return ADPrices.isOwnedBy(placeable, farmId)
end
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
local logHusbandryFoodOnce
ADBuildings.findTriggerNodes = function(root, maxDepth) return ADSupplyPlanner.findTriggerNodes(root, maxDepth) end
-- Diagnose én gang pr. stald: hvilke felter foderdataene har, når ingen trigger-node blev fundet.
local loggedFood = setmetatable({}, {__mode = "k"})
logHusbandryFoodOnce = function(placeable)
if loggedFood[placeable] then return end
loggedFood[placeable] = true
local keys = {}
for key, value in pairs(placeable.spec_husbandryFood or {}) do table.insert(keys, tostring(key) .. ":" .. type(value)) end
table.sort(keys)
if Logging ~= nil and Logging.info ~= nil then
Logging.info(ADBuildings.LOG_PREFIX .. "stald '%s' har ingen aflæsnings-trigger; spec_husbandryFood-felter: %s", getName(placeable, "?"), table.concat(keys, ", "))
end
end
-- Trigger-noder pr. bygning: læsse (loadTriggers.triggerNode, pallespawner, objektlager) og
-- aflæsning (unloadTriggers exactFillRootNode/triggerNode/aiNode).
-- loadGroups: én gruppe noder pr. læssepunkt (mælketank, gyllebeholder ...), så hvert punkt får sin
-- egen markør; pallespawner/objektlager er en gruppe for sig.
local function collectNodes(placeable, loadStations, unloadStations)
local loadGroups, unloadNodes = {}, {}
-- én gruppe pr. LÆSSEPUNKT: en stald har ofte én station med både mælke- og gyllepunkt
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(loadGroups, {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
local nodes = {}
for _, place in ipairs(spawner.spawnPlaces or {}) do
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
end
if #nodes > 0 then
nodes.isSpawner = true
table.insert(loadGroups, nodes)
end
end
-- stald uden aflæsningsnoder i stationen (hønsehus: kun foder-triggeren) -> søg i staldens foderdata
if #unloadNodes == 0 and placeable.getHusbandryFillLevel ~= nil then
for _, node in ipairs(ADSupplyPlanner.findTriggerNodes(placeable.spec_husbandryFood)) do
table.insert(unloadNodes, node)
end
if #unloadNodes == 0 then
logHusbandryFoodOnce(placeable)
end
end
-- stald med egne pallespawnere (hønsehus: æg, fårestald: uld) — én læssegruppe pr. spawner
local husbandryPallets = placeable.spec_husbandryPallets
local seenSpawners = {}
for _, palletSpawner in pairs(husbandryPallets ~= nil and husbandryPallets.fillTypeIndexToPalletSpawner or {}) do
if not seenSpawners[palletSpawner] then
seenSpawners[palletSpawner] = true
local nodes = {}
for _, place in ipairs(palletSpawner.spawnPlaces or {}) do
if place.startNode ~= nil then table.insert(nodes, place.startNode) end
end
if #nodes > 0 then
nodes.isSpawner = true
table.insert(loadGroups, nodes)
end
end
end
if placeable.spec_objectStorage ~= nil and placeable.rootNode ~= nil then
table.insert(loadGroups, {placeable.rootNode})
table.insert(unloadNodes, placeable.rootNode)
end
return loadGroups, unloadNodes
end
-- Naturlig sortering: tal i navne sammenlignes som tal ("Silo 9" < "Silo 10", "25 Silo" < "26 Silo").
local function naturalKey(text)
local parts = {}
for chunk in string.gmatch(tostring(text or ""), "%d+%.?%d*[^%d]*") do
local number, rest = string.match(chunk, "^(%d+%.?%d*)(.*)$")
table.insert(parts, {tonumber(number), string.lower(rest)})
end
local leading = string.match(tostring(text or ""), "^[^%d]*")
if leading ~= nil and leading ~= "" then
table.insert(parts, 1, {nil, string.lower(leading)})
end
return parts
end
function ADBuildings.naturalLess(a, b)
local ka, kb = naturalKey(a), naturalKey(b)
for index = 1, math.max(#ka, #kb) do
local pa, pb = ka[index], kb[index]
if pa == nil then return true end
if pb == nil then return false end
if pa[1] ~= nil and pb[1] ~= nil and pa[1] ~= pb[1] then return pa[1] < pb[1] end
if (pa[1] == nil) ~= (pb[1] == nil) then return pa[1] == nil end
if pa[2] ~= pb[2] then return pa[2] < pb[2] end
end
return false
end
-- Bygninger sorteres efter navn (naturligt), dernæst første læssemarkørs navn (ens navne, fx 40 Farma-siloer).
function ADBuildings.compare(a, b)
if a.name ~= b.name then return ADBuildings.naturalLess(a.name, b.name) end
local ma = a.loadMarkers ~= nil and a.loadMarkers[1] ~= nil and a.loadMarkers[1].name or (a.unloadMarker ~= nil and a.unloadMarker.name or "")
local mb = b.loadMarkers ~= nil and b.loadMarkers[1] ~= nil and b.loadMarkers[1].name or (b.unloadMarker ~= nil and b.unloadMarker.name or "")
return ADBuildings.naturalLess(ma, mb)
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
-- Aflæsningsmarkør: nærmeste ikke-Wait-markør ≤ MAX_MARKER_DISTANCE til aflæsningsnoderne — men ikke
-- bygningens egne LÆSSEmarkører, når der findes en anden (Sugarbeet Crusher: pickup står tættere på
-- aflæsningstriggeren end drop). Kun én fælles markør (AIO Load / Unload) bruges til begge.
-- Alle ikke-Wait-markører ≤ MAX_MARKER_DISTANCE til nodes, nærmeste først: {{id, name, distance}}.
local function markerCandidates(adEnv, nodes)
local positions, found = {}, {}
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
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
if wayPoint ~= nil then
local nearest = nil
for _, position in ipairs(positions) do
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
if nearest == nil or distance < nearest then nearest = distance end
end
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then
table.insert(found, {id = marker.id, name = tostring(marker.name), distance = nearest})
end
end
end
table.sort(found, function(a, b) return a.distance < b.distance end)
return found
end
-- Markørnavnets rolle: "unload" (unload/drop/aflæs/aflever/indkast/tip) eller "load" (pickup/hent/læs/load) —
-- aflæsning tjekkes først ("unload" og "aflæs" indeholder "load"/"læs"). nil = neutralt navn.
local UNLOAD_WORDS = {"unload", "drop", "aflæs", "aflev", "indkast", "tip"}
local LOAD_WORDS = {"pickup", "pick up", "hent", "læs", "load"}
function ADBuildings.nameRole(name)
local lower = tostring(name or ""):lower()
for _, word in ipairs(UNLOAD_WORDS) do if lower:find(word, 1, true) then return "unload" end end
for _, word in ipairs(LOAD_WORDS) do if lower:find(word, 1, true) then return "load" end end
return nil
end
-- Når to markører står tæt (inden for NAME_WINDOW_M af den nærmeste), afgør navnet: den første med den ønskede rolle
-- flyttes forrest. Ellers urørt (nærmeste først). (in-game 30/9: Sell Egg tog "Chicken Storage Unload" 17 m frem for
-- "Egg Storage Pickup" 27 m ved Æglageret)
ADBuildings.NAME_WINDOW_M = 20
function ADBuildings.preferByName(candidates, wantRole)
local list = candidates or {}
if #list < 2 then return list end
local limit = (list[1].distance or 0) + ADBuildings.NAME_WINDOW_M
if ADBuildings.nameRole(list[1].name) == wantRole then return list end
for index, candidate in ipairs(list) do
if (candidate.distance or math.huge) > limit then break end
if ADBuildings.nameRole(candidate.name) == wantRole then
local reordered = {candidate}
for other, item in ipairs(list) do if other ~= index then table.insert(reordered, item) end end
return reordered
end
end
return list
end
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
for _, candidate in ipairs(ADBuildings.preferByName(candidates, "load")) do
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
if not isUnloads then
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
end
end
return nil
end
-- Diagnose (én gang pr. stald pr. session): hvilke markører ligger nærmest læsse- og aflæsningspunkterne.
-- (in-game 27/9: Goat Food fik "Sheep Milk"-markørerne som mål og Goat Milk Pickup "Sheep Food" som kilde)
local diagnosed = {}
local function closestMarkers(adEnv, nodes, count)
local positions, found = {}, {}
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
local wayPoint = not ADSupplyPlanner.isWaitMarker(marker) and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
if wayPoint ~= nil then
local nearest = nil
for _, position in ipairs(positions) do
local distance = math.sqrt((wayPoint.x - position.x) ^ 2 + (wayPoint.z - position.z) ^ 2)
if nearest == nil or distance < nearest then nearest = distance end
end
if nearest ~= nil and nearest < ADBuildings.MAX_MARKER_DISTANCE then table.insert(found, {name = tostring(marker.name), distance = nearest}) end
end
end
table.sort(found, function(a, b) return a.distance < b.distance end)
local parts = {}
for index = 1, math.min(count, #found) do table.insert(parts, string.format("%s %.0f m", found[index].name, found[index].distance)) end
return #parts > 0 and table.concat(parts, ", ") or "ingen markør"
end
local function diagnoseHusbandry(adEnv, id, name, loadGroups, unloadNodes, loadMarkers, unloadMarker)
if diagnosed[id] then return end
diagnosed[id] = true
local groups = {}
for index, nodes in ipairs(loadGroups) do table.insert(groups, string.format("læs%d: %s", index, closestMarkers(adEnv, nodes, 2))) end
local loads = {}
for _, marker in ipairs(loadMarkers) do table.insert(loads, tostring(marker.name)) end
Logging.info("ADSmartPickup: stald '%s': %s | aflæsning nærmest: %s | valgt læs=[%s] aflæs=%s", tostring(name),
#groups > 0 and table.concat(groups, "; ") or "ingen læssegrupper", closestMarkers(adEnv, unloadNodes, 3),
table.concat(loads, ", "), unloadMarker ~= nil and tostring(unloadMarker.name) or "-")
end
local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
if #nodes == 0 then
return nil
end
local isLoad = {}
for _, marker in ipairs(loadMarkers or {}) do isLoad[marker.id] = true 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
local best, bestOther, nearbyAll = nil, nil, {}
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 < ADBuildings.MAX_MARKER_DISTANCE then
table.insert(nearbyAll, {marker = marker, distance = distance})
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
bestOther = {marker = marker, distance = distance}
end
end
end
end
end
end
-- siloer: altid nærmeste (en fælles markør må ikke erstattes af nabosiloens); ellers kun en "anden"
-- markør der står tæt på aflæsningen
local chosen = best
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
chosen = bestOther
end
-- navnet afgør mellem tætte markører: en "Unload/Drop"-markør frem for en "Pickup"-markør (in-game 30/9)
table.sort(nearbyAll, function(a, b) return a.distance < b.distance end)
if chosen ~= nil and ADBuildings.nameRole(chosen.marker.name) == "load" then
for _, entry in ipairs(nearbyAll) do
if entry.distance <= chosen.distance + ADBuildings.NAME_WINDOW_M and ADBuildings.nameRole(entry.marker.name) == "unload" then
chosen = entry
break
end
end
end
if chosen == nil then
return nil
end
return {id = chosen.marker.id, name = tostring(chosen.marker.name), distance = math.floor(chosen.distance + 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
-- Brændstof er ikke en vare der køres rundt (gårdens tankanlæg har en læssestation til diesel).
local FUEL_FILL_TYPES = {DIESEL = true, DEF = true, ELECTRICCHARGE = true, METHANE = true}
local function isFuelOnly(station)
local supported = station.supportedFillTypes
if supported == nil or next(supported) == nil or g_fillTypeManager == nil then
return false
end
for fillType, isSupported in pairs(supported) do
if isSupported and not FUEL_FILL_TYPES[tostring(g_fillTypeManager:getFillTypeNameByIndex(fillType))] then
return false
end
end
return true
end
local function hasOwnLoadingStation(placeable, loadStations)
for _, station in pairs(loadStations) do
if station.owningPlaceable == placeable and not isFuelOnly(station) then return true end
end
return false
end
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
-- generelt lager: gårdens bygning med egen læssestation uden silo/fabrik/stald-spec
-- (fx Pumps&Hoses' separatorhal der udleverer digestat)
if kind == nil and hasOwnLoadingStation(placeable, loadStations) then kind = "storage" end
if kind ~= nil and (kind == "sell" or isOwnedBy(placeable, farmId)) then
local id = ADBuildings.getId(placeable)
if id ~= nil then
local loadGroups, unloadNodes = collectNodes(placeable, loadStations, unloadStations)
local loadMarkers, seen = {}, {}
if kind == "silo" then
local marker = siloLoadMarker(adEnv, farmId, placeable, loadStations)
if marker ~= nil then table.insert(loadMarkers, marker) end
else
local unloadBest = markerCandidates(adEnv, unloadNodes)[1]
for _, nodes in ipairs(loadGroups) do
local marker = ADBuildings.pickLoadMarker(markerCandidates(adEnv, nodes), unloadBest, nodes.isSpawner == true)
if marker ~= nil and not seen[marker.id] then
seen[marker.id] = true
table.insert(loadMarkers, marker)
end
end
table.sort(loadMarkers, function(a, b) return ADBuildings.naturalLess(a.name, b.name) end)
end
-- siloer og palleskure læsser og aflæsser samme sted: altid nærmeste markør
local unloadMarker = unloadMarkerFor(adEnv, unloadNodes, loadMarkers, kind ~= "silo" and kind ~= "objectStorage")
if kind == "husbandry" then pcall(diagnoseHusbandry, adEnv, id, getName(placeable, id), loadGroups, unloadNodes, loadMarkers, unloadMarker) end
-- silo der kun kan FYLDES (fx drive-on-bunker der fodrer en biogas-fermentor): aflæsning,
-- ingen læssestation -> et forsyningsmål, aldrig en kilde
local isFillOnlySilo = kind == "silo" and loadMarkers[1] == nil and #loadGroups == 0 and unloadMarker ~= nil
table.insert(result, {
id = id, name = getName(placeable, id), kind = kind, placeable = placeable,
loadMarker = loadMarkers[1],
loadMarkers = loadMarkers,
unloadMarker = unloadMarker,
canBeSource = kind ~= "sell" and not isFillOnlySilo,
canBeTarget = kind == "husbandry" or kind == "factory" or isFillOnlySilo,
canSell = kind == "sell",
})
end
end
end
table.sort(result, ADBuildings.compare)
return result
end
function ADBuildings.invalidate()
if ADCache ~= nil then ADCache.clear() end
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
-- nye bygninger uden eget navn navngives (tilføjes navnefilen; eksisterende navne røres ikke)
if ADNames ~= nil and ADNames.autoName ~= nil then
local ok, err = pcall(ADNames.autoName, adEnv, cache.list)
if not ok then Logging.warning("ADSmartPickup: automatiske bygningsnavne fejlede: %s", tostring(err)) end
end
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
if role == "unload" then
if building.unloadMarker ~= nil and building.unloadMarker.id == wayPointId then return building end
else
for _, marker in ipairs(building.loadMarkers or {}) do
if marker.id == wayPointId then return building end
end
end
end
return nil
end
-- Alle markører for en rolle: læsning = én pr. læssestation; aflæsning = én.
function ADBuildings.getMarkers(building, role)
if role == "unload" then
return building.unloadMarker ~= nil and {building.unloadMarker} or {}
end
return building.loadMarkers or (building.loadMarker ~= nil and {building.loadMarker} or {})
end