feat(navne): nye bygninger navngives automatisk (stald efter dyretype, gårdsilo/marksilo, palleskur, fabrik efter butiksnavn; numre fortsætter) + Omdøb bygning-knap i bygningsfanerne (v1.40.0.0)

This commit is contained in:
2026-09-27 15:10:24 +02:00
parent 9a284c875e
commit 7d372eb5ab
8 changed files with 274 additions and 6 deletions
+5
View File
@@ -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
+128 -5
View File
@@ -1,7 +1,7 @@
-- AD Names
-- Egne bygningsnavne (FS25 kan ikke omdøbe bygninger): ADSmartPickup_names.xml i savegame-mappen,
-- <ADSmartPickupNames><building id="placeable…" name="Gårdsilo 10"/></ADSmartPickupNames>.
-- 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
+71
View File
@@ -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
@@ -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.
+2
View File
@@ -216,6 +216,8 @@
<text name="spu_button_start" text="Start flow"/>
<text name="spu_button_stop" text="Stop flow"/>
<text name="spu_button_new" text="Nyt flow"/>
<text name="spu_button_renameBuilding" text="Omdøb bygning"/>
<text name="spu_renameBuildingTitle" text="Nyt navn på bygningen (tomt = butiksnavnet)"/>
<text name="spu_button_rename" text="Omdøb"/>
<text name="spu_button_delete" text="Slet flow"/>
<text name="spu_newRunName" text="Flow %d"/>
+2
View File
@@ -216,6 +216,8 @@
<text name="spu_button_start" text="Start flow"/>
<text name="spu_button_stop" text="Stop flow"/>
<text name="spu_button_new" text="New flow"/>
<text name="spu_button_renameBuilding" text="Rename building"/>
<text name="spu_renameBuildingTitle" text="New name for the building (empty = shop name)"/>
<text name="spu_button_rename" text="Rename"/>
<text name="spu_button_delete" text="Delete flow"/>
<text name="spu_newRunName" text="Flow %d"/>
+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.39.0.0</version>
<version>1.40.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -13,6 +13,7 @@
<l10n filenamePrefix="l10n/l10n"/>
<extraSourceFiles>
<sourceFile filename="adCache.lua"/>
<sourceFile filename="adNaming.lua"/>
<sourceFile filename="adNames.lua"/>
<sourceFile filename="adStuckDiag.lua"/>
<sourceFile filename="adSupplyPlanner.lua"/>
+38
View File
@@ -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)