feat(ui): Flows-oprydning — tavle grupperet Ind/Ud efter navn, traktorer: egne+ledige (Vis alle), bygninger: valgte først, uden markør skjult (Vis alle); fix: staldes foderkrybbe (feedingTrough) i stationsopslag og bygningsmarkører — hønsehus (v1.19.0.0)

This commit is contained in:
2026-09-24 19:31:57 +02:00
parent b3d49c4857
commit 99c509ea10
13 changed files with 334 additions and 26 deletions
+5
View File
@@ -104,6 +104,11 @@ local function collectNodes(placeable, loadStations, unloadStations)
end
if #nodes > 0 then table.insert(loadGroups, nodes) end
end
-- staldens foderkrybbe (hønsehus: kun krybben, ingen trigger i aflæsningsstationen)
local food = placeable.spec_husbandryFood
local trough = food ~= nil and food.feedingTrough or nil
local troughNode = trough ~= nil and (trough.exactFillRootNode or trough.triggerNode) or nil
if troughNode ~= nil then table.insert(unloadNodes, troughNode) end
-- stald med egne pallespawnere (hønsehus: æg, fårestald: uld) — én læssegruppe pr. spawner
local husbandryPallets = placeable.spec_husbandryPallets
local seenSpawners = {}
+119
View File
@@ -0,0 +1,119 @@
-- AD Flow View
-- Rene regler for Flows-fanens oprydning (ingen spil-API): tavlens grupper og rækkefølge, hvilke
-- traktorer og bygninger der vises som standard, og hvad "Vis alle" tilføjer.
ADFlowView = {}
-- Naturlig sortering: tal i navne sammenlignes som tal ("Biogas 9" < "Biogas 10").
local function naturalKey(text)
local parts = {}
local value = string.lower(tostring(text or ""))
local position = 1
while position <= #value do
local digits = string.match(value, "^%d+", position)
if digits ~= nil then
table.insert(parts, {number = tonumber(digits)})
position = position + #digits
else
local chunk = string.match(value, "^%D+", position)
table.insert(parts, {text = chunk})
position = position + #chunk
end
end
return parts
end
function ADFlowView.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.number ~= nil and pb.number ~= nil then
if pa.number ~= pb.number then return pa.number < pb.number end
elseif pa.number ~= nil or pb.number ~= nil then
return pa.number ~= nil
elseif pa.text ~= pb.text then
return pa.text < pb.text
end
end
return false
end
local function sortedBy(list, field)
local copy = {}
for _, item in ipairs(list or {}) do table.insert(copy, item) end
table.sort(copy, function(a, b)
if a[field] ~= b[field] then return ADFlowView.naturalLess(a[field], b[field]) end
return tostring(a.id) < tostring(b.id)
end)
return copy
end
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
-- {{kind, runs}, ...}; tomme grupper udelades.
function ADFlowView.boardGroups(runs)
local byKind = {supply = {}, outbound = {}}
for _, run in ipairs(runs or {}) do
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
end
local groups = {}
for _, kind in ipairs({"supply", "outbound"}) do
if #byKind[kind] > 0 then
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")})
end
end
return groups
end
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
function ADFlowView.orderedRuns(runs)
local result = {}
for _, group in ipairs(ADFlowView.boardGroups(runs)) do
for _, run in ipairs(group.runs) do table.insert(result, run) end
end
return result
end
-- Traktorer: vehicles = {{id, label, state = "in"|"free"|"other"|"blocked"}}. Standard: flowets egne,
-- så ledige der kan bruges. showAll tilføjer traktorer i andre flows og låste (vogn passer ikke).
local STATE_ORDER = {"in", "free", "other", "blocked"}
function ADFlowView.vehicleRows(vehicles, showAll)
local byState = {}
for _, entry in ipairs(vehicles or {}) do
byState[entry.state] = byState[entry.state] or {}
table.insert(byState[entry.state], entry)
end
local rows = {}
for _, state in ipairs(STATE_ORDER) do
if showAll or state == "in" or state == "free" then
for _, entry in ipairs(sortedBy(byState[state], "label")) do table.insert(rows, entry) end
end
end
return rows
end
function ADFlowView.hiddenCount(all, shown)
return #(all or {}) - #(shown or {})
end
-- Bygninger: buildings = {{id, name, hasMarker}}, chosen = {[id] = true}. Valgte først; bygninger uden
-- markør kun hvis valgte eller showAll (de kan alligevel ikke bruges før en markør er sat).
function ADFlowView.buildingRows(buildings, chosen, showAll)
local picked, withMarker, withoutMarker = {}, {}, {}
for _, building in ipairs(buildings or {}) do
if chosen ~= nil and chosen[building.id] then
table.insert(picked, building)
elseif building.hasMarker then
table.insert(withMarker, building)
elseif showAll then
table.insert(withoutMarker, building)
end
end
local rows = {}
for _, list in ipairs({sortedBy(picked, "name"), sortedBy(withMarker, "name"), sortedBy(withoutMarker, "name")}) do
for _, building in ipairs(list) do table.insert(rows, building) end
end
return rows
end
+41 -1
View File
@@ -167,8 +167,48 @@ local function getLoadStationAtMarker(adEnv, wayPoint, farmId)
return getClosestStation(adEnv, g_currentMission.storageSystem:getLoadingStations(), "loadTriggers", LOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId)
end
-- Staldes foderkrybbe (spec_husbandryFood.feedingTrough, samme tippe-trigger som AutoDrive bruger) står
-- ikke altid i aflæsningsstationens unloadTriggers — hønsehuset har KUN krybben. Opslaget får derfor en
-- proxy pr. krybbe der peger på staldens station (eller en minimal station). Caches i UNLOAD_CACHE_MS.
ADSmartPickup.UNLOAD_CACHE_MS = 1000
local unloadCache = {list = nil, time = -math.huge}
function ADSmartPickup.invalidateUnloadCandidates()
unloadCache.list, unloadCache.time = nil, -math.huge
end
local function getUnloadCandidates()
local now = g_currentMission ~= nil and type(g_currentMission.time) == "number" and g_currentMission.time or nil
if unloadCache.list ~= nil and now ~= nil and (now - unloadCache.time) < ADSmartPickup.UNLOAD_CACHE_MS then
return unloadCache.list
end
local list, byPlaceable = {}, {}
for _, station in pairs(g_currentMission.storageSystem:getUnloadingStations()) do
table.insert(list, station)
if station.owningPlaceable ~= nil and byPlaceable[station.owningPlaceable] == nil then
byPlaceable[station.owningPlaceable] = station
end
end
local placeables = g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
for _, placeable in pairs(placeables) do
local food = placeable.spec_husbandryFood
local trough = food ~= nil and food.feedingTrough or nil
local node = trough ~= nil and (trough.exactFillRootNode or trough.triggerNode) or nil
if node ~= nil then
local station = byPlaceable[placeable] or {
owningPlaceable = placeable,
-- spillets adgangstjek kalder getOwnerFarmId på stationen
getOwnerFarmId = function() return placeable.getOwnerFarmId ~= nil and placeable:getOwnerFarmId() or placeable.ownerFarmId end,
}
table.insert(list, setmetatable({unloadTriggers = {{exactFillRootNode = node}}}, {__index = station}))
end
end
unloadCache.list, unloadCache.time = list, now or -math.huge
return list
end
local function getUnloadStationAtMarker(adEnv, wayPoint, farmId)
return getClosestStation(adEnv, g_currentMission.storageSystem:getUnloadingStations(), "unloadTriggers", UNLOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId)
return getClosestStation(adEnv, getUnloadCandidates(), "unloadTriggers", UNLOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId)
end
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
+24 -8
View File
@@ -70,10 +70,14 @@ function SmartPickupFrame:fillBoardRows(adEnv)
local rows = {}
local header = self:addTextRow(self.i18n:getText("spu_section_flows"))
table.insert(rows, header.parent)
for _, run in ipairs(ADRunsController.store.runs) do
local title = self:addTextRow(self:describeFlow(adEnv, run, run.id == self.selectedRunId))
table.insert(rows, title.parent)
table.insert(self.boardRows, {run = run, title = title})
-- grupperet: Ind til bygning, så Ud fra bygning — hver efter navn
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
table.insert(rows, self:addTextRow(self.i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply")).parent)
for _, run in ipairs(group.runs) do
local title = self:addTextRow(self:describeFlow(adEnv, run, run.id == self.selectedRunId))
table.insert(rows, title.parent)
table.insert(self.boardRows, {run = run, title = title})
end
end
if #ADRunsController.store.runs == 0 then
table.insert(rows, self:addTextRow(self.i18n:getText("spu_noRuns")).parent)
@@ -126,17 +130,29 @@ function SmartPickupFrame:fillBuildingRows(adEnv, run, role)
local rowKind = role == "load" and "sourceBuilding" or role == "store" and "storeBuilding" or "targetBuilding"
for _, id in ipairs(ids or {}) do chosen[id] = true end
local count = 0
-- kandidater -> valgte først, så med markør; uden markør kun med "Vis alle bygninger"
local candidates = {}
for _, building in ipairs(ADBuildings.list(adEnv, g_currentMission:getFarmId())) do
-- "Lager til": siloer og gårdens fabrikker (fx BGA) — markøren er bygningens aflæsningsmarkør
local fits = (role == "load" and building.canBeSource) or (role == "unload" and building.canBeTarget)
or (role == "store" and (building.kind == "silo" or building.kind == "factory"))
if fits then
count = count + 1
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = building.id})
row:getDescendantByName("title"):setText(self:describeBuilding(building, role == "store" and "unload" or role))
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, role == "store" and "unload" or role) or {}
table.insert(candidates, {id = building.id, name = building.name, hasMarker = markers[1] ~= nil, building = building})
end
end
local shown = ADFlowView.buildingRows(candidates, chosen, self.showAllBuildings == true)
local hiddenBuildings = ADFlowView.hiddenCount(candidates, shown)
if hiddenBuildings > 0 or self.showAllBuildings then
self:addToggleRow("showAllBuildings", string.format(self.i18n:getText("spu_showAllBuildings"), hiddenBuildings), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllBuildings == true)
end
for _, entry in ipairs(shown) do
local building = entry.building
count = count + 1
local row = self:addRow(self.ingredientPrefab, {kind = rowKind, buildingId = building.id})
row:getDescendantByName("title"):setText(self:describeBuilding(building, role == "store" and "unload" or role))
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
end
-- valgte bygninger der ikke længere findes (solgt)
for _, id in ipairs(ids or {}) do
if ADBuildings.find(adEnv, g_currentMission:getFarmId(), id) == nil then
+36 -9
View File
@@ -267,7 +267,11 @@ function SmartPickupFrame:setStaticRowsVisible(tab, run)
end
end
-- Samme rækkefølge som tavlen: Ind til bygning først, så Ud fra bygning, hver efter navn.
function SmartPickupFrame:getRunsForTab()
if ADFlowView ~= nil then
return ADFlowView.orderedRuns(ADRunsController.store.runs)
end
return ADRunsController.store.runs
end
@@ -433,28 +437,46 @@ function SmartPickupFrame:fillWaitPoolOption(adEnv, run)
self.waitPoolOption:setState(index, false)
end
-- Traktorer: som standard flowets egne og de ledige der kan bruges; "Vis alle" tilføjer traktorer i andre
-- flows og låste (vognen passer ikke) — de er tit mange og gør listen uoverskuelig.
function SmartPickupFrame:fillVehicleRows(adEnv, run)
local inRun = {}
for _, id in ipairs(run.vehicleIds) do
inRun[id] = true
end
local entries = {}
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle)
local otherRun = ADRunsController.getRunForVehicle(vehicle)
local state = "free"
if inRun[id] then
state = "in"
elseif otherRun ~= nil and otherRun.id ~= run.id then
state = "other"
elseif SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] then
state = "blocked"
end
table.insert(entries, {id = id, label = ADRunsController.getVehicleLabel(adEnv, vehicle), state = state, vehicle = vehicle, otherRun = otherRun})
end
local rows = ADFlowView.vehicleRows(entries, self.showAllVehicles == true)
local hidden = ADFlowView.hiddenCount(entries, rows)
if hidden > 0 or self.showAllVehicles then
self.showAllVehiclesOption = self:addToggleRow("showAllVehicles", string.format(self.i18n:getText("spu_showAllVehicles"), hidden), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllVehicles == true)
end
for _, entry in ipairs(rows) do
local vehicle = entry.vehicle
local row = self:addRow(self.vehiclePrefab, {kind = "vehicle", vehicle = vehicle, row = nil})
local option, title = row:getDescendantByName("option"), row:getDescendantByName("title")
self.rowData[option].row = row
option:setState(inRun[id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
-- Traktorer i et ANDET run kan ikke vælges her (én traktor = ét run); rækken viser hvor den er.
local otherRun = ADRunsController.getRunForVehicle(vehicle)
if otherRun ~= nil and otherRun.id ~= run.id then
option:setState(entry.state == "in" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
if entry.state == "other" then
-- én traktor = ét flow; rækken viser hvor den er
option:setDisabled(true)
self.rowData[option].locked = true
title:setText(string.format("%s — %s", ADRunsController.getVehicleLabel(adEnv, vehicle), string.format(self.i18n:getText("spu_inRun"), tostring(otherRun.name))))
title:setText(string.format("%s — %s", entry.label, string.format(self.i18n:getText("spu_inRun"), tostring(entry.otherRun.name))))
else
title:setText(self:describeVehicle(adEnv, run, vehicle, inRun[id]))
-- Vogn der ikke kan bære run'ets varer (ingen vogn / forkert vogn / paller uden UAL) kan ikke
-- vælges; er den allerede i run'et, kan den stadig fravælges.
if not inRun[id] and SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] then
title:setText(self:describeVehicle(adEnv, run, vehicle, entry.state == "in"))
if entry.state == "blocked" then
option:setDisabled(true)
self.rowData[option].locked = true
end
@@ -678,6 +700,11 @@ function SmartPickupFrame:onClickIngredient(state, element)
return self:onClickCollectRound(state)
elseif data ~= nil and data.kind == "direction" then
return self:onClickDirection(state)
elseif data ~= nil and (data.kind == "showAllVehicles" or data.kind == "showAllBuildings") then
local isOn = state == SmartPickupFrame.STATE_ON
if data.kind == "showAllVehicles" then self.showAllVehicles = isOn else self.showAllBuildings = isOn end
self:requestRebuild()
return
elseif data ~= nil and (data.kind == "sourceBuilding" or data.kind == "targetBuilding" or data.kind == "storeBuilding") then
return self:onClickBuilding(state, element)
end
+4
View File
@@ -135,6 +135,10 @@
<text name="spu_stat_use" text="forbrug %s/døgn"/>
<text name="spu_stat_prod" text="prod. %s/døgn"/>
<text name="spu_stat_total" text="Bygningen i alt"/>
<text name="spu_board_supply" text="· Ind til bygning ·"/>
<text name="spu_board_outbound" text="· Ud fra bygning ·"/>
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
<text name="spu_showAllBuildings" text="Vis alle bygninger (%d skjult uden markør)"/>
<text name="spu_section_store" text="— Lager til (valgfrit; intet valgt = moden vælger selv) —"/>
<text name="spu_stat_rising" text="stiger"/>
<text name="spu_tab_factories" text="Fabrikker"/>
+4
View File
@@ -135,6 +135,10 @@
<text name="spu_stat_use" text="use %s/day"/>
<text name="spu_stat_prod" text="prod. %s/day"/>
<text name="spu_stat_total" text="Building total"/>
<text name="spu_board_supply" text="· Into building ·"/>
<text name="spu_board_outbound" text="· Out of building ·"/>
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
<text name="spu_showAllBuildings" text="Show all buildings (%d hidden without marker)"/>
<text name="spu_section_store" text="— Store to (optional; none chosen = the mod picks) —"/>
<text name="spu_stat_rising" text="rising"/>
<text name="spu_tab_factories" text="Factories"/>
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.18.8.0</version>
<version>1.19.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -21,6 +21,7 @@
<sourceFile filename="adPrices.lua"/>
<sourceFile filename="adOutboundPlanner.lua"/>
<sourceFile filename="adStorageStats.lua"/>
<sourceFile filename="adFlowView.lua"/>
<sourceFile filename="adSources.lua"/>
<sourceFile filename="adBuildings.lua"/>
<sourceFile filename="adOutbound.lua"/>