diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index dcd6c80..0cbb5f7 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -10,6 +10,11 @@ ADSmartPickup = {} +ADSmartPickup.dir = g_currentModDirectory +if ADSmartPickup.dir ~= nil and source ~= nil then + source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua") +end + -- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et -- vogntog bagved. Siloer tættere end dette regnes som "markørens silo". ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40 @@ -671,6 +676,76 @@ local function restoreStoppedVehicles() end end +-- Indsætter fanen i ESC-menuen foran Statistik/Priser (mønster fra FS25_FarmOperationsDashboard). +local function insertInGameMenuPage(frame, pageName, iconFile, uvs, position, predicate) + local inGameMenu = g_gui ~= nil and g_gui.screenControllers ~= nil and g_gui.screenControllers[InGameMenu] or nil + if inGameMenu == nil or inGameMenu.pagingElement == nil then + return false + end + local insertAt = position + for i = 1, #inGameMenu.pagingElement.elements do + local child = inGameMenu.pagingElement.elements[i] + if child == inGameMenu.pageStatistics or child == inGameMenu.pagePrices then + insertAt = i + break + end + end + if inGameMenu.controlIDs ~= nil then + inGameMenu.controlIDs[pageName] = nil + end + inGameMenu[pageName] = frame + inGameMenu.pagingElement:addElement(frame) + if inGameMenu.exposeControlsAsFields ~= nil then + inGameMenu:exposeControlsAsFields(pageName) + end + for _, list in ipairs({inGameMenu.pagingElement.elements, inGameMenu.pageFrames}) do + for i = 1, #list do + if list[i] == frame then + table.remove(list, i) + table.insert(list, insertAt, frame) + break + end + end + end + for i = 1, #inGameMenu.pagingElement.pages do + if inGameMenu.pagingElement.pages[i].element == frame then + local page = table.remove(inGameMenu.pagingElement.pages, i) + table.insert(inGameMenu.pagingElement.pages, insertAt, page) + break + end + end + inGameMenu.pagingElement:updateAbsolutePosition() + inGameMenu.pagingElement:updatePageMapping() + inGameMenu:registerPage(frame, position, predicate) + inGameMenu:addPageTab(frame, iconFile, GuiUtils.getUVs(uvs)) + inGameMenu:rebuildTabList() + return true +end + +local function loadGui() + if g_gui == nil or SmartPickupFrame == nil or ADSmartPickup.dir == nil then + return + end + g_gui:loadProfiles(ADSmartPickup.dir .. "gui/guiProfiles.xml") + local frame = SmartPickupFrame.new(g_i18n, getAutoDriveEnv) + g_gui:loadGui(ADSmartPickup.dir .. "gui/SmartPickupFrame.xml", "smartPickupFrame", frame, true) + local iconFile = Utils.getFilename("icon.dds", ADSmartPickup.dir) + if not insertInGameMenuPage(frame, "smartPickupFrame", iconFile, {0, 0, 512, 512}, 2, function() return g_currentMission ~= nil end) then + Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke indsætte fanen i ESC-menuen") + return + end + frame:initialize() + ADSmartPickup.guiFrame = frame + Logging.info(ADSmartPickup.LOG_PREFIX .. "ESC-fane 'Smart Pickup' registreret") +end + +function ADSmartPickup:loadMap() + local ok, err = pcall(loadGui) + if not ok then + Logging.warning(ADSmartPickup.LOG_PREFIX .. "GUI kunne ikke indlæses (resten af moden virker): %s", tostring(err)) + end +end + function ADSmartPickup:update(dt) if not isHooked then local adEnv = getAutoDriveEnv() diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua new file mode 100644 index 0000000..b44168d --- /dev/null +++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua @@ -0,0 +1,407 @@ +-- Smart Pickup — ESC-fane +-- Viser og redigerer runs og starter/stopper dem. Al logik ligger i ADRunsController; +-- her oversættes kun mellem GUI-elementer og run-felter. Alt kører i pcall så en GUI-fejl +-- aldrig tager spillet med sig. + +SmartPickupFrame = {} +SmartPickupFrame._mt = Class(SmartPickupFrame, TabbedMenuFrameElement) + +SmartPickupFrame.REFRESH_INTERVAL_MS = 1000 +SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: " +SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2 + +function SmartPickupFrame.new(i18n, getAdEnv) + local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt) + self.name = "SmartPickupFrame" + self.i18n = i18n + self.getAdEnv = getAdEnv + self.selectedRunId = nil + self.runIds, self.targetIds, self.poolGroups = {}, {}, {} + self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...} + self.clonedRows = {} + self.refreshTimer = 0 + self.statusOverride = nil + self.backButtonInfo = {inputAction = InputAction.MENU_BACK} + self.startButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickStartStop() end} + self.newButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_button_new"), callback = function() self:onClickNew() end} + self.renameButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_rename"), callback = function() self:onClickRename() end} + self.deleteButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickDelete() end} + self:setMenuButtonInfo({self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}) + return self +end + +function SmartPickupFrame:delete() + SmartPickupFrame:superClass().delete(self) +end + +function SmartPickupFrame:copyAttributes(src) + SmartPickupFrame:superClass().copyAttributes(self, src) + self.i18n, self.getAdEnv = src.i18n, src.getAdEnv +end + +-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker. +function SmartPickupFrame:initialize() + for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "poolPrefab"}) do + local prefab = self[name] + if prefab ~= nil then + prefab:unlinkElement() + FocusManager:removeElement(prefab) + end + end + self.ingredientModeOption:setTexts({self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")}) + local loopTexts = {self.i18n:getText("spu_loops_infinite")} + for loops = 1, ADRuns.MAX_LOOPS do + table.insert(loopTexts, tostring(loops)) + end + self.loopsOption:setTexts(loopTexts) +end + +function SmartPickupFrame:onFrameOpen() + SmartPickupFrame:superClass().onFrameOpen(self) + self.statusOverride = nil + self:safe(self.rebuild) + FocusManager:setFocus(self.runOption) +end + +function SmartPickupFrame:onFrameClose() + SmartPickupFrame:superClass().onFrameClose(self) +end + +-- Live-opdatering af status/traktor-rækker mens siden er åben (uden at genopbygge rækkerne). +function SmartPickupFrame:update(dt) + SmartPickupFrame:superClass().update(self, dt) + self.refreshTimer = self.refreshTimer + dt + if self.refreshTimer >= SmartPickupFrame.REFRESH_INTERVAL_MS then + self.refreshTimer = 0 + self:safe(self.refreshLive) + end +end + +function SmartPickupFrame:safe(fn, ...) + local ok, err = pcall(fn, self, ...) + if not ok then + Logging.warning(SmartPickupFrame.LOG_PREFIX .. "GUI-fejl: %s", tostring(err)) + self.statusOverride = string.format(self.i18n:getText("spu_status_error"), tostring(err)) + if self.statusText ~= nil then + self.statusText:setText(self.statusOverride) + end + end +end + +function SmartPickupFrame:getRun() + return self.selectedRunId ~= nil and ADRuns.getRun(ADRunsController.store, self.selectedRunId) or nil +end + +-- ---- opbygning ---- + +function SmartPickupFrame:clearRows() + for _, row in ipairs(self.clonedRows) do + row:delete() + end + self.clonedRows, self.rowData = {}, {} +end + +function SmartPickupFrame:addRow(prefab, parent, data) + local row = prefab:clone(parent) + FocusManager:loadElementFromCustomValues(row) + table.insert(self.clonedRows, row) + local option = row:getDescendantByName("option") + if option ~= nil then + option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}) + self.rowData[option] = data + end + return row +end + +function SmartPickupFrame:rebuild() + local adEnv = self.getAdEnv() + local store = ADRunsController.store + if self.selectedRunId == nil or ADRuns.getRun(store, self.selectedRunId) == nil then + self.selectedRunId = store.runs[1] ~= nil and store.runs[1].id or nil + end + self:clearRows() + self:fillRunOption(adEnv) + local run = self:getRun() + local hasRun = run ~= nil and adEnv ~= nil + for _, element in ipairs({self.targetOption, self.ingredientModeOption, self.waitPoolOption, self.loopsOption}) do + element:setDisabled(not hasRun) + end + if hasRun then + self:fillTargetOption(adEnv, run) + self.ingredientModeOption:setState(run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, true) + self:fillIngredientRows(adEnv, run) + self:fillWaitPoolOption(adEnv, run) + self.loopsOption:setState(run.loops + 1, true) + self:fillVehicleRows(adEnv, run) + end + self:fillPoolRows(adEnv) + for _, box in ipairs({self.ingredientBox, self.vehicleBox, self.poolBox, self.boxLayout}) do + box:invalidateLayout() + end + self:refreshLive() +end + +function SmartPickupFrame:fillRunOption(adEnv) + local texts, index = {}, 1 + self.runIds = {} + for position, run in ipairs(ADRunsController.store.runs) do + local status = adEnv ~= nil and ADRunsController.getRunStatus(adEnv, run) or {running = 0, total = #run.vehicleIds} + table.insert(texts, string.format("%s (%d/%d)", run.name, status.running, status.total)) + table.insert(self.runIds, run.id) + if run.id == self.selectedRunId then + index = position + end + end + if #texts == 0 then + texts = {self.i18n:getText("spu_noRuns")} + end + self.runOption:setTexts(texts) + self.runOption:setState(index, true) + self.runOption:setDisabled(#self.runIds == 0) +end + +function SmartPickupFrame:fillTargetOption(adEnv, run) + local texts, index = {self.i18n:getText("spu_noTarget")}, 1 + self.targetIds = {false} + for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, g_currentMission:getFarmId())) do + table.insert(texts, tostring(marker.name)) + table.insert(self.targetIds, marker.id) + if marker.id == run.targetWayPointId then + index = #texts + end + end + self.targetOption:setTexts(texts) + self.targetOption:setState(index, true) +end + +function SmartPickupFrame:fillIngredientRows(adEnv, run) + if run.ingredientMode ~= "manual" then + return + end + local chosen = {} + for _, name in ipairs(run.ingredients) do + chosen[name] = true + end + for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, run)) do + local row = self:addRow(self.ingredientPrefab, self.ingredientBox, {kind = "ingredient", name = fillType.name}) + row:getDescendantByName("title"):setText(tostring(fillType.title)) + row:getDescendantByName("option"):setState(chosen[fillType.name] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, true) + end +end + +function SmartPickupFrame:fillWaitPoolOption(adEnv, run) + local texts, index = {self.i18n:getText("spu_waitPool_all")}, 1 + self.poolGroups = {""} + for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do + table.insert(texts, string.format("%s (%d)", pool.group, pool.count)) + table.insert(self.poolGroups, pool.group) + if pool.group == run.waitPoolGroup then + index = #texts + end + end + self.waitPoolOption:setTexts(texts) + self.waitPoolOption:setState(index, true) +end + +function SmartPickupFrame:fillVehicleRows(adEnv, run) + local inRun = {} + for _, id in ipairs(run.vehicleIds) do + inRun[id] = true + end + for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do + local id = ADRunsController.getVehicleId(vehicle) + local row = self:addRow(self.vehiclePrefab, self.vehicleBox, {kind = "vehicle", vehicle = vehicle, row = nil}) + self.rowData[row:getDescendantByName("option")].row = row + row:getDescendantByName("option"):setState(inRun[id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, true) + row:getDescendantByName("title"):setText(self:describeVehicle(adEnv, run, vehicle, inRun[id])) + end +end + +-- "Fastrac 2135 4WS (#1) — 1.000.000 l — ✔" / "… — ✖ ingen vogn"; reservationsnote fra Start bevares. +function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun) + local label = ADRunsController.getVehicleLabel(adEnv, vehicle) + if not isInRun then + return label + end + local validation = ADRunsController.validateVehicle(adEnv, run, vehicle) + local capacity = validation.capacity ~= nil and string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)) or nil + local parts = {label} + if capacity ~= nil then + table.insert(parts, capacity) + end + if validation.ok then + table.insert(parts, "✔") + else + table.insert(parts, "✖ " .. self.i18n:getText("spu_reason_" .. tostring(validation.reason))) + end + return table.concat(parts, " — ") +end + +function SmartPickupFrame:fillPoolRows(adEnv) + if adEnv == nil then + return + end + local occupied = {} + for _, claim in ipairs(ADUnloadWait.getClaims()) do + occupied[claim.marker.group] = occupied[claim.marker.group] or {} + table.insert(occupied[claim.marker.group], ADRunsController.getVehicleLabel(adEnv, claim.vehicle)) + end + for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do + local names = occupied[pool.group] or {} + local suffix = #names > 0 and (" (" .. table.concat(names, ", ") .. ")") or "" + local row = self:addRow(self.poolPrefab, self.poolBox, {kind = "pool"}) + row:getDescendantByName("title"):setText(string.format(self.i18n:getText("spu_waitPoolRow"), pool.group, pool.count, #names, suffix)) + end +end + +-- Status + Start/Stop-knappens tekst, kaldes hvert sekund. +function SmartPickupFrame:refreshLive() + local adEnv = self.getAdEnv() + local run = self:getRun() + if adEnv == nil then + self.statusText:setText(self.i18n:getText("spu_status_noAd")) + return + end + local isRunning = false + if run ~= nil then + local status = ADRunsController.getRunStatus(adEnv, run) + isRunning = status.running > 0 + local text = isRunning and string.format(self.i18n:getText("spu_status_running"), status.running, status.total) + or string.format(self.i18n:getText("spu_status_stopped"), status.total) + self.statusText:setText(self.statusOverride or text) + for option, data in pairs(self.rowData) do + if data.kind == "vehicle" and data.row ~= nil then + data.row:getDescendantByName("title"):setText(self:describeVehicle(adEnv, run, data.vehicle, option:getState() == SmartPickupFrame.STATE_ON)) + end + end + else + self.statusText:setText(self.statusOverride or self.i18n:getText("spu_noRuns")) + end + self.startButtonInfo.text = self.i18n:getText(isRunning and "spu_button_stop" or "spu_button_start") + self.startButtonInfo.disabled = run == nil + self.renameButtonInfo.disabled = run == nil + self.deleteButtonInfo.disabled = run == nil + self:setMenuButtonInfoDirty() +end + +-- ---- callbacks fra XML (state = 1-baseret indeks; element = det klikkede element) ---- + +function SmartPickupFrame:onClickRun(state) + self.selectedRunId = self.runIds[state] + self.statusOverride = nil + self:safe(self.rebuild) +end + +function SmartPickupFrame:onClickTarget(state) + local run = self:getRun() + if run == nil then return end + -- targetIds[1] er false = "(vælg mål)"; modellen tolker false som "ryd målet" + ADRunsController.updateRun(run.id, {targetWayPointId = self.targetIds[state] or false}) + self:safe(self.rebuild) +end + +function SmartPickupFrame:onClickIngredientMode(state) + local run = self:getRun() + if run == nil then return end + ADRunsController.updateRun(run.id, {ingredientMode = state == SmartPickupFrame.STATE_ON and "manual" or "auto"}) + self:safe(self.rebuild) +end + +function SmartPickupFrame:onClickIngredient(state, element) + local run, data = self:getRun(), self.rowData[element] + if run == nil or data == nil then return end + local names = {} + for _, name in ipairs(run.ingredients) do + if name ~= data.name then table.insert(names, name) end + end + if state == SmartPickupFrame.STATE_ON then + table.insert(names, data.name) + end + ADRunsController.updateRun(run.id, {ingredients = names}) +end + +function SmartPickupFrame:onClickWaitPool(state) + local run = self:getRun() + if run == nil then return end + ADRunsController.updateRun(run.id, {waitPoolGroup = self.poolGroups[state] or ""}) +end + +function SmartPickupFrame:onClickLoops(state) + local run = self:getRun() + if run == nil then return end + ADRunsController.updateRun(run.id, {loops = state - 1}) +end + +function SmartPickupFrame:onClickVehicle(state, element) + local run, data = self:getRun(), self.rowData[element] + if run == nil or data == nil then return end + if state == SmartPickupFrame.STATE_ON then + local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle) + if previousRunId ~= nil then + local previous = ADRuns.getRun(ADRunsController.store, previousRunId) + self.statusOverride = string.format(self.i18n:getText("spu_movedFrom"), previous ~= nil and previous.name or tostring(previousRunId)) + end + else + ADRunsController.unassignVehicle(data.vehicle) + end + self:safe(self.refreshLive) +end + +-- ---- knapper i bundlinjen ---- + +function SmartPickupFrame:onClickStartStop() + self:safe(function(frame) + local adEnv, run = frame.getAdEnv(), frame:getRun() + if adEnv == nil or run == nil then return end + if ADRunsController.getRunStatus(adEnv, run).running > 0 then + ADRunsController.stopRun(adEnv, run.id) + frame.statusOverride = nil + else + local started, skipped = 0, 0 + for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do + if result.ok then started = started + 1 else skipped = skipped + 1 end + end + frame.statusOverride = string.format(frame.i18n:getText("spu_status_started"), started, skipped) + end + ADRunsController.save() + frame:rebuild() + end) +end + +function SmartPickupFrame:onClickNew() + self:safe(function(frame) + local run = ADRunsController.createRun(string.format(frame.i18n:getText("spu_newRunName"), ADRunsController.store.nextId - 1)) + frame.selectedRunId = run.id + frame.statusOverride = nil + frame:rebuild() + end) +end + +function SmartPickupFrame:onClickRename() + local run = self:getRun() + if run == nil then return end + TextInputDialog.show(SmartPickupFrame.onRenameResult, self, run.name, self.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok")) +end + +function SmartPickupFrame:onRenameResult(value, clickOk) + local run = self:getRun() + if clickOk and run ~= nil and value ~= nil and value:match("%S") then + ADRunsController.updateRun(run.id, {name = value}) + self:safe(self.rebuild) + end +end + +function SmartPickupFrame:onClickDelete() + local run = self:getRun() + if run == nil then return end + YesNoDialog.show(SmartPickupFrame.onDeleteResult, self, string.format(self.i18n:getText("spu_deleteConfirm"), run.name), self.i18n:getText("spu_button_delete")) +end + +function SmartPickupFrame:onDeleteResult(yes) + local run = self:getRun() + if yes and run ~= nil then + ADRunsController.deleteRun(run.id) + self.selectedRunId = nil + self:safe(self.rebuild) + end +end diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.xml b/FS25_ADSmartPickup/gui/SmartPickupFrame.xml new file mode 100644 index 0000000..76d1c52 --- /dev/null +++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/gui/guiProfiles.xml b/FS25_ADSmartPickup/gui/guiProfiles.xml new file mode 100644 index 0000000..e75ec5c --- /dev/null +++ b/FS25_ADSmartPickup/gui/guiProfiles.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml new file mode 100644 index 0000000..16d65fe --- /dev/null +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml new file mode 100644 index 0000000..ea4064e --- /dev/null +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index e23508f..29b968b 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -10,6 +10,7 @@ icon.dds +