diff --git a/FS25_ADSmartPickup/adBuildings.lua b/FS25_ADSmartPickup/adBuildings.lua
index f7aada6..2cc741f 100644
--- a/FS25_ADSmartPickup/adBuildings.lua
+++ b/FS25_ADSmartPickup/adBuildings.lua
@@ -460,6 +460,11 @@ function ADBuildings.list(adEnv, farmId)
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
diff --git a/FS25_ADSmartPickup/adNames.lua b/FS25_ADSmartPickup/adNames.lua
index a5856da..f7f221a 100644
--- a/FS25_ADSmartPickup/adNames.lua
+++ b/FS25_ADSmartPickup/adNames.lua
@@ -1,7 +1,7 @@
-- AD Names
-- Egne bygningsnavne (FS25 kan ikke omdøbe bygninger): ADSmartPickup_names.xml i savegame-mappen,
-- .
--- Moden læser filen og skriver den aldrig — spillerens/Claudes navne overlever. Bruges af alle navneopslag.
+-- Moden tilføjer kun navne (nye bygninger / Omdøb-knappen) og ændrer aldrig et eksisterende navn af sig selv.
ADNames = {}
ADNames.FILE_NAME = "ADSmartPickup_names.xml"
@@ -18,12 +18,16 @@ function ADNames.fromRecords(records)
return byId
end
+local function getPath()
+ local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
+ if path == nil then return nil end
+ return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADNames.FILE_NAME))
+end
+
function ADNames.load()
ADNames.byId = {}
- local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
- if path == nil then return end
- path = path:gsub(ADRunsStorage.FILE_NAME .. "$", ADNames.FILE_NAME)
- if not fileExists(path) then return end
+ local path = getPath()
+ if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupNames", path)
if xmlFile == nil then return end
local records = {}
@@ -43,3 +47,122 @@ function ADNames.forPlaceable(placeable)
if ADNames.byId == nil then pcall(ADNames.load) end
return (ADNames.byId or {})[tostring(placeable.uniqueId)]
end
+
+function ADNames.save()
+ local path = getPath()
+ if path == nil then return false end
+ local xmlFile = XMLFile.create("ADSmartPickupNames", path, ADNames.ROOT)
+ if xmlFile == nil then return false end
+ local ids = {}
+ for id in pairs(ADNames.byId or {}) do table.insert(ids, id) end
+ table.sort(ids)
+ for index, id in ipairs(ids) do
+ local key = string.format("%s.building(%d)", ADNames.ROOT, index - 1)
+ xmlFile:setString(key .. "#id", id)
+ xmlFile:setString(key .. "#name", ADNames.byId[id])
+ end
+ xmlFile:save()
+ xmlFile:delete()
+ return true
+end
+
+-- Omdøb (knappen i bygningsfanerne): gemmes straks; tomt navn = tilbage til butiksnavnet.
+function ADNames.set(id, name)
+ if ADNames.byId == nil then pcall(ADNames.load) end
+ local trimmed = type(name) == "string" and name:match("^%s*(.-)%s*$") or ""
+ ADNames.byId[id] = trimmed ~= "" and trimmed or nil
+ pcall(ADNames.save)
+ Logging.info("ADSmartPickup: bygning %s omdøbt til '%s'", tostring(id), trimmed)
+ if ADBuildings ~= nil and ADBuildings.invalidate ~= nil then ADBuildings.invalidate() end
+end
+
+local function position(placeable)
+ local node = placeable ~= nil and placeable.rootNode or nil
+ if node == nil then return nil end
+ local x, _, z = getWorldTranslation(node)
+ return x, z
+end
+
+local function animalTypeName(placeable)
+ local ok, name = pcall(function()
+ local index = placeable:getAnimalTypeIndex()
+ local animalType = g_currentMission.animalSystem:getTypeByIndex(index)
+ return animalType ~= nil and animalType.name or nil
+ end)
+ return ok and name or nil
+end
+
+local function rawName(placeable)
+ local ok, name = pcall(placeable.getName, placeable)
+ return ok and name ~= nil and tostring(name) or nil
+end
+
+-- Mark-markører ("F118") fra AutoDrive: {{name, x, z}}
+local function fieldMarkers(adEnv)
+ local result = {}
+ for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
+ if tostring(marker.name):match("^F%d+$") then
+ local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
+ if wayPoint ~= nil then table.insert(result, {name = marker.name, x = wayPoint.x, z = wayPoint.z}) end
+ end
+ end
+ return result
+end
+
+-- Nye bygninger (uden navn) får et navn efter ADNaming; kaldes når bygningslisten bygges. Retter navnene i
+-- listen med det samme og gemmer filen, hvis der kom nye.
+function ADNames.autoName(adEnv, buildings)
+ if ADNames.byId == nil then pcall(ADNames.load) end
+ local existing = ADNames.byId or {}
+ local missing = false
+ for _, building in ipairs(buildings) do if existing[building.id] == nil and building.kind ~= "sell" then missing = true break end end
+ if not missing then return end
+ -- gårdens klynge: valgte gårdlagre i marksilo-flowet + siloer der allerede hedder "Gårdsilo …", og naboer ≤ 60 m
+ local silos, farm, queue = {}, {}, {}
+ for _, building in ipairs(buildings) do
+ if building.kind == "silo" then
+ local x, z = position(building.placeable)
+ if x ~= nil then
+ local silo = {id = building.id, x = x, z = z}
+ table.insert(silos, silo)
+ local isStore = ADFieldSilos ~= nil and ADFieldSiloConfig ~= nil and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id)
+ if isStore or tostring(existing[building.id] or ""):match("^Gårdsilo") then farm[building.id] = true; table.insert(queue, silo) end
+ end
+ end
+ end
+ while #queue > 0 do
+ local current = table.remove(queue)
+ for _, silo in ipairs(silos) do
+ if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= 3600 then farm[silo.id] = true; table.insert(queue, silo) end
+ end
+ end
+ local fields = fieldMarkers(adEnv)
+ local inputs = {}
+ for _, building in ipairs(buildings) do
+ if existing[building.id] == nil then
+ local x, z = position(building.placeable)
+ local nearest = nil
+ if x ~= nil and building.kind == "silo" then
+ for _, field in ipairs(fields) do
+ local distance = math.sqrt((field.x - x) ^ 2 + (field.z - z) ^ 2)
+ if nearest == nil or distance < nearest.distance then nearest = {name = field.name, distance = distance} end
+ end
+ end
+ table.insert(inputs, {id = building.id, kind = building.kind, animal = building.kind == "husbandry" and animalTypeName(building.placeable) or nil,
+ storeName = rawName(building.placeable), inFarmCluster = farm[building.id] == true, nearestField = nearest})
+ end
+ end
+ local added = ADNaming.assign(inputs, existing)
+ local count = 0
+ for id, name in pairs(added) do
+ existing[id] = name
+ count = count + 1
+ Logging.info("ADSmartPickup: ny bygning navngivet '%s' (%s)", name, id)
+ end
+ if count == 0 then return end
+ ADNames.byId = existing
+ for _, building in ipairs(buildings) do
+ if added[building.id] ~= nil then building.name = added[building.id] end
+ end
+ pcall(ADNames.save)
+end
diff --git a/FS25_ADSmartPickup/adNaming.lua b/FS25_ADSmartPickup/adNaming.lua
new file mode 100644
index 0000000..5fe2766
--- /dev/null
+++ b/FS25_ADSmartPickup/adNaming.lua
@@ -0,0 +1,71 @@
+-- AD Naming
+-- Regler for automatiske navne på bygninger uden eget navn (rene funktioner; spil-delen samler input i adNames).
+-- Stalde efter dyretype, siloer efter gårdens klynge/nærmeste mark, palleskure nummereret, fabrikker efter
+-- butiksnavnet. Numrene fortsætter fra de navne, der allerede findes ("Svinestald 1-5" -> "Svinestald 6").
+
+ADNaming = {}
+
+ADNaming.ANIMAL_PREFIX = {COW = "Kostald", PIG = "Svinestald", SHEEP = "Fårestald", GOAT = "Gedestald",
+ CHICKEN = "Hønsehus", HORSE = "Hestestald"}
+ADNaming.FIELD_SILO_DISTANCE = 200
+
+local function escape(text) return (tostring(text):gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")) end
+
+-- Højeste "prefix N" blandt navnene + 1.
+function ADNaming.nextNumber(names, prefix)
+ local highest, pattern = 0, "^" .. escape(prefix) .. " (%d+)$"
+ for _, name in ipairs(names) do
+ local number = tonumber(tostring(name):match(pattern))
+ if number ~= nil and number > highest then highest = number end
+ end
+ return highest + 1
+end
+
+local function contains(names, wanted)
+ for _, name in ipairs(names) do if name == wanted then return true end end
+ return false
+end
+
+-- Et fast navn der kan være brugt: "X", ellers "X 2", "X 3" ...
+local function uniqueFixed(names, base)
+ if not contains(names, base) then return base end
+ return string.format("%s %d", base, math.max(2, ADNaming.nextNumber(names, base)))
+end
+
+-- Navn for én bygning (nil = navngives ikke, fx salgssteder).
+-- b = {kind, animal, storeName, inFarmCluster, nearestField = {name, distance}}
+local function nameFor(b, names)
+ if b.kind == "husbandry" then
+ local prefix = ADNaming.ANIMAL_PREFIX[tostring(b.animal or ""):upper()] or "Stald"
+ return string.format("%s %d", prefix, ADNaming.nextNumber(names, prefix))
+ elseif b.kind == "silo" then
+ if b.inFarmCluster then return string.format("Gårdsilo %d", ADNaming.nextNumber(names, "Gårdsilo")) end
+ local field = b.nearestField
+ if field ~= nil and field.name ~= nil and (field.distance or math.huge) <= ADNaming.FIELD_SILO_DISTANCE then
+ return uniqueFixed(names, field.name .. " marksilo")
+ end
+ return string.format("Silo %d", ADNaming.nextNumber(names, "Silo"))
+ elseif b.kind == "objectStorage" then
+ return string.format("Palleskur %d", ADNaming.nextNumber(names, "Palleskur"))
+ elseif b.kind == "sell" then
+ return nil
+ end
+ if b.storeName == nil or b.storeName == "" then return nil end
+ return uniqueFixed(names, b.storeName)
+end
+
+-- Navne til bygninger der ikke har et endnu. existing = {[id] = navn}; returnerer kun de nye {[id] = navn}.
+function ADNaming.assign(buildings, existing)
+ local names, result = {}, {}
+ for _, name in pairs(existing or {}) do table.insert(names, name) end
+ for _, b in ipairs(buildings or {}) do
+ if b.id ~= nil and (existing or {})[b.id] == nil then
+ local name = nameFor(b, names)
+ if name ~= nil then
+ result[b.id] = name
+ table.insert(names, name)
+ end
+ end
+ end
+ return result
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
index 7dec211..3ddfd64 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
@@ -54,6 +54,9 @@ function SmartPickupFrame.new(i18n, getAdEnv)
self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end}
self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end}
self.monthButtons = {self.backButtonInfo}
+ -- bygningsfanerne (Siloer/Stalde/Fabrikker/Palleskure): omdøb den valgte bygning
+ self.renameBuildingButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_renameBuilding"), callback = function() self:onClickRenameBuilding() end}
+ self.storageButtons = {self.backButtonInfo, self.renameBuildingButtonInfo}
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark
@@ -569,6 +572,9 @@ function SmartPickupFrame:refreshButtons()
wanted = self:getFieldButtons()
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
wanted = self.monthButtons
+ elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
+ wanted = self.storageButtons
+ self.renameBuildingButtonInfo.disabled = self.storageDetailPlaceable == nil
end
if self.menuButtonInfo ~= wanted then
self.menuButtonInfo = wanted
@@ -816,6 +822,26 @@ function SmartPickupFrame:onClickNew()
self:onClickFlowNew()
end
+-- Omdøb den valgte bygning i en bygningsfane (gemmes i ADSmartPickup_names.xml; tomt = butiksnavnet igen)
+function SmartPickupFrame:onClickRenameBuilding()
+ self:safe(function(frame)
+ local placeable = frame.storageDetailPlaceable
+ if placeable == nil then return end
+ frame.renamingBuildingId = ADBuildings.getId(placeable)
+ local current = ADNames.forPlaceable(placeable) or (placeable.getName ~= nil and placeable:getName()) or ""
+ TextInputDialog.show(SmartPickupFrame.onRenameBuildingResult, frame, tostring(current), frame.i18n:getText("spu_renameBuildingTitle"), nil, 40, g_i18n:getText("button_ok"))
+ end)
+end
+
+function SmartPickupFrame:onRenameBuildingResult(value, clickOk)
+ self:safe(function(frame)
+ if not clickOk or frame.renamingBuildingId == nil then return end
+ ADNames.set(frame.renamingBuildingId, value)
+ frame.renamingBuildingId = nil
+ frame:requestRebuild()
+ end)
+end
+
function SmartPickupFrame:onClickRename()
-- Dialogen kan kaste (fx manglende i18n-nøgle) — fang det som resten af GUI'et, i stedet
-- for at lade fejlen undslippe klik-callbacken uden statuslinje-besked.
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index a0c3dee..9d4c41d 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -216,6 +216,8 @@
+
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index bef7fcb..919a3e9 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -216,6 +216,8 @@
+
+
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 003c6f5..09a353e 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -1,7 +1,7 @@
masterdraco
- 1.39.0.0
+ 1.40.0.0
AD Smart Pickup
@@ -13,6 +13,7 @@
+
diff --git a/tests/test_adNaming.lua b/tests/test_adNaming.lua
new file mode 100644
index 0000000..4dc105a
--- /dev/null
+++ b/tests/test_adNaming.lua
@@ -0,0 +1,38 @@
+-- Kør: luajit tests/test_adNaming.lua (fra repo-roden)
+-- Automatiske navne på bygninger uden eget navn (in-game 27/9: "36 Farma" hed alle "Farma 500 + Obi 1000").
+dofile("FS25_ADSmartPickup/adNaming.lua")
+local N = ADNaming
+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
+
+-- næste ledige nummer
+check("næste efter 1-5", N.nextNumber({"Svinestald 1", "Svinestald 5", "Kostald 7"}, "Svinestald"), 6)
+check("ingen endnu -> 1", N.nextNumber({"Kostald 1"}, "Svinestald"), 1)
+check("prefix-præfiks forveksles ikke", N.nextNumber({"Gårdsilo ekstra ved 1", "Gårdsilo 3"}, "Gårdsilo"), 4)
+
+-- navne efter type
+local function one(b, existing) return N.assign({b}, existing or {})[b.id] end
+check("svinestald", one({id = "a", kind = "husbandry", animal = "PIG"}, {x = "Svinestald 5"}), "Svinestald 6")
+check("gedestald", one({id = "a", kind = "husbandry", animal = "GOAT"}), "Gedestald 1")
+check("ukendt dyr -> Stald", one({id = "a", kind = "husbandry", animal = "LLAMA"}), "Stald 1")
+check("silo i gårdens klynge", one({id = "a", kind = "silo", inFarmCluster = true}, {x = "Gårdsilo 35"}), "Gårdsilo 36")
+check("silo ved mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}), "F118 marksilo")
+check("anden marksilo ved samme mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}, {x = "F118 marksilo"}), "F118 marksilo 2")
+check("silo langt fra alt", one({id = "a", kind = "silo", nearestField = {name = "F1", distance = 900}}), "Silo 1")
+check("palleskur", one({id = "a", kind = "objectStorage"}, {x = "Palleskur 1"}), "Palleskur 2")
+check("fabrik: butiksnavnet", one({id = "a", kind = "factory", storeName = "Sugarbeet Crusher"}), "Sugarbeet Crusher")
+check("fabrik nr. 2 med samme navn", one({id = "a", kind = "factory", storeName = "Roesnitter"}, {x = "Roesnitter"}), "Roesnitter 2")
+check("salgssted navngives ikke", one({id = "a", kind = "sell", storeName = "Mølle"}), nil)
+check("har allerede navn -> urørt", N.assign({{id = "a", kind = "husbandry", animal = "PIG"}}, {a = "Min stald"}).a, nil)
+
+-- flere nye i samme runde får hver sit nummer
+local two = N.assign({{id = "a", kind = "husbandry", animal = "COW"}, {id = "b", kind = "husbandry", animal = "COW"}}, {})
+check("to nye kostalde: 1 og 2", (two.a or "") .. "|" .. (two.b or ""), "Kostald 1|Kostald 2")
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)