From 7d372eb5ab0b8be336d1da3428a6b05aa7e51c14 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Sun, 27 Sep 2026 15:10:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(navne):=20nye=20bygninger=20navngives=20au?= =?UTF-8?q?tomatisk=20(stald=20efter=20dyretype,=20g=C3=A5rdsilo/marksilo,?= =?UTF-8?q?=20palleskur,=20fabrik=20efter=20butiksnavn;=20numre=20forts?= =?UTF-8?q?=C3=A6tter)=20+=20Omd=C3=B8b=20bygning-knap=20i=20bygningsfaner?= =?UTF-8?q?ne=20(v1.40.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adBuildings.lua | 5 + FS25_ADSmartPickup/adNames.lua | 133 +++++++++++++++++++- FS25_ADSmartPickup/adNaming.lua | 71 +++++++++++ FS25_ADSmartPickup/gui/SmartPickupFrame.lua | 26 ++++ FS25_ADSmartPickup/l10n/l10n_da.xml | 2 + FS25_ADSmartPickup/l10n/l10n_en.xml | 2 + FS25_ADSmartPickup/modDesc.xml | 3 +- tests/test_adNaming.lua | 38 ++++++ 8 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 FS25_ADSmartPickup/adNaming.lua create mode 100644 tests/test_adNaming.lua 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 <en>AD Smart Pickup</en> @@ -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)