From be868ec60c2ba7d8fcaaeda9e0dd68d552358a6b Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Fri, 6 Mar 2026 17:13:25 +0100 Subject: [PATCH 01/10] feat: Add console commands reference and enhance README with detailed features --- .gitignore | 7 +++++++ .vscode/settings.json | 3 +++ docs | 1 + 3 files changed, 11 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 160000 docs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..194e419 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.claude/settings.local.json +.vscode/lua-format.yaml +build.* +CLAUDE.md +feature.md +roadmap.md +skillsEmployee.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b2556d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "DBAPI.dataPath": "C:\\Users\\xalsi\\Documents\\My Games\\FarmingSimulator2025\\savegame2\\DBAPI_data" +} \ No newline at end of file diff --git a/docs b/docs new file mode 160000 index 0000000..993763e --- /dev/null +++ b/docs @@ -0,0 +1 @@ +Subproject commit 993763ec73ecf45deda9e8e3c3cdcca529d32759 From 485d1dab6d38418d2c4a58c141c50289f9705e71 Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Fri, 6 Mar 2026 21:35:32 +0100 Subject: [PATCH 02/10] feat: Update GUI icons and profiles for employee management pages --- scripts/gui/EMGui.lua | 15 +++++++++------ xml/gui/EMEmployeeFrame.xml | 2 +- xml/gui/EMFieldFrame.xml | 2 +- xml/gui/EMVehicleFrame.xml | 2 +- xml/gui/EMWorkflowFrame.xml | 2 +- xml/gui/MenuEmployeeManager.xml | 2 +- xml/gui/guiProfiles.xml | 24 ++++++++++++++++++++++++ 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/scripts/gui/EMGui.lua b/scripts/gui/EMGui.lua index c8e4c64..fd04ebf 100644 --- a/scripts/gui/EMGui.lua +++ b/scripts/gui/EMGui.lua @@ -28,16 +28,19 @@ end function EMGui:setupPages(gui) local pages = { - { gui.pageEmployees, "ingameMenu/tab_character" }, - { gui.pageWorkflows, "ingameMenu/tab_contracts" }, - { gui.pageFields, "ingameMenu/tab_map" }, - { gui.pageVehicles, "ingameMenu/tab_vehicles" }, + { gui.pageEmployees, "images/EMEmployeeIcon.dds" }, + { gui.pageWorkflows, "images/EMWorkflowIcon.dds" }, + { gui.pageFields, "images/EMFieldIcon.dds" }, + { gui.pageVehicles, "images/EMVehicleIcon.dds" }, } + local uvs = GuiUtils.getUVs({0, 0, 1024, 1024}) + for idx, thisPage in ipairs(pages) do - local page, sliceId = unpack(thisPage) + local page, iconPath = unpack(thisPage) + local fullPath = g_modDirectory .. iconPath gui:registerPage(page, idx) - gui:addPageTab(page, nil, nil, sliceId) + gui:addPageTab(page, fullPath, uvs) end gui:rebuildTabList() diff --git a/xml/gui/EMEmployeeFrame.xml b/xml/gui/EMEmployeeFrame.xml index 60a52f0..2e6eb43 100644 --- a/xml/gui/EMEmployeeFrame.xml +++ b/xml/gui/EMEmployeeFrame.xml @@ -2,7 +2,7 @@ - + diff --git a/xml/gui/EMFieldFrame.xml b/xml/gui/EMFieldFrame.xml index 7b89737..9689b16 100644 --- a/xml/gui/EMFieldFrame.xml +++ b/xml/gui/EMFieldFrame.xml @@ -2,7 +2,7 @@ - + diff --git a/xml/gui/EMVehicleFrame.xml b/xml/gui/EMVehicleFrame.xml index adfcb11..5272d2e 100644 --- a/xml/gui/EMVehicleFrame.xml +++ b/xml/gui/EMVehicleFrame.xml @@ -2,7 +2,7 @@ - + diff --git a/xml/gui/EMWorkflowFrame.xml b/xml/gui/EMWorkflowFrame.xml index 0563189..5233ffe 100644 --- a/xml/gui/EMWorkflowFrame.xml +++ b/xml/gui/EMWorkflowFrame.xml @@ -2,7 +2,7 @@ - + diff --git a/xml/gui/MenuEmployeeManager.xml b/xml/gui/MenuEmployeeManager.xml index a31051b..61abd90 100644 --- a/xml/gui/MenuEmployeeManager.xml +++ b/xml/gui/MenuEmployeeManager.xml @@ -8,7 +8,7 @@ --> - + diff --git a/xml/gui/guiProfiles.xml b/xml/gui/guiProfiles.xml index f03c692..50793af 100644 --- a/xml/gui/guiProfiles.xml +++ b/xml/gui/guiProfiles.xml @@ -4,6 +4,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 1c1af22ff38bff5fe7d9c955935702a6b0883c34 Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:13:56 +0100 Subject: [PATCH 03/10] feat: Enhance localization files with new workflow-related messages and error handling --- l10n/l10n_de.xml | 9 +++++++++ l10n/l10n_en.xml | 10 ++++++++++ l10n/l10n_es.xml | 9 +++++++++ l10n/l10n_fr.xml | 10 ++++++++++ l10n/l10n_it.xml | 9 +++++++++ l10n/l10n_pl.xml | 9 +++++++++ 6 files changed, 56 insertions(+) diff --git a/l10n/l10n_de.xml b/l10n/l10n_de.xml index bb6e1a9..fdbc63c 100644 --- a/l10n/l10n_de.xml +++ b/l10n/l10n_de.xml @@ -71,11 +71,17 @@ + + + + + + @@ -152,6 +158,9 @@ + + + diff --git a/l10n/l10n_en.xml b/l10n/l10n_en.xml index 5b926b9..5d450e9 100644 --- a/l10n/l10n_en.xml +++ b/l10n/l10n_en.xml @@ -71,11 +71,17 @@ + + + + + + @@ -90,6 +96,7 @@ + @@ -152,6 +159,9 @@ + + + diff --git a/l10n/l10n_es.xml b/l10n/l10n_es.xml index 439ee7e..374df01 100644 --- a/l10n/l10n_es.xml +++ b/l10n/l10n_es.xml @@ -71,11 +71,17 @@ + + + + + + @@ -152,6 +158,9 @@ + + + diff --git a/l10n/l10n_fr.xml b/l10n/l10n_fr.xml index c05b378..5cd94ef 100644 --- a/l10n/l10n_fr.xml +++ b/l10n/l10n_fr.xml @@ -71,11 +71,17 @@ + + + + + + @@ -90,6 +96,7 @@ + @@ -152,6 +159,9 @@ + + + diff --git a/l10n/l10n_it.xml b/l10n/l10n_it.xml index 9f2b086..ec2b5f3 100644 --- a/l10n/l10n_it.xml +++ b/l10n/l10n_it.xml @@ -74,11 +74,17 @@ + + + + + + @@ -155,6 +161,9 @@ + + + diff --git a/l10n/l10n_pl.xml b/l10n/l10n_pl.xml index f124b69..f7afcf2 100644 --- a/l10n/l10n_pl.xml +++ b/l10n/l10n_pl.xml @@ -71,11 +71,17 @@ + + + + + + @@ -152,6 +158,9 @@ + + + From 5162493bf6c04591e7c86f9b3defdfd891fb6b9d Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:14:01 +0100 Subject: [PATCH 04/10] fix: Improve employee stop warning message localization and cleanup --- scripts/extensions/AIOverrideExtension.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/extensions/AIOverrideExtension.lua b/scripts/extensions/AIOverrideExtension.lua index 82f9ab7..b8fcd91 100644 --- a/scripts/extensions/AIOverrideExtension.lua +++ b/scripts/extensions/AIOverrideExtension.lua @@ -38,16 +38,15 @@ function AIOverrideExtension.onToggleAI(vehicle, actionName, inputValue, callbac if vehicle == nil then return end local employee = g_employeeManager:getEmployeeByVehicle(vehicle) - + if employee ~= nil then if employee.currentJob ~= nil or vehicle:getIsAIActive() then g_employeeManager:consoleStopJob(employee.id) - g_currentMission:showBlinkingWarning(string.format("Employee %s stopped.", employee.name), 2000) + g_currentMission:showBlinkingWarning(string.format(g_i18n:getText("em_employee_stopped") or "Employee %s stopped.", employee.name), 2000) else g_gui:showGui("MenuEmployeeManager") end else g_gui:showGui("MenuEmployeeManager") - g_currentMission:showBlinkingWarning("No employee assigned to this vehicle!", 2000) end end From 7e4ed7fa10527831f40ef0de33d4fba0841d5f53 Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:14:15 +0100 Subject: [PATCH 05/10] feat: Update GUI icons and workflows with new icon management and status handling --- scripts/ModGui.lua | 14 ++++- scripts/gui/EMEmployeeFrame.lua | 34 ++++++------ scripts/gui/EMFieldFrame.lua | 62 ++++++++++++++++++---- scripts/gui/EMGui.lua | 28 +++++++--- scripts/gui/EMVehicleFrame.lua | 2 + scripts/gui/EMWorkflowFrame.lua | 80 +++++++++++++++++++++++++---- scripts/gui/MenuEmployeeManager.lua | 18 +------ scripts/gui/SimpleStatusHUD.lua | 18 +++++-- textures/ui_elements.xml | 8 ++- xml/gui/EMEmployeeFrame.xml | 4 +- xml/gui/EMFieldFrame.xml | 8 ++- xml/gui/EMVehicleFrame.xml | 4 +- xml/gui/EMWorkflowFrame.xml | 4 +- xml/gui/MenuEmployeeManager.xml | 4 +- xml/gui/guiProfiles.xml | 35 +++++++++---- 15 files changed, 240 insertions(+), 83 deletions(-) diff --git a/scripts/ModGui.lua b/scripts/ModGui.lua index 6310248..983ceb4 100644 --- a/scripts/ModGui.lua +++ b/scripts/ModGui.lua @@ -155,9 +155,21 @@ function ModGui:loadMenuFrame(class) g_gui:loadGui(class.XML_FILENAME, class.CLASS_NAME, pageController, true) - local iconPath = 'images/MenuIcon.dds' + local iconSliceToFile = { + EM_IconMenu = "images/MenuIcon.dds", + EM_IconEmployee = "images/EMEmployeeIcon.dds", + EM_IconWorkflow = "images/EMWorkflowIcon.dds", + EM_IconField = "images/EMFieldIcon.dds", + EM_IconVehicle = "images/EMVehicleIcon.dds", + } + + local iconPath = "images/MenuIcon.dds" local uvs = {0, 0, 1024, 1024} + if class.MENU_ICON_SLICE_ID ~= nil and iconSliceToFile[class.MENU_ICON_SLICE_ID] ~= nil then + iconPath = iconSliceToFile[class.MENU_ICON_SLICE_ID] + end + local position = "pageSettings" local predicate = function() return true end addIngameMenuPage(pageController, pageName, iconPath, uvs, position, predicate) diff --git a/scripts/gui/EMEmployeeFrame.lua b/scripts/gui/EMEmployeeFrame.lua index a7cf30a..47c178d 100644 --- a/scripts/gui/EMEmployeeFrame.lua +++ b/scripts/gui/EMEmployeeFrame.lua @@ -7,6 +7,8 @@ EMEmployeeFrame.LIST_TYPE = { HIRED = 2, } +EMEmployeeFrame.MENU_ICON_SLICE_ID = 'EM_IconEmployee' + function EMEmployeeFrame:new() local self = TabbedMenuFrameElement.new(nil, EMEmployeeFrame_mt) self.employees = {} @@ -120,8 +122,15 @@ function EMEmployeeFrame:populateCellForItemInSection(list, section, index, cell elseif emp.isOnBreak then statusText = g_i18n:getText("em_status_on_break") elseif emp.currentJob then - if emp.currentJob.type == "RETURN_TO_PARKING" then + local jobType = emp.currentJob.type + if jobType == "RETURN_TO_PARKING" then statusText = g_i18n:getText("em_status_returning") + elseif jobType == "DRIVING_TO_TOOL" then + statusText = g_i18n:getText("em_status_driving_to_tool") + elseif jobType == "APPROACHING_TOOL" or jobType == "ATTACHING_TOOL" then + statusText = g_i18n:getText("em_status_attaching_tool") + elseif jobType == "RETURNING_TOOL" then + statusText = g_i18n:getText("em_status_returning_tool") else statusText = emp.currentJob.workType or "Working" end @@ -165,7 +174,6 @@ function EMEmployeeFrame:rebuildTable() self.noEmployeesContainer:setVisible(not hasItems) end - -- Hide detail panels when rebuilding if self.detailPanel then self.detailPanel:setVisible(false) end if self.rightPanel then self.rightPanel:setVisible(false) end if self.columnSeparator then self.columnSeparator:setVisible(false) end @@ -209,37 +217,30 @@ function EMEmployeeFrame:displayEmployeeDetails(index) return end - -- Show detail panels if self.detailPanel then self.detailPanel:setVisible(true) end if self.rightPanel then self.rightPanel:setVisible(true) end if self.columnSeparator then self.columnSeparator:setVisible(true) end if self.noSelectedText then self.noSelectedText:setVisible(false) end - -- Portrait if self.detailAvatar then self.detailAvatar:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") end - -- Identity if self.txtName then self.txtName:setText(emp.name) end if self.txtId then self.txtId:setText(string.format("ID: %d", emp.id)) end - -- Status if self.txtStatus then local statusKey = emp.isHired and "em_status_hired" or "em_status_available" self.txtStatus:setText(g_i18n:getText(statusKey)) end - -- Skills with progress bars self:displaySkills(emp) - -- Traits if self.txtTraitsList then local traitName = emp.getTraitName and emp:getTraitName() or nil self.txtTraitsList:setText(traitName or g_i18n:getText("em_none")) end - -- Wage if self.txtWage then local hourly = emp.getHourlyWage and emp:getHourlyWage() or 0 local marketMult = 1.0 @@ -269,7 +270,6 @@ function EMEmployeeFrame:displayEmployeeDetails(index) self.txtWageBreakdown:setText(parts) end - -- Toggle Column 3 content based on hired/available local isHired = emp.isHired if self.hiredInfoSection then self.hiredInfoSection:setVisible(isHired) end if self.availableInfoSection then self.availableInfoSection:setVisible(not isHired) end @@ -278,7 +278,6 @@ function EMEmployeeFrame:displayEmployeeDetails(index) self:displayWorkStats(emp) self:displayPerformanceStats(emp) - -- Workflow summary if self.txtWorkflowSummary then local queue = emp.taskQueue or {} if #queue > 0 then @@ -327,13 +326,11 @@ function EMEmployeeFrame:displaySkills(employee) end function EMEmployeeFrame:displayPersonalInfo(employee) - -- Age if self.txtPersonalAge then local age = employee.age or 30 self.txtPersonalAge:setText(tostring(age)) end - -- Nationality if self.txtPersonalNationality then local natKey = "em_nationality_" .. (employee.nationality or "FR") local natText = g_i18n:getText(natKey) @@ -343,7 +340,6 @@ function EMEmployeeFrame:displayPersonalInfo(employee) self.txtPersonalNationality:setText(natText) end - -- Biography if self.txtPersonalBio then local bioKey = employee.bioKey or "em_bio_default" local bioText = g_i18n:getText(bioKey) @@ -351,7 +347,6 @@ function EMEmployeeFrame:displayPersonalInfo(employee) self.txtPersonalBio:setText(bioText) end - -- Quote if self.txtPersonalQuote then local quoteKey = employee.quoteKey or "em_quote_default" local quoteText = g_i18n:getText(quoteKey) @@ -373,8 +368,15 @@ function EMEmployeeFrame:displayWorkStats(employee) if self.statCurrentJob then if employee.currentJob then local jobType = employee.currentJob.workType or employee.currentJob.type or "Unknown" - if employee.currentJob.type == "RETURN_TO_PARKING" then + local jt = employee.currentJob.type + if jt == "RETURN_TO_PARKING" then jobType = g_i18n:getText("em_status_returning") + elseif jt == "DRIVING_TO_TOOL" then + jobType = g_i18n:getText("em_status_driving_to_tool") + elseif jt == "APPROACHING_TOOL" or jt == "ATTACHING_TOOL" then + jobType = g_i18n:getText("em_status_attaching_tool") + elseif jt == "RETURNING_TOOL" then + jobType = g_i18n:getText("em_status_returning_tool") end local fieldId = employee.currentJob.fieldId if fieldId then diff --git a/scripts/gui/EMFieldFrame.lua b/scripts/gui/EMFieldFrame.lua index 28ec62f..2b2bf67 100644 --- a/scripts/gui/EMFieldFrame.lua +++ b/scripts/gui/EMFieldFrame.lua @@ -9,6 +9,8 @@ function EMFieldFrame:new() return self end +EMFieldFrame.MENU_ICON_SLICE_ID = 'EM_IconField' + function EMFieldFrame:copyAttributes(src) EMFieldFrame:superClass().copyAttributes(self, src) end @@ -16,6 +18,18 @@ end function EMFieldFrame:initialize() self.backButtonInfo = { inputAction = InputAction.MENU_BACK } self.menuButtonInfo = { self.backButtonInfo } + + self.cropNames = {} + if g_employeeManager and g_employeeManager.cropManager then + for name, _ in pairs(g_employeeManager.cropManager.crops) do + table.insert(self.cropNames, name) + end + table.sort(self.cropNames) + end + + if self.targetCropSelector then + self.targetCropSelector:setTexts(self.cropNames) + end end function EMFieldFrame:onGuiSetupFinished() @@ -161,6 +175,20 @@ function EMFieldFrame:displayFieldDetails(index) self.txtGrowthState:setText(growthText) end + if self.targetCropSelector then + local targetCrop = g_employeeManager:getFieldTargetCrop(fieldData.fieldId) + local state = 1 + if targetCrop then + for i, name in ipairs(self.cropNames) do + if name == targetCrop then + state = i + break + end + end + end + self.targetCropSelector:setState(state, false) + end + local conditionText = self:getFieldCondition(fieldData.fieldRef) if self.txtFieldCondition then self.txtFieldCondition:setText(conditionText) @@ -248,12 +276,10 @@ function EMFieldFrame:getAssignedEmployee(fieldId) end function EMFieldFrame:displayAssignedEmployee(emp) - -- Name if self.txtAssignedEmployee then self.txtAssignedEmployee:setText(emp.name or "???") end - -- Status if self.txtEmpStatus then local statusText if emp.isUnpaid then @@ -263,8 +289,15 @@ function EMFieldFrame:displayAssignedEmployee(emp) statusText = g_i18n:getText("em_status_on_break") self.txtEmpStatus:setTextColor(1, 1, 0, 1) elseif emp.currentJob then - if emp.currentJob.type == "RETURN_TO_PARKING" then + local jobType = emp.currentJob.type + if jobType == "RETURN_TO_PARKING" then statusText = g_i18n:getText("em_status_returning") + elseif jobType == "DRIVING_TO_TOOL" then + statusText = g_i18n:getText("em_status_driving_to_tool") + elseif jobType == "APPROACHING_TOOL" or jobType == "ATTACHING_TOOL" then + statusText = g_i18n:getText("em_status_attaching_tool") + elseif jobType == "RETURNING_TOOL" then + statusText = g_i18n:getText("em_status_returning_tool") else statusText = emp.currentJob.workType or g_i18n:getText("em_status_working") end @@ -276,7 +309,6 @@ function EMFieldFrame:displayAssignedEmployee(emp) self.txtEmpStatus:setText(statusText) end - -- Vehicle if self.txtEmpVehicle then local vehicleName = g_i18n:getText("em_none") if emp.assignedVehicleId and g_employeeManager then @@ -293,7 +325,6 @@ function EMFieldFrame:displayAssignedEmployee(emp) self.txtEmpVehicle:setText(vehicleName) end - -- Shift if self.txtEmpShift then self.txtEmpShift:setText(string.format("%02d:00 %s %02d:00", emp.shiftStart or 6, @@ -301,7 +332,6 @@ function EMFieldFrame:displayAssignedEmployee(emp) emp.shiftEnd or 18)) end - -- Fatigue if self.txtEmpFatigue then local fatigue = emp.fatigueLevel or 0 self.txtEmpFatigue:setText(string.format("%.0f%%", fatigue)) @@ -316,10 +346,8 @@ function EMFieldFrame:displayAssignedEmployee(emp) end end - -- Skills self:displaySkills(emp) - -- Workflow queue if self.txtEmpWorkflow then local queue = emp.taskQueue or {} if #queue > 0 then @@ -333,12 +361,18 @@ function EMFieldFrame:displayAssignedEmployee(emp) end end - -- Current task if self.txtEmpCurrentTask then if emp.currentJob then local jobType = emp.currentJob.workType or emp.currentJob.type or "Unknown" - if emp.currentJob.type == "RETURN_TO_PARKING" then + local jt = emp.currentJob.type + if jt == "RETURN_TO_PARKING" then jobType = g_i18n:getText("em_status_returning") + elseif jt == "DRIVING_TO_TOOL" then + jobType = g_i18n:getText("em_status_driving_to_tool") + elseif jt == "APPROACHING_TOOL" or jt == "ATTACHING_TOOL" then + jobType = g_i18n:getText("em_status_attaching_tool") + elseif jt == "RETURNING_TOOL" then + jobType = g_i18n:getText("em_status_returning_tool") end local fieldId = emp.currentJob.fieldId if fieldId then @@ -406,3 +440,11 @@ end function EMFieldFrame:getMenuButtonInfo() return self.menuButtonInfo end + +function EMFieldFrame:onTargetCropChanged(state) + local index = self.fieldList:getSelectedIndex() + local fieldData = self.fields[index] + if fieldData and self.cropNames[state] then + g_employeeManager:setFieldTargetCrop(fieldData.fieldId, self.cropNames[state]) + end +end diff --git a/scripts/gui/EMGui.lua b/scripts/gui/EMGui.lua index fd04ebf..9983448 100644 --- a/scripts/gui/EMGui.lua +++ b/scripts/gui/EMGui.lua @@ -28,19 +28,31 @@ end function EMGui:setupPages(gui) local pages = { - { gui.pageEmployees, "images/EMEmployeeIcon.dds" }, - { gui.pageWorkflows, "images/EMWorkflowIcon.dds" }, - { gui.pageFields, "images/EMFieldIcon.dds" }, - { gui.pageVehicles, "images/EMVehicleIcon.dds" }, + gui.pageEmployees, + gui.pageWorkflows, + gui.pageFields, + gui.pageVehicles, } - local uvs = GuiUtils.getUVs({0, 0, 1024, 1024}) + for idx, page in ipairs(pages) do + local iconSliceToFile = { + EM_IconMenu = "images/MenuIcon.dds", + EM_IconEmployee = "images/EMEmployeeIcon.dds", + EM_IconWorkflow = "images/EMWorkflowIcon.dds", + EM_IconField = "images/EMFieldIcon.dds", + EM_IconVehicle = "images/EMVehicleIcon.dds", + } + + local iconPath = "images/MenuIcon.dds" + local uvs = {0, 0, 1024, 1024} + + if page.MENU_ICON_SLICE_ID ~= nil and iconSliceToFile[page.MENU_ICON_SLICE_ID] ~= nil then + iconPath = iconSliceToFile[page.MENU_ICON_SLICE_ID] + end - for idx, thisPage in ipairs(pages) do - local page, iconPath = unpack(thisPage) local fullPath = g_modDirectory .. iconPath gui:registerPage(page, idx) - gui:addPageTab(page, fullPath, uvs) + gui:addPageTab(page, fullPath, GuiUtils.getUVs(uvs)) end gui:rebuildTabList() diff --git a/scripts/gui/EMVehicleFrame.lua b/scripts/gui/EMVehicleFrame.lua index 22eb463..dfc189a 100644 --- a/scripts/gui/EMVehicleFrame.lua +++ b/scripts/gui/EMVehicleFrame.lua @@ -9,6 +9,8 @@ function EMVehicleFrame:new() return self end +EMVehicleFrame.MENU_ICON_SLICE_ID = 'EM_IconVehicle' + function EMVehicleFrame:copyAttributes(src) EMVehicleFrame:superClass().copyAttributes(self, src) end diff --git a/scripts/gui/EMWorkflowFrame.lua b/scripts/gui/EMWorkflowFrame.lua index 28e2988..e057a21 100644 --- a/scripts/gui/EMWorkflowFrame.lua +++ b/scripts/gui/EMWorkflowFrame.lua @@ -20,6 +20,8 @@ EMWorkflowFrame.TASK_REQUIREMENTS = { MULCH_LEAVES = { skill = "driving", level = 1 }, } +EMWorkflowFrame.MENU_ICON_SLICE_ID = 'EM_IconWorkflow' + function EMWorkflowFrame:new() local self = TabbedMenuFrameElement.new(nil, EMWorkflowFrame_mt) @@ -52,6 +54,12 @@ function EMWorkflowFrame:initialize() text = g_i18n:getText("em_btn_save_start"), callback = function() self:onSaveAndStart() end, } + self.stopButtonInfo = { + profile = "buttonActivate", + inputAction = InputAction.MENU_EXTRA_2, + text = g_i18n:getText("em_btn_stop"), + callback = function() self:onStop() end, + } local hourTexts = {} for h = 0, 23 do @@ -172,7 +180,6 @@ function EMWorkflowFrame:populateCellForItemInSection(list, section, index, cell local avatarEl = cell:getAttribute("avatar") local iconEl = cell:getAttribute("icon") - -- Show avatar, hide atlas icon if avatarEl then avatarEl:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") avatarEl:setVisible(true) @@ -485,14 +492,26 @@ function EMWorkflowFrame:onSave() local employee = self:getSelectedEmployee() if not employee then return end + local hasFullConfig = employee.targetFieldId and employee.assignedVehicleId and #(employee.taskQueue or {}) > 0 + + if hasFullConfig then + employee.isAutonomous = true + employee.currentTaskIndex = employee.currentTaskIndex or 1 + end + if self.txtStatusMessage then - self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_saved"), employee.name)) + if hasFullConfig then + self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_saved_hint"), employee.name)) + else + self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_saved"), employee.name)) + end end - CustomUtils:info("[EMWorkflowFrame] Saved workflow for %s: %d tasks, field=%s, vehicle=%s, shift=%d-%d", + CustomUtils:info("[EMWorkflowFrame] Saved workflow for %s: %d tasks, field=%s, vehicle=%s, shift=%d-%d, autonomous=%s", employee.name, #(employee.taskQueue or {}), tostring(employee.targetFieldId), tostring(employee.assignedVehicleId), - employee.shiftStart or 6, employee.shiftEnd or 18 + employee.shiftStart or 6, employee.shiftEnd or 18, tostring(employee.isAutonomous) ) + self:updateMenuButtons() end function EMWorkflowFrame:onSaveAndStart() @@ -522,20 +541,54 @@ function EMWorkflowFrame:onSaveAndStart() self:onSave() local firstTask = queue[1] + employee.currentTaskIndex = 1 employee.isAutonomous = true - if g_employeeManager.jobManager:startFieldWork(employee, employee.targetFieldId, firstTask) then - if self.txtStatusMessage then - self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_started"), - employee.name, employee.targetFieldId, firstTask)) + local currentHour = 0 + if g_currentMission and g_currentMission.environment then + currentHour = g_currentMission.environment.currentHour or 0 + end + + if employee:isWithinShift(currentHour) then + if g_employeeManager.jobManager:startFieldWork(employee, employee.targetFieldId, firstTask) then + if self.txtStatusMessage then + self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_started"), + employee.name, employee.targetFieldId, firstTask)) + end + else + if self.txtStatusMessage then + self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_start_failed"), employee.name)) + end end else if self.txtStatusMessage then - self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_start_failed"), employee.name)) + self.txtStatusMessage:setText(string.format( + g_i18n:getText("em_workflow_scheduled"), employee.name, employee.shiftStart or 6)) end + CustomUtils:info("[EMWorkflowFrame] %s scheduled: outside shift hours (%d:00, shift starts at %d:00)", + employee.name, currentHour, employee.shiftStart or 6) end end +function EMWorkflowFrame:onStop() + local employee = self:getSelectedEmployee() + if not employee then return end + + employee.isAutonomous = false + + if employee.currentJob then + if g_employeeManager and g_employeeManager.jobManager then + g_employeeManager.jobManager:stopJob(employee) + end + end + + if self.txtStatusMessage then + self.txtStatusMessage:setText(string.format(g_i18n:getText("em_workflow_stopped"), employee.name)) + end + CustomUtils:info("[EMWorkflowFrame] Stopped autonomous mode for %s", employee.name) + self:updateMenuButtons() +end + function EMWorkflowFrame:buildOwnedFieldsList() local fields = {} local farmId = g_currentMission:getFarmId() @@ -618,12 +671,17 @@ function EMWorkflowFrame:buildOwnedVehiclesList() end function EMWorkflowFrame:updateMenuButtons() - local hasSelection = self:getSelectedEmployee() ~= nil + local employee = self:getSelectedEmployee() + local hasSelection = employee ~= nil self.menuButtonInfo = { self.backButtonInfo } if hasSelection then table.insert(self.menuButtonInfo, self.saveButtonInfo) - table.insert(self.menuButtonInfo, self.saveStartButtonInfo) + if employee.isAutonomous then + table.insert(self.menuButtonInfo, self.stopButtonInfo) + else + table.insert(self.menuButtonInfo, self.saveStartButtonInfo) + end end self:setMenuButtonInfoDirty() diff --git a/scripts/gui/MenuEmployeeManager.lua b/scripts/gui/MenuEmployeeManager.lua index 2a4d954..ec29ee1 100644 --- a/scripts/gui/MenuEmployeeManager.lua +++ b/scripts/gui/MenuEmployeeManager.lua @@ -4,7 +4,7 @@ MenuEmployeeManager.CLASS_NAME = 'MenuEmployeeManager' MenuEmployeeManager.MENU_PAGE_NAME = 'menuEmployeeManager' MenuEmployeeManager.XML_FILENAME = g_modDirectory .. 'xml/gui/MenuEmployeeManager.xml' -MenuEmployeeManager.MENU_ICON_SLICE_ID = 'MenuEmployeeManager.menuIcon' +MenuEmployeeManager.MENU_ICON_SLICE_ID = 'EM_IconMenu' MenuEmployeeManager._mt = Class(MenuEmployeeManager, TabbedMenuFrameElement) @@ -115,7 +115,6 @@ function MenuEmployeeManager:updateContent() local hasItem = self.leftListTable:getItemCount() > 0 - -- Toggle visibility of containers if self.employeesContainer then self.employeesContainer:setVisible(hasItem) end @@ -169,29 +168,23 @@ function MenuEmployeeManager:displayEmployeeDetails(employee) self.personalPanelContainer:setVisible(true) if self.columnSeparator then self.columnSeparator:setVisible(true) end - -- Avatar if self.detailAvatar ~= nil then self.detailAvatar:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") end - -- Identity self.employeeName:setText(employee.name) self.employeeId:setText(string.format("ID: %d", employee.id)) - -- Trait (subtitle under name) if self.employeeTrait ~= nil then local traitName = employee.getTraitName and employee:getTraitName() or nil self.employeeTrait:setText(traitName or g_i18n:getText("em_none")) end - -- Status local statusKey = employee.isHired and "em_status_hired" or "em_status_available" self.employeeStatusValue:setText(g_i18n:getText(statusKey)) - -- Skills with progress bars self:displaySkills(employee) - -- Work stats (conditionally visible) local isHired = employee.isHired if self.workStatsSection then self.workStatsSection:setVisible(isHired) @@ -200,17 +193,14 @@ function MenuEmployeeManager:displayEmployeeDetails(employee) self:displayWorkStats(employee) end - -- Traits list if self.txtTraitsList then local traitName = employee.getTraitName and employee:getTraitName() or nil self.txtTraitsList:setText(traitName or g_i18n:getText("em_none")) end - -- Wage local wage = employee.getDailyWage and employee:getDailyWage() or 0 self.employeeWageValue:setText(g_i18n:formatMoney(wage, 0, true, false)) - -- Personal info (right column) self:displayPersonalInfo(employee) end @@ -275,7 +265,6 @@ function MenuEmployeeManager:displayWorkStats(employee) end end - -- Assigned field if self.txtAssignedField then if employee.targetFieldId then self.txtAssignedField:setText(string.format("Field %d", employee.targetFieldId)) @@ -284,7 +273,6 @@ function MenuEmployeeManager:displayWorkStats(employee) end end - -- Fatigue if self.statFatigue then local fatigue = employee.fatigueLevel or 0 if employee.isOnBreak then @@ -300,13 +288,11 @@ function MenuEmployeeManager:displayWorkStats(employee) end function MenuEmployeeManager:displayPersonalInfo(employee) - -- Age if self.txtPersonalAge then local age = employee.age or 30 self.txtPersonalAge:setText(tostring(age)) end - -- Nationality if self.txtPersonalNationality then local natKey = "em_nationality_" .. (employee.nationality or "FR") local natText = g_i18n:getText(natKey) @@ -316,7 +302,6 @@ function MenuEmployeeManager:displayPersonalInfo(employee) self.txtPersonalNationality:setText(natText) end - -- Biography if self.txtPersonalBio then local bioKey = employee.bioKey or "em_bio_default" local bioText = g_i18n:getText(bioKey) @@ -324,7 +309,6 @@ function MenuEmployeeManager:displayPersonalInfo(employee) self.txtPersonalBio:setText(bioText) end - -- Quote if self.txtPersonalQuote then local quoteKey = employee.quoteKey or "em_quote_default" local quoteText = g_i18n:getText(quoteKey) diff --git a/scripts/gui/SimpleStatusHUD.lua b/scripts/gui/SimpleStatusHUD.lua index a2a0791..3074032 100644 --- a/scripts/gui/SimpleStatusHUD.lua +++ b/scripts/gui/SimpleStatusHUD.lua @@ -64,9 +64,21 @@ function SimpleStatusHUD:draw() color = {1, 1, 0, 1} end - if emp.currentJob and emp.currentJob.type == "RETURN_TO_PARKING" then - status = g_i18n:getText("em_status_returning") - color = {0.5, 0.8, 1, 1} + if emp.currentJob then + local jobType = emp.currentJob.type + if jobType == "RETURN_TO_PARKING" then + status = g_i18n:getText("em_status_returning") + color = {0.5, 0.8, 1, 1} + elseif jobType == "DRIVING_TO_TOOL" then + status = g_i18n:getText("em_status_driving_to_tool") + color = {1, 0.8, 0.2, 1} + elseif jobType == "APPROACHING_TOOL" or jobType == "ATTACHING_TOOL" then + status = g_i18n:getText("em_status_attaching_tool") + color = {1, 0.8, 0.2, 1} + elseif jobType == "RETURNING_TOOL" then + status = g_i18n:getText("em_status_returning_tool") + color = {0.5, 0.8, 1, 1} + end end setTextColor(unpack(color)) diff --git a/textures/ui_elements.xml b/textures/ui_elements.xml index 9e4f2e1..bda2677 100644 --- a/textures/ui_elements.xml +++ b/textures/ui_elements.xml @@ -1,4 +1,8 @@ - - \ No newline at end of file + + + + + + diff --git a/xml/gui/EMEmployeeFrame.xml b/xml/gui/EMEmployeeFrame.xml index 2e6eb43..9bbaa5a 100644 --- a/xml/gui/EMEmployeeFrame.xml +++ b/xml/gui/EMEmployeeFrame.xml @@ -2,7 +2,9 @@ - + + + diff --git a/xml/gui/EMFieldFrame.xml b/xml/gui/EMFieldFrame.xml index 9689b16..6b3c896 100644 --- a/xml/gui/EMFieldFrame.xml +++ b/xml/gui/EMFieldFrame.xml @@ -2,7 +2,9 @@ - + + + @@ -41,6 +43,10 @@ + + + + diff --git a/xml/gui/EMVehicleFrame.xml b/xml/gui/EMVehicleFrame.xml index 5272d2e..a47e671 100644 --- a/xml/gui/EMVehicleFrame.xml +++ b/xml/gui/EMVehicleFrame.xml @@ -2,7 +2,9 @@ - + + + diff --git a/xml/gui/EMWorkflowFrame.xml b/xml/gui/EMWorkflowFrame.xml index 5233ffe..08b7282 100644 --- a/xml/gui/EMWorkflowFrame.xml +++ b/xml/gui/EMWorkflowFrame.xml @@ -2,7 +2,9 @@ - + + + diff --git a/xml/gui/MenuEmployeeManager.xml b/xml/gui/MenuEmployeeManager.xml index 61abd90..c56f765 100644 --- a/xml/gui/MenuEmployeeManager.xml +++ b/xml/gui/MenuEmployeeManager.xml @@ -8,7 +8,9 @@ --> - + + + diff --git a/xml/gui/guiProfiles.xml b/xml/gui/guiProfiles.xml index 50793af..c2d78e6 100644 --- a/xml/gui/guiProfiles.xml +++ b/xml/gui/guiProfiles.xml @@ -8,24 +8,39 @@ - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + From 74280c24965b0b4e7198084891d939e8aec09fea Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:14:38 +0100 Subject: [PATCH 06/10] Refactor JobManager: Enhance equipment handling and tool attachment logic - Improved equipment readiness checks and deferred job start via EQUIPMENT_READY state. - Added functionality to detach all implements from vehicles before attaching new tools. - Implemented a three-tier search for required tools: attached, owned, and rental. - Enhanced tool attachment process with direct attach attempts and fallback teleportation. - Introduced robust logging for equipment readiness and tool attachment processes. - Added rapid fail detection for fieldwork completion to prevent infinite loops. - Streamlined job handling for employees, including task queue management and tool return logic. --- scripts/managers/cropmanager.lua | 4 +- scripts/managers/employeemanager.lua | 126 +++- scripts/managers/jobmanager.lua | 845 ++++++++++++++++++++++----- scripts/types/employee.lua | 16 +- 4 files changed, 844 insertions(+), 147 deletions(-) diff --git a/scripts/managers/cropmanager.lua b/scripts/managers/cropmanager.lua index bf634a6..2102471 100644 --- a/scripts/managers/cropmanager.lua +++ b/scripts/managers/cropmanager.lua @@ -55,8 +55,10 @@ function CropManager:getNextStep(field, targetCropName) CustomUtils:debug("[CropManager] Field %d Analysis for %s:", field.fieldId, targetCropName) CustomUtils:debug(" - Fruit: %d (Target: %d)", state.fruitTypeIndex, self:getFruitTypeIndex(targetCropName)) CustomUtils:debug(" - Growth: %d", state.growthState) + CustomUtils:debug(" - GroundType: %d (%s)", state.groundType, FieldGroundType.getName(state.groundType) or "UNKNOWN") + CustomUtils:debug(" - SprayType: %d | SprayLevel: %d", state.sprayType or 0, state.sprayLevel) CustomUtils:debug(" - Plow: %d | Lime: %d | Stones: %d", state.plowLevel, state.limeLevel, state.stoneLevel) - CustomUtils:debug(" - Stubble: %d | Weed: %d | Spray: %d", state.stubbleShredLevel, state.weedState, state.sprayLevel) + CustomUtils:debug(" - Stubble: %d | Weed: %d | Roller: %d", state.stubbleShredLevel, state.weedState, state.rollerLevel) local targetFruitIndex = self:getFruitTypeIndex(targetCropName) diff --git a/scripts/managers/employeemanager.lua b/scripts/managers/employeemanager.lua index eb767b5..67f00bb 100644 --- a/scripts/managers/employeemanager.lua +++ b/scripts/managers/employeemanager.lua @@ -85,7 +85,8 @@ function EmployeeManager:update(dt) CustomUtils:info("[EmployeeManager] %s stopped working (unpaid)", employee.name) end elseif employee.isHired and employee.currentJob ~= nil then - local hoursWorked = employee:updateWorkTime(dt) + local effectiveDt = math.min(dt, 200) + local hoursWorked = employee:updateWorkTime(effectiveDt) if hoursWorked > 0 then local fatigueMult = employee:getFatigueMultiplier() local wage = employee:getHourlyWage() * marketMult * hoursWorked @@ -133,6 +134,54 @@ function EmployeeManager:update(dt) end end end + + if employee.isHired and not employee.isUnpaid and employee.isAutonomous + and employee.currentJob == nil + and (employee.targetCrop == nil or employee.targetCrop == "") + and employee.taskQueue and #employee.taskQueue > 0 + and employee.targetFieldId ~= nil then + + if not employee:canWork() then + -- skip: on break or exhausted + elseif g_currentMission and g_currentMission.environment and not employee:isWithinShift(g_currentMission.environment.currentHour or 0) then + -- skip: outside shift + else + employee.decisionTimer = (employee.decisionTimer or 0) + dt + if employee.decisionTimer > 5000 then + employee.decisionTimer = 0 + local idx = employee.currentTaskIndex or 1 + if idx <= #employee.taskQueue then + local task = employee.taskQueue[idx] + CustomUtils:info("[EmployeeManager] %s resuming task %d/%d: %s", + employee.name, idx, #employee.taskQueue, task) + local success = self.jobManager:startFieldWork(employee, employee.targetFieldId, task) + if not success then + CustomUtils:warning("[EmployeeManager] %s auto-start FAILED for task %s on field %d", + employee.name, task, employee.targetFieldId) + end + end + end + end + end + + if employee.isHired and employee.isAutonomous and employee.currentJob == nil + and employee.taskQueue and #employee.taskQueue > 0 then + employee.diagTimer = (employee.diagTimer or 0) + dt + + if employee.diagTimer > 60000 then + employee.diagTimer = 0 + local hour = (g_currentMission and g_currentMission.environment) and g_currentMission.environment.currentHour or 0 + local reasons = {} + if employee.isUnpaid then table.insert(reasons, "UNPAID") end + if not employee:canWork() then table.insert(reasons, "CANNOT_WORK (break/exhausted)") end + if not employee:isWithinShift(hour) then table.insert(reasons, string.format("OUTSIDE_SHIFT (%d:00, shift %d-%d)", hour, employee.shiftStart or 6, employee.shiftEnd or 18)) end + if not employee.targetFieldId then table.insert(reasons, "NO_FIELD") end + if employee.targetCrop and employee.targetCrop ~= "" then table.insert(reasons, "HAS_TARGET_CROP (using crop-based path)") end + if #reasons > 0 then + CustomUtils:debug("[EmployeeManager] %s idle but autonomous: %s", employee.name, table.concat(reasons, ", ")) + end + end + end end self:checkPoolRefresh() @@ -238,16 +287,18 @@ function EmployeeManager:onHourChanged() local currentHour = g_currentMission.environment.currentHour or 0 for _, employee in ipairs(self.employees) do - -- if not employee.isHired then if employee.isOnBreak then if employee.breakEndTime ~= nil and g_currentMission.time >= employee.breakEndTime then employee.isOnBreak = false employee.breakEndTime = nil CustomUtils:info("[EmployeeManager] %s break is over, ready to work", employee.name) end + elseif employee.currentJob == nil and employee.isAutonomous and currentHour == (employee.shiftStart or 6) then + CustomUtils:info("[EmployeeManager] Shift start: %s is autonomous and idle at %d:00 (shift %d-%d), update() will auto-start", + employee.name, currentHour, employee.shiftStart or 6, employee.shiftEnd or 18) elseif employee.currentJob ~= nil then local jobType = employee.currentJob.type - if jobType ~= "TRANSIT" and jobType ~= "RETURN_TO_PARKING" and jobType ~= "PREPARING" then + if jobType ~= "TRANSIT" and jobType ~= "RETURN_TO_PARKING" and jobType ~= "PREPARING" and jobType ~= "EQUIPMENT_READY" then if not employee:isWithinShift(currentHour) then self.jobManager:stopJob(employee) CustomUtils:info("[EmployeeManager] %s stopped: outside shift hours (%d:00, shift %d-%d)", @@ -268,6 +319,34 @@ function EmployeeManager:onHourChanged() end end end + + if employee.isHired then + local jobDesc = "NONE" + if employee.currentJob then + jobDesc = string.format("%s (field %s)", employee.currentJob.type or "?", tostring(employee.currentJob.fieldId or "?")) + end + local queueDesc = "empty" + if employee.taskQueue and #employee.taskQueue > 0 then + queueDesc = string.format("%d tasks, idx=%d", #employee.taskQueue, employee.currentTaskIndex or 1) + end + CustomUtils:info( + "[EmployeeManager] HOURLY[%d:00] %s | autonomous=%s | job=%s | field=%s | vehicle=%s | crop=%s | queue=%s | shift=%d-%d | inShift=%s | canWork=%s | unpaid=%s | hoursWorked=%.1f", + currentHour, + employee.name, + tostring(employee.isAutonomous), + jobDesc, + tostring(employee.targetFieldId), + tostring(employee.assignedVehicleId), + tostring(employee.targetCrop), + queueDesc, + employee.shiftStart or 6, + employee.shiftEnd or 18, + tostring(employee:isWithinShift(currentHour)), + tostring(employee:canWork()), + tostring(employee.isUnpaid), + employee.dailyHoursWorked or 0 + ) + end end end @@ -439,16 +518,25 @@ function EmployeeManager:returnRentedEquipment(employee) local tool = self:getVehicleById(toolId) if tool then - CustomUtils:info("[EmployeeManager] Returning rented equipment %s for employee %s", tool:getName(), employee.name) - + CustomUtils:info("[EmployeeManager] Returning rented equipment %s (ID: %d) for employee %s", tool:getName(), toolId, employee.name) + local vehicle = self:getVehicleById(employee.assignedVehicleId) if vehicle and vehicle.detachImplementByObject then vehicle:detachImplementByObject(tool) end - - tool:delete() + + if g_currentMission.removeSaleableItem then + pcall(g_currentMission.removeSaleableItem, g_currentMission, tool) + end + + local ok, err = pcall(tool.delete, tool) + if not ok then + CustomUtils:warning("[EmployeeManager] Error deleting rented tool ID %d: %s", toolId, tostring(err)) + end + else + CustomUtils:warning("[EmployeeManager] Rented tool ID %d not found (already deleted?), clearing reference for %s", toolId, employee.name) end - + employee.temporaryRental = nil employee.isRenting = false end @@ -661,6 +749,19 @@ function EmployeeManager:setFieldConfig(fieldId, cropName, assignments) CustomUtils:info("[EmployeeManager] Configured workflow for Field %d: %s", fieldId, cropName) end +function EmployeeManager:setFieldTargetCrop(fieldId, cropName) + if not self.fieldConfigs[fieldId] then + self.fieldConfigs[fieldId] = {} + end + self.fieldConfigs[fieldId].cropName = cropName + CustomUtils:info("[EmployeeManager] Set target crop for Field %d to %s", fieldId, cropName) +end + +function EmployeeManager:getFieldTargetCrop(fieldId) + local config = self.fieldConfigs[fieldId] + return config and config.cropName or nil +end + function EmployeeManager:getAssignedEmployeeForStep(fieldId, stepName) local config = self.fieldConfigs[fieldId] if config and config.assignments then @@ -867,9 +968,10 @@ function EmployeeManager:saveToXMLFile(xmlFile, key) if e.targetCrop ~= nil then setXMLString(xmlFile, base .. "#targetCrop", e.targetCrop) - setXMLInt(xmlFile, base .. "#targetFieldId", e.targetFieldId or 0) - setXMLBool(xmlFile, base .. "#isAutonomous", e.isAutonomous or false) end + setXMLInt(xmlFile, base .. "#targetFieldId", e.targetFieldId or 0) + setXMLBool(xmlFile, base .. "#isAutonomous", e.isAutonomous or false) + setXMLInt(xmlFile, base .. "#currentTaskIndex", e.currentTaskIndex or 1) setXMLInt(xmlFile, base .. "#shiftStart", e.shiftStart or 6) setXMLInt(xmlFile, base .. "#shiftEnd", e.shiftEnd or 18) @@ -1003,18 +1105,18 @@ function EmployeeManager:loadFromXMLFile(xmlFile, key) table.insert(emp.taskQueue, taskName) qi = qi + 1 end + emp.currentTaskIndex = Utils.getNoNil(getXMLInt(xmlFile, base .. "#currentTaskIndex"), 1) table.insert(self.employees, emp) i = i + 1 end - -- Compute max ID from loaded employees as safety fallback local maxId = 0 for _, emp in ipairs(self.employees) do if emp.id > maxId then maxId = emp.id end end self.nextEmployeeId = Utils.getNoNil(getXMLInt(xmlFile, key .. ".poolState#nextEmployeeId"), maxId + 1) - -- Ensure nextEmployeeId is always above any loaded ID (guards against stale save data) + if self.nextEmployeeId <= maxId then self.nextEmployeeId = maxId + 1 end diff --git a/scripts/managers/jobmanager.lua b/scripts/managers/jobmanager.lua index 6b4595d..bbb39dc 100644 --- a/scripts/managers/jobmanager.lua +++ b/scripts/managers/jobmanager.lua @@ -100,64 +100,25 @@ function JobManager:startFieldWork(employee, fieldId, workType) vehicle:stopAIJob() end - self:ensureEquipment(vehicle, workType, function(success) - if not success then - CustomUtils:error("[JobManager] Could not ensure equipment for %s - Job Aborted", workType) + self:ensureEquipment(vehicle, workType, function(result, data) + if result == true then + CustomUtils:info("[JobManager] Equipment ensured. Deferring start via EQUIPMENT_READY state...") + employee.currentJob = { + type = "EQUIPMENT_READY", + fieldId = fieldId, + workType = workType, + readyFrame = 0, + readyTime = g_currentMission.time + 1000 + } + elseif result == "DRIVE_TO_TOOL" then + CustomUtils:info("[JobManager] Owned tool found. Starting drive-to-tool for %s...", employee.name) + self:startDriveToTool(employee, vehicle, data.tool, fieldId, workType) + else + local msg = string.format(g_i18n:getText("em_error_equipment_failed"), employee.name, workType) + CustomUtils:error("[JobManager] " .. msg) + g_currentMission:showBlinkingWarning(msg, 5000) employee.currentJob = nil - return - end - - CustomUtils:debug("[JobManager] Equipment ensured. Check distance...") - - local x, z = field:getCenterOfFieldWorldPosition() - local vx, _, vz = getWorldTranslation(vehicle.rootNode) - local distance = MathUtil.vector2Length(vx - x, vz - z) - - CustomUtils:info("[JobManager] Distance to Field %d: %.1f m", fieldId, distance) - - if distance > 150 then - CustomUtils:info("[JobManager] Field is far. Starting TRANSIT (GOTO) job first.") - - local aiJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.GOTO) - if aiJob then - local farmId = g_currentMission:getFarmId() - aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) - aiJob.positionAngleParameter:setPosition(x, z) - - local dx, dz = x - vx, z - vz - local angle = MathUtil.getYRotationFromDirection(dx, dz) - aiJob.positionAngleParameter:setAngle(angle) - - aiJob:setValues() - - local validateSuccess, errorMessage = aiJob:validate(farmId) - if validateSuccess then - g_currentMission.aiSystem:startJob(aiJob, farmId) - - employee.currentJob = { - aiJobId = aiJob.jobId, - type = "TRANSIT", - fieldId = fieldId, - workType = workType, - startTime = g_currentMission.time - } - - employee.pendingJob = { - fieldId = fieldId, - workType = workType - } - CustomUtils:info("[JobManager] Employee %s is now in TRANSIT to field %d", employee.name, fieldId) - return - else - CustomUtils:error("[JobManager] Transit GOTO job failed validation: %s", errorMessage) - end - else - CustomUtils:error("[JobManager] Failed to create GOTO job") - end end - - CustomUtils:debug("[JobManager] Starting FIELDWORK immediately (Direct or Close Proximity).") - self:startFieldWorkJob(employee, vehicle, fieldId, workType) end) return true @@ -176,19 +137,32 @@ function JobManager:startFieldWorkJob(employee, vehicle, fieldId, workType) local farmId = g_currentMission:getFarmId() local x, z = field:getCenterOfFieldWorldPosition() - local vx, _, vz = getWorldTranslation(vehicle.rootNode) - local distance = MathUtil.vector2Length(vx - x, vz - z) - aiJob.isDirectStart = distance < 50 - aiJob:applyCurrentState(vehicle, g_currentMission, farmId, aiJob.isDirectStart) + if field.fieldState == nil then + field.fieldState = FieldState.new() + end + field.fieldState:update(x, z) + local fs = field.fieldState - if not aiJob.isDirectStart then - aiJob.positionAngleParameter:setPosition(x, z) - else - local dirX, _, dirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1) - local angle = MathUtil.getYRotationFromDirection(dirX, dirZ) - aiJob.positionAngleParameter:setAngle(angle) + local fruitName = "NONE" + if fs.fruitTypeIndex ~= FruitType.UNKNOWN then + local ft = g_fruitTypeManager:getFruitTypeByIndex(fs.fruitTypeIndex) + fruitName = ft and ft.name or "UNKNOWN" end + local groundName = FieldGroundType.getName(fs.groundType) or "UNKNOWN" + + CustomUtils:info("[JobManager] Field %d state before %s:", fieldId, workType) + CustomUtils:info(" Fruit: %s (%d) | Growth: %d | Ground: %s (%d)", + fruitName, fs.fruitTypeIndex, fs.growthState, groundName, fs.groundType) + CustomUtils:info(" Plow: %d | Lime: %d | Stones: %d | Spray: %d | SprayType: %d", + fs.plowLevel, fs.limeLevel, fs.stoneLevel, fs.sprayLevel, fs.sprayType or 0) + CustomUtils:info(" Stubble: %d | Weed: %d | Roller: %d", + fs.stubbleShredLevel, fs.weedState, fs.rollerLevel) + + aiJob.isDirectStart = false + + aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) + aiJob.positionAngleParameter:setPosition(x, z) aiJob:setValues() @@ -219,8 +193,32 @@ function JobManager:startFieldWorkJob(employee, vehicle, fieldId, workType) end end ----Checks if vehicle has required tool, otherwise rents one +---Detaches all implements from a vehicle +---@param vehicle table +function JobManager:detachAllImplements(vehicle) + if not vehicle or not vehicle.getAttachedImplements then return end + + local attachedImplements = vehicle:getAttachedImplements() + for i = #attachedImplements, 1, -1 do + local implement = attachedImplements[i] + if implement and implement.object then + CustomUtils:info("[JobManager] Detaching %s from %s", implement.object:getName(), vehicle:getName()) + if vehicle.detachImplementByObject then + vehicle:detachImplementByObject(implement.object) + end + end + end +end + +---Checks if vehicle has required tool via three-tier search: attached → owned → rental +---Callback receives (true) for ready, ("DRIVE_TO_TOOL", data) for owned tool needing drive, or (false) for failure function JobManager:ensureEquipment(vehicle, workType, callback) + local employee = g_employeeManager:getEmployeeByVehicle(vehicle) + if employee and employee.temporaryRental then + CustomUtils:warning("[JobManager] ensureEquipment: cleaning up lingering rental for %s", employee.name) + g_employeeManager:returnRentedEquipment(employee) + end + local categoryName = JobManager.WORK_TYPE_TO_CATEGORY[workType] if not categoryName then callback(true) @@ -228,33 +226,43 @@ function JobManager:ensureEquipment(vehicle, workType, callback) end local attachedImplements = vehicle:getAttachedImplements() - local hasAttachedTool = #attachedImplements > 0 for _, implement in ipairs(attachedImplements) do local obj = implement.object - if obj ~= nil and obj.getIsAIJobSupported ~= nil and obj:getIsAIJobSupported("AIJobFieldWork") then - callback(true) - return + if obj ~= nil then + local storeItem = g_storeManager:getItemByXMLFilename(obj.configFileName) + if storeItem and storeItem.categoryName == categoryName then + CustomUtils:info("[JobManager] Tier 1: Vehicle already has correct tool (%s) for %s", obj:getName(), workType) + callback(true) + return + end end end - if hasAttachedTool then - CustomUtils:warning("[JobManager] Vehicle already has an implement attached but it doesn't support %s. Cannot attach another.", workType) - callback(false) - return + if #attachedImplements > 0 then + CustomUtils:info("[JobManager] Detaching existing implements before attaching tool for %s", workType) + self:detachAllImplements(vehicle) end - local parkedTool = self:findToolInParking(categoryName, vehicle) - if parkedTool then - CustomUtils:info("[JobManager] Found owned tool %s in parking, attaching...", parkedTool:getName()) - if vehicle.attachImplement then - vehicle:attachImplement(parkedTool, 1, 1) + local ownedTool = self:findOwnedTool(categoryName, vehicle) + if ownedTool then + local tx, _, tz = getWorldTranslation(ownedTool.rootNode) + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local dist = MathUtil.vector2Length(vx - tx, vz - tz) + + if dist < 10 then + CustomUtils:info("[JobManager] Tier 2: Owned tool %s is nearby (%.1fm). Attempting native attach...", + ownedTool:getName(), dist) + self:attemptNativeAttach(vehicle, ownedTool, callback) + else + CustomUtils:info("[JobManager] Tier 2: Found owned tool %s at %.1fm away. Employee must drive to it.", + ownedTool:getName(), dist) + callback("DRIVE_TO_TOOL", { tool = ownedTool, toolX = tx, toolZ = tz }) end - callback(true) return end - CustomUtils:info("[JobManager] No tool found for %s. Renting equipment...", workType) + CustomUtils:info("[JobManager] Tier 3: No owned tool found for %s. Renting equipment...", workType) local storeItem = self:findSuitableTool(categoryName) if storeItem then self:rentAndAttach(vehicle, storeItem, callback) @@ -264,6 +272,442 @@ function JobManager:ensureEquipment(vehicle, workType, callback) end end +---Searches ALL farm vehicles for an unattached, unused tool matching the category +---@param categoryName string +---@param vehicle table The tractor that needs the tool +---@return table|nil The found tool vehicle, or nil +function JobManager:findOwnedTool(categoryName, vehicle) + if not g_currentMission or not g_currentMission.vehicleSystem then return nil end + + local farmId = g_currentMission:getFarmId() + local vehicles = g_currentMission.vehicleSystem.vehicles + + for _, v in ipairs(vehicles) do + if v ~= vehicle and v.ownerFarmId == farmId then + local storeItem = g_storeManager:getItemByXMLFilename(v.configFileName) + if storeItem and storeItem.categoryName == categoryName then + if not self:isAttachedToAnyVehicle(v) and not self:isToolInUseByEmployee(v.id) then + CustomUtils:debug("[JobManager] findOwnedTool: Match found — %s (ID: %d)", v:getName(), v.id) + return v + end + end + end + end + + return nil +end + +---Checks if a tool is currently attached to any vehicle +---@param tool table +---@return boolean +function JobManager:isAttachedToAnyVehicle(tool) + if tool.getAttacherVehicle and tool:getAttacherVehicle() ~= nil then + return true + end + return false +end + +---Checks if a tool is currently rented/in-use by another employee +---@param toolId number +---@return boolean +function JobManager:isToolInUseByEmployee(toolId) + if not g_employeeManager then return false end + + for _, employee in ipairs(g_employeeManager.employees) do + if employee.isHired and employee.temporaryRental == toolId then + return true + end + end + return false +end + +---Teleports an owned tool to align with the vehicle's rear attacher joint +---Positions the tool so its input attacher joint meets the vehicle's rear attacher joint +---@param tool table +---@param vehicle table +function JobManager:positionToolNearVehicle(tool, vehicle) + local vx, vy, vz = getWorldTranslation(vehicle.rootNode) + + local rearJointX, rearJointY, rearJointZ = vx, vy, vz + if vehicle.spec_attacherJoints then + local joints = vehicle:getAttacherJoints() + if joints then + for _, joint in ipairs(joints) do + if joint.jointTransform then + local _, _, lz = localToLocal(joint.jointTransform, vehicle.rootNode, 0, 0, 0) + if lz < -0.2 then + rearJointX, rearJointY, rearJointZ = getWorldTranslation(joint.jointTransform) + break + end + end + end + end + end + + local toolJointOffsetX, toolJointOffsetZ = 0, 0 + local inputJoints = tool.getInputAttacherJoints and tool:getInputAttacherJoints() + if inputJoints and #inputJoints > 0 then + local joint = inputJoints[1] + if joint.node then + local lx, _, lz = localToLocal(joint.node, tool.rootNode, 0, 0, 0) + toolJointOffsetX, toolJointOffsetZ = lx, lz + end + end + + local backDirX, _, backDirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, -1) + local backRightX, _, backRightZ = localDirectionToWorld(vehicle.rootNode, 1, 0, 0) + + local targetX = rearJointX - backRightX * toolJointOffsetX - backDirX * toolJointOffsetZ + local targetY = rearJointY + 0.3 + local targetZ = rearJointZ - backRightZ * toolJointOffsetX - backDirZ * toolJointOffsetZ + + local vDirX, _, vDirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1) + local vehicleRotY = MathUtil.getYRotationFromDirection(vDirX, vDirZ) + + if tool.setAbsolutePosition then + tool:setAbsolutePosition(targetX, targetY, targetZ, 0, vehicleRotY, 0) + CustomUtils:debug("[JobManager] Positioned %s at (%.1f, %.1f, %.1f) rot=%.1f° via setAbsolutePosition", + tool:getName(), targetX, targetY, targetZ, math.deg(vehicleRotY)) + elseif tool.rootNode then + setWorldTranslation(tool.rootNode, targetX, targetY, targetZ) + setWorldRotation(tool.rootNode, 0, vehicleRotY, 0) + CustomUtils:debug("[JobManager] Positioned %s at (%.1f, %.1f, %.1f) rot=%.1f° via setWorldTranslation", + tool:getName(), targetX, targetY, targetZ, math.deg(vehicleRotY)) + else + CustomUtils:warning("[JobManager] Cannot position tool %s — no positioning method available", tool:getName()) + end +end + +---Attempts native FS25 attach when tool is close to the vehicle +---@param vehicle table +---@param tool table +---@param callback function +function JobManager:attemptNativeAttach(vehicle, tool, callback) + if vehicle.spec_attacherJoints then + AttacherJoints.updateVehiclesInAttachRange(vehicle, + AttacherJoints.MAX_ATTACH_DISTANCE_SQ or 100, + AttacherJoints.MAX_ATTACH_ANGLE or 1.0, true) + + local info = vehicle.spec_attacherJoints.attachableInfo + if info and info.attachable == tool then + CustomUtils:info("[JobManager] Native attach: Tool %s detected in range. Attaching via base game...", tool:getName()) + vehicle:attachImplementFromInfo(info) + callback(true) + return + end + end + + CustomUtils:info("[JobManager] Native attach scan missed. Falling back to position + attachImplement for %s", tool:getName()) + self:positionToolNearVehicle(tool, vehicle) + + local vJointIdx, tJointIdx = self:findCompatibleJoints(vehicle, tool) + if vehicle.attachImplement then + vehicle:attachImplement(tool, tJointIdx, vJointIdx) + end + callback(true) +end + +---Calculates optimal approach position for a vehicle to reach a tool for attaching +---Returns a point roughly 8m in front of the tool's input attacher joint direction +---@param vehicle table +---@param tool table +---@return number approachX, number approachZ, number approachAngle +function JobManager:calculateApproachPosition(vehicle, tool) + local tx, _, tz = getWorldTranslation(tool.rootNode) + + local inputJoints = tool.getInputAttacherJoints and tool:getInputAttacherJoints() + if inputJoints and #inputJoints > 0 then + local joint = inputJoints[1] + if joint.node then + local jdx, _, jdz = localDirectionToWorld(joint.node, 0, 0, 1) + local jointLen = math.sqrt(jdx * jdx + jdz * jdz) + if jointLen > 0.001 then + jdx = jdx / jointLen + jdz = jdz / jointLen + + local approachX = tx + jdx * 8 + local approachZ = tz + jdz * 8 + + local approachAngle = MathUtil.getYRotationFromDirection(-jdx, -jdz) + return approachX, approachZ, approachAngle + end + end + end + + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local dx = tx - vx + local dz = tz - vz + local dist = math.sqrt(dx * dx + dz * dz) + if dist > 0.001 then + dx = dx / dist + dz = dz / dist + else + dx, dz = 0, 1 + end + + local approachX = tx - dx * 8 + local approachZ = tz - dz * 8 + local approachAngle = MathUtil.getYRotationFromDirection(dx, dz) + return approachX, approachZ, approachAngle +end + +---Starts a GOTO AI job to drive the vehicle to a tool for attachment +---@param employee table +---@param vehicle table +---@param tool table +---@param fieldId number +---@param workType string +function JobManager:startDriveToTool(employee, vehicle, tool, fieldId, workType) + local approachX, approachZ, approachAngle = self:calculateApproachPosition(vehicle, tool) + + local aiJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.GOTO) + if not aiJob then + CustomUtils:error("[JobManager] Failed to create GOTO job for drive-to-tool. Falling back to teleport.") + self:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) + return + end + + local farmId = g_currentMission:getFarmId() + aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) + aiJob.positionAngleParameter:setPosition(approachX, approachZ) + aiJob.positionAngleParameter:setAngle(approachAngle) + aiJob:setValues() + + local validateSuccess, errorMessage = aiJob:validate(farmId) + if validateSuccess then + g_currentMission.aiSystem:startJob(aiJob, farmId) + employee.currentJob = { + type = "DRIVING_TO_TOOL", + aiJobId = aiJob.jobId, + fieldId = fieldId, + workType = workType, + targetToolId = tool.id, + startTime = g_currentMission.time + } + CustomUtils:info("[JobManager] %s is now driving to tool %s (ID: %d)", employee.name, tool:getName(), tool.id) + else + CustomUtils:error("[JobManager] Drive-to-tool GOTO failed validation: %s. Falling back to teleport.", tostring(errorMessage)) + self:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) + end +end + +---Attempts to attach the tool directly if in range, otherwise teleports it +---@param employee table +---@param vehicle table +---@param tool table +---@param fieldId number +---@param workType string +function JobManager:tryDirectAttachOrTeleport(employee, vehicle, tool, fieldId, workType) + if vehicle.spec_attacherJoints then + AttacherJoints.updateVehiclesInAttachRange(vehicle, + AttacherJoints.MAX_ATTACH_DISTANCE_SQ or 100, + AttacherJoints.MAX_ATTACH_ANGLE or 1.0, true) + + local info = vehicle.spec_attacherJoints.attachableInfo + if info and info.attachable == tool then + CustomUtils:info("[JobManager] %s: Tool %s in attach range! Attaching directly...", employee.name, tool:getName()) + vehicle:attachImplementFromInfo(info) + employee.currentJob = { + type = "ATTACHING_TOOL", + fieldId = fieldId, + workType = workType, + targetToolId = tool.id, + attachStartTime = g_currentMission.time + } + return + end + end + + CustomUtils:warning("[JobManager] %s: Tool not in attach range. Teleporting tool to vehicle rear.", employee.name) + self:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) +end + +---Fallback: teleport tool near vehicle and attach directly (used when GOTO fails or timeout) +---@param employee table +---@param vehicle table +---@param tool table +---@param fieldId number +---@param workType string +function JobManager:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) + CustomUtils:warning("[JobManager] FALLBACK: Teleporting tool %s to vehicle %s", tool:getName(), vehicle:getName()) + + if tool.removeFromPhysics then + tool:removeFromPhysics() + end + + self:positionToolNearVehicle(tool, vehicle) + + if tool.addToPhysics then + tool:addToPhysics() + end + + local vJointIdx, tJointIdx = self:findCompatibleJoints(vehicle, tool) + if vehicle.attachImplement then + vehicle:attachImplement(tool, tJointIdx, vJointIdx) + end + + self:stabilizeVehicle(vehicle) + + employee.currentJob = { + type = "EQUIPMENT_READY", + fieldId = fieldId, + workType = workType, + readyFrame = 0, + readyTime = g_currentMission.time + 2000 + } +end + +---Stabilizes a vehicle after tool teleport to prevent flip/roll +---Preserves position and heading but resets pitch and roll to upright +---@param vehicle table +function JobManager:stabilizeVehicle(vehicle) + if not vehicle or not vehicle.rootNode then + return + end + + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local _, yRot, _ = getWorldRotation(vehicle.rootNode) + + if vehicle.removeFromPhysics then + vehicle:removeFromPhysics() + end + + if vehicle.setRelativePosition then + vehicle:setRelativePosition(vx, 0.5, vz, yRot) + CustomUtils:debug("[JobManager] Stabilized vehicle %s at (%.1f, %.1f) heading %.1f°", vehicle:getName(), vx, vz, math.deg(yRot)) + end + + if vehicle.addToPhysics then + vehicle:addToPhysics() + end +end + +---Starts a GOTO job to return the current tool to its parking spot before switching tools +---@param employee table +---@param vehicle table +---@param tool table The tool to return +---@param nextFieldId number +---@param nextWorkType string +function JobManager:startReturnTool(employee, vehicle, tool, nextFieldId, nextWorkType) + CustomUtils:info("[JobManager] %s: Detaching tool %s at field edge before switching to %s", + employee.name, tool:getName(), nextWorkType) + self:detachAllImplements(vehicle) + employee.currentJob = nil + self:startFieldWork(employee, nextFieldId, nextWorkType) +end + +---Starts a GOTO job to return the current tool to a specific parking spot +---Used when the employee needs to drive the tool back to its original location +---@param employee table +---@param vehicle table +---@param parkingX number +---@param parkingZ number +---@param nextFieldId number +---@param nextWorkType string +function JobManager:startReturnToolToSpot(employee, vehicle, parkingX, parkingZ, nextFieldId, nextWorkType) + local aiJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.GOTO) + if not aiJob then + CustomUtils:error("[JobManager] Failed to create GOTO job for tool return. Detaching in place.") + self:detachAllImplements(vehicle) + employee.currentJob = nil + self:startFieldWork(employee, nextFieldId, nextWorkType) + return + end + + local farmId = g_currentMission:getFarmId() + aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) + aiJob.positionAngleParameter:setPosition(parkingX, parkingZ) + aiJob:setValues() + + local validateSuccess, errorMessage = aiJob:validate(farmId) + if validateSuccess then + g_currentMission.aiSystem:startJob(aiJob, farmId) + employee.currentJob = { + type = "RETURNING_TOOL", + aiJobId = aiJob.jobId, + nextFieldId = nextFieldId, + nextWorkType = nextWorkType, + startTime = g_currentMission.time + } + CustomUtils:info("[JobManager] %s is returning tool to parking before switching to %s", + employee.name, nextWorkType) + else + CustomUtils:error("[JobManager] Tool return GOTO failed validation: %s. Detaching in place.", tostring(errorMessage)) + self:detachAllImplements(vehicle) + employee.currentJob = nil + self:startFieldWork(employee, nextFieldId, nextWorkType) + end +end + +---Gets the first attached implement on a vehicle +---@param vehicle table +---@return table|nil implement object +function JobManager:getFirstAttachedImplement(vehicle) + if not vehicle or not vehicle.getAttachedImplements then return nil end + local implements = vehicle:getAttachedImplements() + if #implements > 0 and implements[1].object then + return implements[1].object + end + return nil +end + +---Checks if the vehicle has the correct tool for a given category +---@param vehicle table +---@param categoryName string +---@return boolean +function JobManager:hasCorrectTool(vehicle, categoryName) + if not vehicle or not categoryName then return false end + local attachedImplements = vehicle:getAttachedImplements() + for _, implement in ipairs(attachedImplements) do + local obj = implement.object + if obj then + local storeItem = g_storeManager:getItemByXMLFilename(obj.configFileName) + if storeItem and storeItem.categoryName == categoryName then + return true + end + end + end + return false +end + +---Finds compatible rear attacher joints between a vehicle and a tool +---@param vehicle table +---@param tool table +---@return number vehicleJointIndex, number toolJointIndex +function JobManager:findCompatibleJoints(vehicle, tool) + local vehicleJointIndex = 1 + local toolJointIndex = 1 + + if vehicle.getAttacherJoints and tool.getInputAttacherJoints then + local vJoints = vehicle:getAttacherJoints() + local tJoints = tool:getInputAttacherJoints() + + local rearIndices = {} + for i, joint in ipairs(vJoints) do + local lx, ly, lz = localToLocal(joint.jointTransform, vehicle.rootNode, 0, 0, 0) + if joint.attacherJointDirection == -1 or lz < -0.2 then + table.insert(rearIndices, i) + end + end + + local found = false + for _, vIdx in ipairs(#rearIndices > 0 and rearIndices or {1}) do + local vJoint = vJoints[vIdx] + for tIdx, tJoint in ipairs(tJoints) do + if vJoint.jointType == tJoint.jointType then + vehicleJointIndex = vIdx + toolJointIndex = tIdx + found = true + break + end + end + if found then break end + end + end + + return vehicleJointIndex, toolJointIndex +end + function JobManager:findSuitableTool(categoryName) local items = g_storeManager:getItems() for _, item in pairs(items) do @@ -284,35 +728,7 @@ function JobManager:rentAndAttach(vehicle, storeItem, callback) if vehicleLoadState == VehicleLoadingState.OK then local tool = vehicles[1] - local vehicleJointIndex = 1 - local toolJointIndex = 1 - - if vehicle.getAttacherJoints and tool.getInputAttacherJoints then - local vJoints = vehicle:getAttacherJoints() - local tJoints = tool:getInputAttacherJoints() - - local rearIndices = {} - for i, joint in ipairs(vJoints) do - local lx, ly, lz = localToLocal(joint.jointTransform, vehicle.rootNode, 0, 0, 0) - if joint.attacherJointDirection == -1 or lz < -0.2 then - table.insert(rearIndices, i) - end - end - - local found = false - for _, vIdx in ipairs(#rearIndices > 0 and rearIndices or {1}) do - local vJoint = vJoints[vIdx] - for tIdx, tJoint in ipairs(tJoints) do - if vJoint.jointType == tJoint.jointType then - vehicleJointIndex = vIdx - toolJointIndex = tIdx - found = true - break - end - end - if found then break end - end - end + local vehicleJointIndex, toolJointIndex = self:findCompatibleJoints(vehicle, tool) CustomUtils:debug("[JobManager] Attaching %s (Joint: %d) to %s (Joint: %d)", tool:getName(), toolJointIndex, vehicle:getName(), vehicleJointIndex) @@ -386,6 +802,44 @@ function JobManager:handleFieldworkCompletion(employee) g_employeeManager:onJobCompleted(employee) end + local queue = employee.taskQueue or {} + local currentIdx = employee.currentTaskIndex or 1 + local nextIdx = currentIdx + 1 + + if employee.isAutonomous and nextIdx <= #queue then + local nextTask = queue[nextIdx] + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + + local nextCategory = JobManager.WORK_TYPE_TO_CATEGORY[nextTask] + local currentToolOk = vehicle and self:hasCorrectTool(vehicle, nextCategory) + + if not currentToolOk and vehicle then + local currentTool = self:getFirstAttachedImplement(vehicle) + if currentTool then + CustomUtils:info("[JobManager] %s needs tool swap: current tool won't work for %s. Returning tool first.", + employee.name, nextTask) + employee.currentTaskIndex = nextIdx + self:startReturnTool(employee, vehicle, currentTool, employee.targetFieldId, nextTask) + return + end + end + + employee.currentTaskIndex = nextIdx + CustomUtils:info("[JobManager] %s advancing to task %d/%d: %s", + employee.name, nextIdx, #queue, nextTask) + employee.currentJob = nil + self:startFieldWork(employee, employee.targetFieldId, nextTask) + return + end + + if #queue > 0 then + CustomUtils:info("[JobManager] %s completed all %d tasks in workflow", employee.name, #queue) + employee.currentTaskIndex = 1 + employee.isAutonomous = false + local msg = string.format(g_i18n:getText("em_workflow_queue_complete"), employee.name) + g_currentMission:showBlinkingWarning(msg, 5000) + end + if g_parkingManager and employee.assignedVehicleId then local spot = g_parkingManager:getSpotForVehicle(employee.assignedVehicleId) if spot then @@ -397,7 +851,8 @@ function JobManager:handleFieldworkCompletion(employee) local dist = math.sqrt(dx * dx + dz * dz) if dist > 20 then - CustomUtils:info("[JobManager] %s returning to parking '%s' (%.0fm away)", employee.name, spot.name, dist) + CustomUtils:info("[JobManager] %s returning to parking '%s' (%.0fm away)", + employee.name, spot.name, dist) self:startReturnToParking(employee, vehicle, spot) return end @@ -453,22 +908,6 @@ function JobManager:handleParkingArrival(employee) end end -function JobManager:findToolInParking(categoryName, vehicle) - if not g_parkingManager then return nil end - - local tool, spot = g_parkingManager:findToolInParking(categoryName) - if tool and spot and vehicle and vehicle.rootNode then - local vx, _, vz = getWorldTranslation(vehicle.rootNode) - local dx = vx - spot.x - local dz = vz - spot.z - local dist = math.sqrt(dx * dx + dz * dz) - - if dist < 50 then - return tool - end - end - return nil -end function JobManager:update(dt) for _, employee in ipairs(g_employeeManager.employees) do @@ -494,14 +933,59 @@ function JobManager:update(dt) CustomUtils:info("[JobManager] Job %d for employee %s finished or removed", employee.currentJob.aiJobId, employee.name) if employee.currentJob.type == "TRANSIT" and employee.pendingJob then - CustomUtils:info("[JobManager] Transit complete. Starting pending fieldwork...") - local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + CustomUtils:info("[JobManager] Transit complete. Deferring fieldwork start via EQUIPMENT_READY...") local pending = employee.pendingJob - self:startFieldWorkJob(employee, vehicle, pending.fieldId, pending.workType) + employee.pendingJob = nil + employee.currentJob = { + type = "EQUIPMENT_READY", + fieldId = pending.fieldId, + workType = pending.workType, + readyFrame = 0, + readyTime = g_currentMission.time + 500 + } + elseif employee.currentJob.type == "DRIVING_TO_TOOL" then + CustomUtils:info("[JobManager] %s arrived near tool. Attempting attach or teleport...", employee.name) + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + local tool = g_employeeManager:getVehicleById(employee.currentJob.targetToolId) + if vehicle and tool then + self:tryDirectAttachOrTeleport(employee, vehicle, tool, employee.currentJob.fieldId, employee.currentJob.workType) + else + CustomUtils:error("[JobManager] Vehicle or tool lost after DRIVING_TO_TOOL for %s", employee.name) + employee.currentJob = nil + end + elseif employee.currentJob.type == "RETURNING_TOOL" then + CustomUtils:info("[JobManager] %s arrived at drop-off point. Detaching tool...", employee.name) + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + if vehicle then + self:detachAllImplements(vehicle) + end + local nextFieldId = employee.currentJob.nextFieldId + local nextWorkType = employee.currentJob.nextWorkType + employee.currentJob = nil + self:startFieldWork(employee, nextFieldId, nextWorkType) elseif employee.currentJob.type == "RETURN_TO_PARKING" then self:handleParkingArrival(employee) elseif employee.currentJob.type == "FIELDWORK" then - self:handleFieldworkCompletion(employee) + local elapsed = g_currentMission.time - (employee.currentJob.startTime or 0) + if elapsed < 2000 then + employee.rapidFailCount = (employee.rapidFailCount or 0) + 1 + CustomUtils:warning("[JobManager] %s: FIELDWORK finished instantly (%dms). Rapid fail #%d", + employee.name, elapsed, employee.rapidFailCount) + if employee.rapidFailCount >= 3 then + CustomUtils:warning("[JobManager] %s: Too many rapid fails. Pausing autonomous mode.", employee.name) + employee.isAutonomous = false + employee.rapidFailCount = 0 + employee.currentJob = nil + if employee.temporaryRental then + g_employeeManager:returnRentedEquipment(employee) + end + else + self:handleFieldworkCompletion(employee) + end + else + employee.rapidFailCount = 0 + self:handleFieldworkCompletion(employee) + end else employee.currentJob = nil if g_employeeManager then @@ -512,6 +996,103 @@ function JobManager:update(dt) end end end + elseif employee.currentJob and employee.currentJob.type == "EQUIPMENT_READY" then + local job = employee.currentJob + job.readyFrame = (job.readyFrame or 0) + 1 + + if job.readyFrame >= 3 and g_currentMission.time >= (job.readyTime or 0) then + CustomUtils:info("[JobManager] EQUIPMENT_READY matured for %s (frames: %d). Evaluating distance...", + employee.name, job.readyFrame) + + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + local field = g_fieldManager:getFieldById(job.fieldId) + + if vehicle and field then + local x, z = field:getCenterOfFieldWorldPosition() + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local distance = MathUtil.vector2Length(vx - x, vz - z) + + CustomUtils:info("[JobManager] Distance to Field %d: %.1f m", job.fieldId, distance) + + if distance > 150 then + CustomUtils:info("[JobManager] Field is far. Starting TRANSIT (GOTO) job.") + local aiJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.GOTO) + if aiJob then + local farmId = g_currentMission:getFarmId() + aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) + aiJob.positionAngleParameter:setPosition(x, z) + + local dx, dz = x - vx, z - vz + local angle = MathUtil.getYRotationFromDirection(dx, dz) + aiJob.positionAngleParameter:setAngle(angle) + aiJob:setValues() + + local validateSuccess, errorMessage = aiJob:validate(farmId) + if validateSuccess then + g_currentMission.aiSystem:startJob(aiJob, farmId) + employee.currentJob = { + aiJobId = aiJob.jobId, + type = "TRANSIT", + fieldId = job.fieldId, + workType = job.workType, + startTime = g_currentMission.time + } + employee.pendingJob = { + fieldId = job.fieldId, + workType = job.workType + } + CustomUtils:info("[JobManager] %s is now in TRANSIT to field %d", employee.name, job.fieldId) + else + CustomUtils:error("[JobManager] Transit GOTO failed validation: %s", tostring(errorMessage)) + employee.currentJob = nil + end + else + CustomUtils:error("[JobManager] Failed to create GOTO job") + employee.currentJob = nil + end + else + CustomUtils:info("[JobManager] Starting FIELDWORK for %s (close proximity).", employee.name) + self:startFieldWorkJob(employee, vehicle, job.fieldId, job.workType) + end + else + CustomUtils:error("[JobManager] EQUIPMENT_READY: vehicle or field not found for %s", employee.name) + employee.currentJob = nil + end + else + employee.debugTimer = (employee.debugTimer or 0) + dt + if employee.debugTimer > 5000 then + employee.debugTimer = 0 + CustomUtils:debug("[JobMonitor] %s: EQUIPMENT_READY (frame %d, waiting for stabilization)...", + employee.name, job.readyFrame or 0) + end + end + elseif employee.currentJob and employee.currentJob.type == "ATTACHING_TOOL" then + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + if vehicle then + local allDone = true + local implements = vehicle:getAttachedImplements() + for _, impl in ipairs(implements) do + if impl.attachingIsInProgress then + allDone = false + end + end + + if allDone then + local job = employee.currentJob + CustomUtils:info("[JobManager] %s: Tool attachment complete! Proceeding to EQUIPMENT_READY.", + employee.name) + employee.currentJob = { + type = "EQUIPMENT_READY", + fieldId = job.fieldId, + workType = job.workType, + readyFrame = 0, + readyTime = g_currentMission.time + 1000 + } + end + else + CustomUtils:error("[JobManager] ATTACHING_TOOL: vehicle lost for %s", employee.name) + employee.currentJob = nil + end elseif employee.currentJob and employee.currentJob.type == "PREPARING" then employee.debugTimer = (employee.debugTimer or 0) + dt if employee.debugTimer > 5000 then diff --git a/scripts/types/employee.lua b/scripts/types/employee.lua index e470f8f..0a57704 100644 --- a/scripts/types/employee.lua +++ b/scripts/types/employee.lua @@ -24,6 +24,7 @@ function Employee.new(id, name, skills) self.isRenting = false self.isAutonomous = false self.taskQueue = {} + self.currentTaskIndex = 1 self.shiftStart = 6 self.shiftEnd = 18 @@ -203,6 +204,7 @@ function Employee:toTable() targetFieldId = self.targetFieldId, isAutonomous = self.isAutonomous, taskQueue = self.taskQueue, + currentTaskIndex = self.currentTaskIndex, shiftStart = self.shiftStart, shiftEnd = self.shiftEnd, traits = self.traits, @@ -243,6 +245,7 @@ function Employee.fromTable(data) e.isRenting = data.isRenting e.assignedVehicleId = data.assignedVehicleId e.taskQueue = data.taskQueue or {} + e.currentTaskIndex = data.currentTaskIndex or 1 e.shiftStart = data.shiftStart or 6 e.shiftEnd = data.shiftEnd or 18 if data.traits and type(data.traits) == "table" then @@ -283,8 +286,9 @@ function Employee:writeStream(streamId, connection) end streamWriteInt32(streamId, self.shiftStart or 6) streamWriteInt32(streamId, self.shiftEnd or 18) + streamWriteInt32(streamId, self.currentTaskIndex or 1) - streamWriteInt8(streamId, 3) + streamWriteInt8(streamId, 4) local traitsStr = TraitSystem.serialize(self.traits) streamWriteString(streamId, traitsStr) @@ -306,7 +310,8 @@ function Employee:writeStream(streamId, connection) streamWriteFloat32(streamId, self.milestoneWageMult or 1.0) - -- v3: personal info + streamWriteBool(streamId, self.isAutonomous or false) + streamWriteInt8(streamId, self.age or 30) streamWriteString(streamId, self.nationality or "FR") streamWriteString(streamId, self.gender or "male") @@ -333,6 +338,7 @@ function Employee:readStream(streamId, connection) end self.shiftStart = streamReadInt32(streamId) self.shiftEnd = streamReadInt32(streamId) + self.currentTaskIndex = streamReadInt32(streamId) local streamVersion = streamReadInt8(streamId) if streamVersion >= 2 then @@ -365,6 +371,12 @@ function Employee:readStream(streamId, connection) self.milestoneWageMult = 1.0 end + if streamVersion >= 4 then + self.isAutonomous = streamReadBool(streamId) + else + self.isAutonomous = false + end + if streamVersion >= 3 then self.age = streamReadInt8(streamId) self.nationality = streamReadString(streamId) From a914c5e23f491de22cebb1c633f0e6c6f36db6a6 Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:14:45 +0100 Subject: [PATCH 07/10] feat: Enhance DBAPI and XML persistence with ORM support for employee and field configurations --- scripts/persistence/DBAPIPersistence.lua | 250 +++++++++++++++-------- scripts/persistence/XMLPersistence.lua | 23 +++ 2 files changed, 193 insertions(+), 80 deletions(-) diff --git a/scripts/persistence/DBAPIPersistence.lua b/scripts/persistence/DBAPIPersistence.lua index d4efdfd..88af850 100644 --- a/scripts/persistence/DBAPIPersistence.lua +++ b/scripts/persistence/DBAPIPersistence.lua @@ -1,9 +1,12 @@ DBAPIPersistence = {} DBAPIPersistence.__index = DBAPIPersistence DBAPIPersistence.NAMESPACE = "FS25_EmployeeManager" +DBAPIPersistence.CURRENT_VERSION = 1 function DBAPIPersistence:new() - return setmetatable({}, self) + local self = setmetatable({}, DBAPIPersistence) + self.db = nil + return self end function DBAPIPersistence:getName() @@ -17,20 +20,84 @@ function DBAPIPersistence:getAPI() return nil end +function DBAPIPersistence:getDb() + if self.db then return self.db end + + local api = self:getAPI() + if api and api.isReady() and api.hasORM and api.hasORM() then + local db = api.bind(self.NAMESPACE) + if db then + self:initModels(db) + self:migrate(db) + self.db = db + return db + end + end + return nil +end + +function DBAPIPersistence:initModels(db) + local _, err = db:define("Employee", { + fields = { + data = { type = "table", required = true } + } + }) + if err then CustomUtils:error("[DBAPIPersistence] Error defining Employee model: %s", tostring(err)) end + + _, err = db:define("Parking", { + fields = { + spots = { type = "table", required = true }, + nextSpotId = { type = "number", default = 1 } + } + }) + if err then CustomUtils:error("[DBAPIPersistence] Error defining Parking model: %s", tostring(err)) end + + _, err = db:define("Settings", { + fields = { + version = { type = "number", default = 1 }, + nextEmployeeId = { type = "number", default = 1 }, + lastPoolRefreshDay = { type = "number", default = 0 }, + lastPaymentPeriod = { type = "number", default = 0 } + } + }) + if err then CustomUtils:error("[DBAPIPersistence] Error defining Settings model: %s", tostring(err)) end + + _, err = db:define("FieldConfig", { + fields = { + fieldId = { type = "number", required = true }, + cropName = { type = "string", required = true } + } + }) + if err then CustomUtils:error("[DBAPIPersistence] Error defining FieldConfig model: %s", tostring(err)) end +end + +function DBAPIPersistence:migrate(db) + local settings, _ = db:find("Settings") + local version = settings and settings.version or 0 + + if version < self.CURRENT_VERSION then + CustomUtils:info("[DBAPIPersistence] Migrating database from version %d to %d", version, self.CURRENT_VERSION) + + if settings then + db:update("Settings", settings.id, { version = self.CURRENT_VERSION }) + else + db:create("Settings", { version = self.CURRENT_VERSION }) + end + end +end + function DBAPIPersistence:isAvailable() if g_currentMission and g_currentMission.missionDynamicInfo and g_currentMission.missionDynamicInfo.isMultiplayer then - CustomUtils:debug("[DBAPIPersistence] Multiplayer detected, not available") return false end local api = self:getAPI() - if api == nil then - CustomUtils:debug("[DBAPIPersistence] g_globalMods['FS25_DBAPI'] not found") + if api == nil or not api.isReady() then return false end - if not api.isReady() then - CustomUtils:debug("[DBAPIPersistence] DBAPI present but not ready") + if not api.hasORM or not api.hasORM() then + CustomUtils:warning("[DBAPIPersistence] DBAPI version too old (no ORM support)") return false end @@ -38,112 +105,135 @@ function DBAPIPersistence:isAvailable() end function DBAPIPersistence:save(employeeManager, parkingManager) - local api = self:getAPI() - if api == nil then - CustomUtils:error("[DBAPIPersistence] DBAPI not available for save") + local db = self:getDb() + if not db then + CustomUtils:error("[DBAPIPersistence] DBAPI ORM not available for save") return false end - local ns = self.NAMESPACE + local settings = { + nextEmployeeId = employeeManager.nextEmployeeId or 1, + lastPoolRefreshDay = employeeManager.lastPoolRefreshDay or 0, + lastPaymentPeriod = employeeManager.lastPaymentPeriod or 0 + } + local sRec, _ = db:find("Settings") + if sRec then + db:update("Settings", sRec.id, settings) + else + db:create("Settings", settings) + end - local employeeIds = {} - for _, e in ipairs(employeeManager.employees) do - table.insert(employeeIds, e.id) - CustomUtils:debug("[DBAPIPersistence] Saving employee_%d (%s) isHired=%s vehicle=%s", e.id, e.name, tostring(e.isHired), tostring(e.assignedVehicleId)) - local data = e:toTable() - local success, err = api.setValue(ns, "employee_" .. tostring(e.id), data) - CustomUtils:debug("[DBAPIPersistence] setValue employee_%d => success=%s err=%s", e.id, tostring(success), tostring(err)) - if not success then - CustomUtils:error("[DBAPIPersistence] Failed to save employee %d: %s", e.id, tostring(err)) - return false + if parkingManager then + local pData = { + spots = parkingManager.spots or {}, + nextSpotId = parkingManager.nextSpotId or 1 + } + local pRec, _ = db:find("Parking") + if pRec then + db:update("Parking", pRec.id, pData) + else + db:create("Parking", pData) end end - local success, err - success, err = api.setValue(ns, "meta_employeeIds", employeeIds) - CustomUtils:debug("[DBAPIPersistence] setValue meta_employeeIds => success=%s err=%s", tostring(success), tostring(err)) - if not success then - CustomUtils:error("[DBAPIPersistence] Failed to save meta_employeeIds: %s", tostring(err)) - return false + local existing, _ = db:findAll("Employee") + if existing then + for _, rec in ipairs(existing) do + db:delete("Employee", rec.id) + end end - api.setValue(ns, "meta_nextEmployeeId", employeeManager.nextEmployeeId) - api.setValue(ns, "meta_lastPoolRefreshDay", employeeManager.lastPoolRefreshDay or 0) - api.setValue(ns, "meta_lastPaymentPeriod", employeeManager.lastPaymentPeriod or 0) + local count = 0 + for _, e in ipairs(employeeManager.employees) do + local _, err = db:create("Employee", { data = e:toTable() }) + if not err then + count = count + 1 + else + CustomUtils:error("[DBAPIPersistence] Failed to save employee %d: %s", e.id, tostring(err)) + end + end - if parkingManager then - local parkingData = { - spots = parkingManager.spots, - nextSpotId = parkingManager.nextSpotId, - } - api.setValue(ns, "parking", parkingData) + local existingFC, _ = db:findAll("FieldConfig") + if existingFC then + for _, rec in ipairs(existingFC) do + db:delete("FieldConfig", rec.id) + end end - local hiredCount = 0 - for _, e in ipairs(employeeManager.employees) do - if e.isHired then hiredCount = hiredCount + 1 end + local fcCount = 0 + for fieldId, config in pairs(employeeManager.fieldConfigs or {}) do + local _, err = db:create("FieldConfig", { fieldId = fieldId, cropName = config.cropName or "" }) + if not err then + fcCount = fcCount + 1 + end end - CustomUtils:info("[DBAPIPersistence] Saved %d employees (%d hired) via DBAPI", #employeeManager.employees, hiredCount) + + CustomUtils:info("[DBAPIPersistence] Saved %d employees and %d field configs via DBAPI ORM", count, fcCount) return true end function DBAPIPersistence:load(employeeManager, parkingManager) - local api = self:getAPI() - if api == nil then - CustomUtils:error("[DBAPIPersistence] DBAPI not available for load") + local db = self:getDb() + if not db then + CustomUtils:error("[DBAPIPersistence] DBAPI ORM not available for load") return false end - local ns = self.NAMESPACE + local sRec, _ = db:find("Settings") + if sRec then + employeeManager.nextEmployeeId = sRec.nextEmployeeId or 1 + employeeManager.lastPoolRefreshDay = sRec.lastPoolRefreshDay or 0 + employeeManager.lastPaymentPeriod = sRec.lastPaymentPeriod or 0 + end - local employeeIds = api.getValue(ns, "meta_employeeIds") - if employeeIds == nil or type(employeeIds) ~= "table" or #employeeIds == 0 then - CustomUtils:info("[DBAPIPersistence] No employee data found in DBAPI") - return false + if parkingManager then + local pRec, _ = db:find("Parking") + if pRec then + parkingManager.spots = pRec.spots or {} + parkingManager.nextSpotId = pRec.nextSpotId or 1 + CustomUtils:debug("[DBAPIPersistence] Loaded %d parking spots", #parkingManager.spots) + end end - employeeManager.employees = {} - local maxId = 0 - for _, id in ipairs(employeeIds) do - local data = api.getValue(ns, "employee_" .. tostring(id)) - if data ~= nil then - local emp = Employee.fromTable(data) - if emp ~= nil then - if emp.assignedVehicleId and emp.assignedVehicleId ~= 0 then - local vehicle = employeeManager:getVehicleById(emp.assignedVehicleId) - if vehicle then - emp:assignVehicle(vehicle) + local emps, _ = db:findAll("Employee") + if emps and #emps > 0 then + employeeManager.employees = {} + local maxId = 0 + local hiredCount = 0 + for _, rec in ipairs(emps) do + if rec.data then + local emp = Employee.fromTable(rec.data) + if emp then + if emp.assignedVehicleId and emp.assignedVehicleId ~= 0 then + local vehicle = employeeManager:getVehicleById(emp.assignedVehicleId) + if vehicle then + emp:assignVehicle(vehicle) + end end + table.insert(employeeManager.employees, emp) + if emp.id > maxId then maxId = emp.id end + if emp.isHired then hiredCount = hiredCount + 1 end end - table.insert(employeeManager.employees, emp) - if emp.id > maxId then maxId = emp.id end end end - end - local nextId = api.getValue(ns, "meta_nextEmployeeId") - employeeManager.nextEmployeeId = nextId or (maxId + 1) - if employeeManager.nextEmployeeId <= maxId then - employeeManager.nextEmployeeId = maxId + 1 - end - - employeeManager.lastPoolRefreshDay = api.getValue(ns, "meta_lastPoolRefreshDay") or 0 - employeeManager.lastPaymentPeriod = api.getValue(ns, "meta_lastPaymentPeriod") or 0 - - if parkingManager then - local parkingData = api.getValue(ns, "parking") - if parkingData and type(parkingData) == "table" then - parkingManager.spots = parkingData.spots or {} - parkingManager.nextSpotId = parkingData.nextSpotId or 1 - CustomUtils:info("[DBAPIPersistence] Loaded %d parking spots", #parkingManager.spots) + if employeeManager.nextEmployeeId <= maxId then + employeeManager.nextEmployeeId = maxId + 1 end + + CustomUtils:info("[DBAPIPersistence] Loaded %d employees (%d hired) via DBAPI ORM", #employeeManager.employees, hiredCount) + else + CustomUtils:info("[DBAPIPersistence] No employee data found in DBAPI ORM") end - local hiredCount = 0 - for _, e in ipairs(employeeManager.employees) do - if e.isHired then hiredCount = hiredCount + 1 end + local fcs, _ = db:findAll("FieldConfig") + employeeManager.fieldConfigs = {} + if fcs then + for _, rec in ipairs(fcs) do + employeeManager.fieldConfigs[rec.fieldId] = { cropName = rec.cropName } + end + CustomUtils:debug("[DBAPIPersistence] Loaded %d field configs", #fcs) end - CustomUtils:info("[DBAPIPersistence] Loaded %d employees (%d hired) via DBAPI", #employeeManager.employees, hiredCount) local numToGenerate = 10 - #employeeManager.employees if numToGenerate > 0 then diff --git a/scripts/persistence/XMLPersistence.lua b/scripts/persistence/XMLPersistence.lua index e91700f..59f969e 100644 --- a/scripts/persistence/XMLPersistence.lua +++ b/scripts/persistence/XMLPersistence.lua @@ -35,6 +35,16 @@ function XMLPersistence:save(employeeManager, parkingManager) end employeeManager:saveToXMLFile(xmlFile, "employeeManager") + + local fieldKey = "employeeManager.fieldConfigs" + local fIdx = 0 + for fieldId, config in pairs(employeeManager.fieldConfigs or {}) do + local base = string.format("%s.fieldConfig(%d)", fieldKey, fIdx) + setXMLInt(xmlFile, base .. "#fieldId", fieldId) + setXMLString(xmlFile, base .. "#cropName", config.cropName or "") + fIdx = fIdx + 1 + end + saveXMLFile(xmlFile) delete(xmlFile) @@ -67,6 +77,19 @@ function XMLPersistence:load(employeeManager, parkingManager) end employeeManager:loadFromXMLFile(xmlFile, "employeeManager") + + employeeManager.fieldConfigs = {} + local fieldKey = "employeeManager.fieldConfigs" + local fIdx = 0 + while true do + local base = string.format("%s.fieldConfig(%d)", fieldKey, fIdx) + local fId = getXMLInt(xmlFile, base .. "#fieldId") + if not fId then break end + local cName = getXMLString(xmlFile, base .. "#cropName") + employeeManager.fieldConfigs[fId] = { cropName = cName } + fIdx = fIdx + 1 + end + delete(xmlFile) local hiredCount = 0 From c680b83705ed2f83d30ccf22141a0ccca8ad772b Mon Sep 17 00:00:00 2001 From: LeGrizzly Date: Tue, 10 Mar 2026 21:14:52 +0100 Subject: [PATCH 08/10] fix: Update version number and enhance icon management in main script --- modDesc.xml | 2 +- scripts/main.lua | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/modDesc.xml b/modDesc.xml index 1bf0d8c..778de92 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -1,7 +1,7 @@  LeGrizzly - 0.0.6-build-4 + 0.0.7-build-1 <en><![CDATA[Advanced Employee Manager]]></en> <fr><![CDATA[Gestion Avancée Des Employés]]></fr> diff --git a/scripts/main.lua b/scripts/main.lua index 88c0b33..ee5f351 100644 --- a/scripts/main.lua +++ b/scripts/main.lua @@ -7,7 +7,32 @@ MessageType.EMPLOYEE_ADDED = nextMessageTypeId() MessageType.EMPLOYEE_REMOVED = nextMessageTypeId() MessageType.EMPLOYEE_SKILL_LEVELUP = nextMessageTypeId() +g_EMIconMenu = Utils.getFilename("images/MenuIcon.dds", g_modDirectory) +g_EMIconEmployee = Utils.getFilename("images/EMEmployeeIcon.dds", g_modDirectory) +g_EMIconWorkflow = Utils.getFilename("images/EMWorkflowIcon.dds", g_modDirectory) +g_EMIconField = Utils.getFilename("images/EMFieldIcon.dds", g_modDirectory) +g_EMIconVehicle = Utils.getFilename("images/EMVehicleIcon.dds", g_modDirectory) + +local EM_ICON_GLOBALS = { + g_EMIconMenu = true, + g_EMIconEmployee = true, + g_EMIconWorkflow = true, + g_EMIconField = true, + g_EMIconVehicle = true, +} + +local function emResolveFilename(self, superFunc) + local filename = superFunc(self) + if EM_ICON_GLOBALS[filename] then + return _G[filename] + end + return filename +end + +GuiOverlay.resolveFilename = Utils.overwrittenFunction(GuiOverlay.resolveFilename, emResolveFilename) + source(g_modDirectory .. "scripts/utils/Utils.lua") + source(g_modDirectory .. "scripts/types/traitsystem.lua") source(g_modDirectory .. "scripts/types/skillsystem.lua") source(g_modDirectory .. "scripts/types/skilleffects.lua") From 16d0bd14767df30d5ea82d5f24e8e6bd19034ea1 Mon Sep 17 00:00:00 2001 From: LeGrizzly <xalsie.ff@hotmail.fr> Date: Tue, 31 Mar 2026 11:16:11 +0200 Subject: [PATCH 09/10] Refactor persistence management and add SILODB support - Replaced DBAPIPersistence with SILODBPersistence in ModController. - Updated PersistenceManager to handle VehicleSnapshotManager in save/load methods. - Enhanced PersistenceStrategy to include snapshot management. - Implemented SILODBPersistence with ORM support for Employee, Parking, Settings, and VehicleSnapshot models. - Added migration logic for database versioning in SILODBPersistence. - Updated XMLPersistence to support saving/loading of vehicle snapshots. - Introduced new GUI profiles for improved layout and alignment in EMFieldFrame. - Adjusted field configuration layout for better usability. --- modDesc.xml | 4 +- scripts/extensions/AIOverrideExtension.lua | 1 + scripts/gui/EMEmployeeFrame.lua | 14 ++ scripts/gui/EMFieldFrame.lua | 28 ++- scripts/gui/EMWorkflowFrame.lua | 1 + scripts/gui/MenuEmployeeManager.lua | 16 ++ scripts/main.lua | 4 +- scripts/managers/VehicleSnapshotManager.lua | 151 ++++++++++++++++ scripts/managers/employeemanager.lua | 20 ++- scripts/managers/jobmanager.lua | 130 ++++++++++++-- scripts/modcontroller.lua | 12 +- scripts/persistence/PersistenceManager.lua | 8 +- scripts/persistence/PersistenceStrategy.lua | 4 +- ...IPersistence.lua => SILODBPersistence.lua} | 108 +++++++---- scripts/persistence/XMLPersistence.lua | 78 +++++++- scripts/types/employee.lua | 2 + xml/gui/EMFieldFrame.xml | 167 +++++++++--------- xml/gui/guiProfiles.xml | 13 +- 18 files changed, 598 insertions(+), 163 deletions(-) create mode 100644 scripts/managers/VehicleSnapshotManager.lua rename scripts/persistence/{DBAPIPersistence.lua => SILODBPersistence.lua} (60%) diff --git a/modDesc.xml b/modDesc.xml index 778de92..6fb17ee 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8" standalone="no"?> <modDesc descVersion="106"> <author>LeGrizzly</author> - <version>0.0.7-build-1</version> + <version>0.0.8-build-1</version> <title> <en><![CDATA[Advanced Employee Manager]]></en> <fr><![CDATA[Gestion Avancée Des Employés]]></fr> @@ -19,7 +19,7 @@ <es><![CDATA[Aporta un nuevo nivel de profundidad a la fuerza laboral de tu granja. Contrata y gestiona empleados con habilidades únicas para optimizar tus operaciones.]]></es> </description> <dependencies> - <dependency optional="true">FS25_DBAPI</dependency> + <dependency optional="true">FS25_SILODB</dependency> </dependencies> <iconFilename>images/icon_employee.dds</iconFilename> <multiplayer supported="true" /> diff --git a/scripts/extensions/AIOverrideExtension.lua b/scripts/extensions/AIOverrideExtension.lua index b8fcd91..59df08f 100644 --- a/scripts/extensions/AIOverrideExtension.lua +++ b/scripts/extensions/AIOverrideExtension.lua @@ -47,6 +47,7 @@ function AIOverrideExtension.onToggleAI(vehicle, actionName, inputValue, callbac g_gui:showGui("MenuEmployeeManager") end else + -- No employee assigned: open EM menu (vanilla AI is disabled) g_gui:showGui("MenuEmployeeManager") end end diff --git a/scripts/gui/EMEmployeeFrame.lua b/scripts/gui/EMEmployeeFrame.lua index 47c178d..eac1156 100644 --- a/scripts/gui/EMEmployeeFrame.lua +++ b/scripts/gui/EMEmployeeFrame.lua @@ -174,6 +174,7 @@ function EMEmployeeFrame:rebuildTable() self.noEmployeesContainer:setVisible(not hasItems) end + -- Hide detail panels when rebuilding if self.detailPanel then self.detailPanel:setVisible(false) end if self.rightPanel then self.rightPanel:setVisible(false) end if self.columnSeparator then self.columnSeparator:setVisible(false) end @@ -217,30 +218,37 @@ function EMEmployeeFrame:displayEmployeeDetails(index) return end + -- Show detail panels if self.detailPanel then self.detailPanel:setVisible(true) end if self.rightPanel then self.rightPanel:setVisible(true) end if self.columnSeparator then self.columnSeparator:setVisible(true) end if self.noSelectedText then self.noSelectedText:setVisible(false) end + -- Portrait if self.detailAvatar then self.detailAvatar:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") end + -- Identity if self.txtName then self.txtName:setText(emp.name) end if self.txtId then self.txtId:setText(string.format("ID: %d", emp.id)) end + -- Status if self.txtStatus then local statusKey = emp.isHired and "em_status_hired" or "em_status_available" self.txtStatus:setText(g_i18n:getText(statusKey)) end + -- Skills with progress bars self:displaySkills(emp) + -- Traits if self.txtTraitsList then local traitName = emp.getTraitName and emp:getTraitName() or nil self.txtTraitsList:setText(traitName or g_i18n:getText("em_none")) end + -- Wage if self.txtWage then local hourly = emp.getHourlyWage and emp:getHourlyWage() or 0 local marketMult = 1.0 @@ -270,6 +278,7 @@ function EMEmployeeFrame:displayEmployeeDetails(index) self.txtWageBreakdown:setText(parts) end + -- Toggle Column 3 content based on hired/available local isHired = emp.isHired if self.hiredInfoSection then self.hiredInfoSection:setVisible(isHired) end if self.availableInfoSection then self.availableInfoSection:setVisible(not isHired) end @@ -278,6 +287,7 @@ function EMEmployeeFrame:displayEmployeeDetails(index) self:displayWorkStats(emp) self:displayPerformanceStats(emp) + -- Workflow summary if self.txtWorkflowSummary then local queue = emp.taskQueue or {} if #queue > 0 then @@ -326,11 +336,13 @@ function EMEmployeeFrame:displaySkills(employee) end function EMEmployeeFrame:displayPersonalInfo(employee) + -- Age if self.txtPersonalAge then local age = employee.age or 30 self.txtPersonalAge:setText(tostring(age)) end + -- Nationality if self.txtPersonalNationality then local natKey = "em_nationality_" .. (employee.nationality or "FR") local natText = g_i18n:getText(natKey) @@ -340,6 +352,7 @@ function EMEmployeeFrame:displayPersonalInfo(employee) self.txtPersonalNationality:setText(natText) end + -- Biography if self.txtPersonalBio then local bioKey = employee.bioKey or "em_bio_default" local bioText = g_i18n:getText(bioKey) @@ -347,6 +360,7 @@ function EMEmployeeFrame:displayPersonalInfo(employee) self.txtPersonalBio:setText(bioText) end + -- Quote if self.txtPersonalQuote then local quoteKey = employee.quoteKey or "em_quote_default" local quoteText = g_i18n:getText(quoteKey) diff --git a/scripts/gui/EMFieldFrame.lua b/scripts/gui/EMFieldFrame.lua index 2b2bf67..bb7e436 100644 --- a/scripts/gui/EMFieldFrame.lua +++ b/scripts/gui/EMFieldFrame.lua @@ -4,8 +4,9 @@ local EMFieldFrame_mt = Class(EMFieldFrame, TabbedMenuFrameElement) function EMFieldFrame:new() local self = TabbedMenuFrameElement.new(nil, EMFieldFrame_mt) - self.fields = {} - self.menuButtonInfo = {} + self.fields = {} + self.menuButtonInfo = {} + self.pendingTargetCrop = nil return self end @@ -189,6 +190,8 @@ function EMFieldFrame:displayFieldDetails(index) self.targetCropSelector:setState(state, false) end + self.pendingTargetCrop = nil + local conditionText = self:getFieldCondition(fieldData.fieldRef) if self.txtFieldCondition then self.txtFieldCondition:setText(conditionText) @@ -276,10 +279,12 @@ function EMFieldFrame:getAssignedEmployee(fieldId) end function EMFieldFrame:displayAssignedEmployee(emp) + -- Name if self.txtAssignedEmployee then self.txtAssignedEmployee:setText(emp.name or "???") end + -- Status if self.txtEmpStatus then local statusText if emp.isUnpaid then @@ -309,6 +314,7 @@ function EMFieldFrame:displayAssignedEmployee(emp) self.txtEmpStatus:setText(statusText) end + -- Vehicle if self.txtEmpVehicle then local vehicleName = g_i18n:getText("em_none") if emp.assignedVehicleId and g_employeeManager then @@ -325,6 +331,7 @@ function EMFieldFrame:displayAssignedEmployee(emp) self.txtEmpVehicle:setText(vehicleName) end + -- Shift if self.txtEmpShift then self.txtEmpShift:setText(string.format("%02d:00 %s %02d:00", emp.shiftStart or 6, @@ -332,6 +339,7 @@ function EMFieldFrame:displayAssignedEmployee(emp) emp.shiftEnd or 18)) end + -- Fatigue if self.txtEmpFatigue then local fatigue = emp.fatigueLevel or 0 self.txtEmpFatigue:setText(string.format("%.0f%%", fatigue)) @@ -346,8 +354,10 @@ function EMFieldFrame:displayAssignedEmployee(emp) end end + -- Skills self:displaySkills(emp) + -- Workflow queue if self.txtEmpWorkflow then local queue = emp.taskQueue or {} if #queue > 0 then @@ -361,6 +371,7 @@ function EMFieldFrame:displayAssignedEmployee(emp) end end + -- Current task if self.txtEmpCurrentTask then if emp.currentJob then local jobType = emp.currentJob.workType or emp.currentJob.type or "Unknown" @@ -442,9 +453,18 @@ function EMFieldFrame:getMenuButtonInfo() end function EMFieldFrame:onTargetCropChanged(state) + if self.cropNames[state] then + self.pendingTargetCrop = self.cropNames[state] + end +end + +function EMFieldFrame:onSaveFieldConfig() local index = self.fieldList:getSelectedIndex() local fieldData = self.fields[index] - if fieldData and self.cropNames[state] then - g_employeeManager:setFieldTargetCrop(fieldData.fieldId, self.cropNames[state]) + if fieldData == nil then return end + + if self.pendingTargetCrop then + g_employeeManager:setFieldTargetCrop(fieldData.fieldId, self.pendingTargetCrop) + self.pendingTargetCrop = nil end end diff --git a/scripts/gui/EMWorkflowFrame.lua b/scripts/gui/EMWorkflowFrame.lua index e057a21..8b847c1 100644 --- a/scripts/gui/EMWorkflowFrame.lua +++ b/scripts/gui/EMWorkflowFrame.lua @@ -180,6 +180,7 @@ function EMWorkflowFrame:populateCellForItemInSection(list, section, index, cell local avatarEl = cell:getAttribute("avatar") local iconEl = cell:getAttribute("icon") + -- Show avatar, hide atlas icon if avatarEl then avatarEl:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") avatarEl:setVisible(true) diff --git a/scripts/gui/MenuEmployeeManager.lua b/scripts/gui/MenuEmployeeManager.lua index ec29ee1..f61719b 100644 --- a/scripts/gui/MenuEmployeeManager.lua +++ b/scripts/gui/MenuEmployeeManager.lua @@ -115,6 +115,7 @@ function MenuEmployeeManager:updateContent() local hasItem = self.leftListTable:getItemCount() > 0 + -- Toggle visibility of containers if self.employeesContainer then self.employeesContainer:setVisible(hasItem) end @@ -168,23 +169,29 @@ function MenuEmployeeManager:displayEmployeeDetails(employee) self.personalPanelContainer:setVisible(true) if self.columnSeparator then self.columnSeparator:setVisible(true) end + -- Avatar if self.detailAvatar ~= nil then self.detailAvatar:setImageFilename(g_modDirectory .. "textures/assets/profil_male_1.png") end + -- Identity self.employeeName:setText(employee.name) self.employeeId:setText(string.format("ID: %d", employee.id)) + -- Trait (subtitle under name) if self.employeeTrait ~= nil then local traitName = employee.getTraitName and employee:getTraitName() or nil self.employeeTrait:setText(traitName or g_i18n:getText("em_none")) end + -- Status local statusKey = employee.isHired and "em_status_hired" or "em_status_available" self.employeeStatusValue:setText(g_i18n:getText(statusKey)) + -- Skills with progress bars self:displaySkills(employee) + -- Work stats (conditionally visible) local isHired = employee.isHired if self.workStatsSection then self.workStatsSection:setVisible(isHired) @@ -193,14 +200,17 @@ function MenuEmployeeManager:displayEmployeeDetails(employee) self:displayWorkStats(employee) end + -- Traits list if self.txtTraitsList then local traitName = employee.getTraitName and employee:getTraitName() or nil self.txtTraitsList:setText(traitName or g_i18n:getText("em_none")) end + -- Wage local wage = employee.getDailyWage and employee:getDailyWage() or 0 self.employeeWageValue:setText(g_i18n:formatMoney(wage, 0, true, false)) + -- Personal info (right column) self:displayPersonalInfo(employee) end @@ -265,6 +275,7 @@ function MenuEmployeeManager:displayWorkStats(employee) end end + -- Assigned field if self.txtAssignedField then if employee.targetFieldId then self.txtAssignedField:setText(string.format("Field %d", employee.targetFieldId)) @@ -273,6 +284,7 @@ function MenuEmployeeManager:displayWorkStats(employee) end end + -- Fatigue if self.statFatigue then local fatigue = employee.fatigueLevel or 0 if employee.isOnBreak then @@ -288,11 +300,13 @@ function MenuEmployeeManager:displayWorkStats(employee) end function MenuEmployeeManager:displayPersonalInfo(employee) + -- Age if self.txtPersonalAge then local age = employee.age or 30 self.txtPersonalAge:setText(tostring(age)) end + -- Nationality if self.txtPersonalNationality then local natKey = "em_nationality_" .. (employee.nationality or "FR") local natText = g_i18n:getText(natKey) @@ -302,6 +316,7 @@ function MenuEmployeeManager:displayPersonalInfo(employee) self.txtPersonalNationality:setText(natText) end + -- Biography if self.txtPersonalBio then local bioKey = employee.bioKey or "em_bio_default" local bioText = g_i18n:getText(bioKey) @@ -309,6 +324,7 @@ function MenuEmployeeManager:displayPersonalInfo(employee) self.txtPersonalBio:setText(bioText) end + -- Quote if self.txtPersonalQuote then local quoteKey = employee.quoteKey or "em_quote_default" local quoteText = g_i18n:getText(quoteKey) diff --git a/scripts/main.lua b/scripts/main.lua index ee5f351..336e2a3 100644 --- a/scripts/main.lua +++ b/scripts/main.lua @@ -7,6 +7,7 @@ MessageType.EMPLOYEE_ADDED = nextMessageTypeId() MessageType.EMPLOYEE_REMOVED = nextMessageTypeId() MessageType.EMPLOYEE_SKILL_LEVELUP = nextMessageTypeId() +-- Icon globals (resolved by GuiOverlay.resolveFilename hook) g_EMIconMenu = Utils.getFilename("images/MenuIcon.dds", g_modDirectory) g_EMIconEmployee = Utils.getFilename("images/EMEmployeeIcon.dds", g_modDirectory) g_EMIconWorkflow = Utils.getFilename("images/EMWorkflowIcon.dds", g_modDirectory) @@ -57,10 +58,11 @@ source(g_modDirectory .. "scripts/managers/cropmanager.lua") source(g_modDirectory .. "scripts/managers/employeemanager.lua") source(g_modDirectory .. "scripts/managers/jobmanager.lua") source(g_modDirectory .. "scripts/managers/parkingmanager.lua") +source(g_modDirectory .. "scripts/managers/VehicleSnapshotManager.lua") source(g_modDirectory .. "scripts/managers/milestonesystem.lua") source(g_modDirectory .. "scripts/persistence/PersistenceStrategy.lua") source(g_modDirectory .. "scripts/persistence/XMLPersistence.lua") -source(g_modDirectory .. "scripts/persistence/DBAPIPersistence.lua") +source(g_modDirectory .. "scripts/persistence/SILODBPersistence.lua") source(g_modDirectory .. "scripts/persistence/PersistenceManager.lua") source(g_modDirectory .. "scripts/extensions/WearableExtension.lua") source(g_modDirectory .. "scripts/extensions/HarvestExtension.lua") diff --git a/scripts/managers/VehicleSnapshotManager.lua b/scripts/managers/VehicleSnapshotManager.lua new file mode 100644 index 0000000..0db722f --- /dev/null +++ b/scripts/managers/VehicleSnapshotManager.lua @@ -0,0 +1,151 @@ +VehicleSnapshotManager = {} + +local VehicleSnapshotManager_mt = Class(VehicleSnapshotManager) + +---@return VehicleSnapshotManager +function VehicleSnapshotManager:new() + local self = setmetatable({}, VehicleSnapshotManager_mt) + self.snapshots = {} + CustomUtils:debug("[SnapshotManager] Initialized") + return self +end + +---Captures a snapshot of the vehicle and all attached tools' positions +---@param employee table +---@param vehicle table +function VehicleSnapshotManager:captureSnapshot(employee, vehicle) + if not employee or not vehicle or not vehicle.rootNode then + CustomUtils:warning("[SnapshotManager] Cannot capture snapshot: invalid employee or vehicle") + return + end + + local vx, vy, vz = getWorldTranslation(vehicle.rootNode) + local dx, _, dz = localDirectionToWorld(vehicle.rootNode, 0, 0, 1) + local angle = MathUtil.getYRotationFromDirection(dx, dz) + + local snapshot = { + employeeId = employee.id, + timestamp = g_currentMission.time, + vehicle = { + id = vehicle.id, + name = vehicle:getName(), + x = vx, + y = vy, + z = vz, + angle = angle + }, + tools = {} + } + + -- Capture all attached implements + if vehicle.getAttachedImplements then + local implements = vehicle:getAttachedImplements() + for _, implement in ipairs(implements) do + local obj = implement.object + if obj and obj.rootNode then + local tx, ty, tz = getWorldTranslation(obj.rootNode) + local tdx, _, tdz = localDirectionToWorld(obj.rootNode, 0, 0, 1) + local tAngle = MathUtil.getYRotationFromDirection(tdx, tdz) + + table.insert(snapshot.tools, { + id = obj.id, + name = obj:getName(), + x = tx, + y = ty, + z = tz, + angle = tAngle + }) + end + end + end + + self.snapshots[employee.id] = snapshot + CustomUtils:info("[SnapshotManager] Captured snapshot for employee %s: vehicle '%s' at (%.1f, %.1f, %.1f), %d tools", + employee.name, snapshot.vehicle.name, vx, vy, vz, #snapshot.tools) +end + +---Returns the snapshot for a given employee, or nil +---@param employeeId number +---@return table|nil +function VehicleSnapshotManager:getSnapshot(employeeId) + return self.snapshots[employeeId] +end + +---Clears the snapshot for a given employee +---@param employeeId number +function VehicleSnapshotManager:clearSnapshot(employeeId) + if self.snapshots[employeeId] then + CustomUtils:debug("[SnapshotManager] Cleared snapshot for employee %d", employeeId) + self.snapshots[employeeId] = nil + end +end + +---Restores all tools from a snapshot to their original positions +---@param snapshot table +function VehicleSnapshotManager:restoreTools(snapshot) + if not snapshot or not snapshot.tools then return end + + for _, toolData in ipairs(snapshot.tools) do + local tool = g_employeeManager:getVehicleById(toolData.id) + if tool and tool.rootNode then + -- Teleport tool to its original position + if tool.removeFromPhysics then + tool:removeFromPhysics() + end + + local rx, ry, rz = 0, toolData.angle, 0 + setWorldTranslation(tool.rootNode, toolData.x, toolData.y, toolData.z) + setRotation(tool.rootNode, rx, ry, rz) + + if tool.addToPhysics then + tool:addToPhysics() + end + + CustomUtils:info("[SnapshotManager] Restored tool '%s' to (%.1f, %.1f, %.1f)", + toolData.name, toolData.x, toolData.y, toolData.z) + else + CustomUtils:warning("[SnapshotManager] Tool '%s' (ID: %d) no longer exists, skipping restore", + toolData.name or "unknown", toolData.id or 0) + end + end +end + +---Serializes all snapshots to a plain table for persistence +---@return table +function VehicleSnapshotManager:toTable() + local data = {} + for empId, snapshot in pairs(self.snapshots) do + data[tostring(empId)] = { + employeeId = snapshot.employeeId, + timestamp = snapshot.timestamp, + vehicle = snapshot.vehicle, + tools = snapshot.tools + } + end + return data +end + +---Loads snapshots from a previously serialized table +---@param data table +function VehicleSnapshotManager:fromTable(data) + self.snapshots = {} + if not data then return end + + for empIdStr, snapshot in pairs(data) do + local empId = tonumber(empIdStr) + if empId and snapshot.vehicle then + self.snapshots[empId] = { + employeeId = snapshot.employeeId or empId, + timestamp = snapshot.timestamp or 0, + vehicle = snapshot.vehicle, + tools = snapshot.tools or {} + } + end + end + + local count = 0 + for _ in pairs(self.snapshots) do count = count + 1 end + if count > 0 then + CustomUtils:info("[SnapshotManager] Loaded %d snapshots from persistence", count) + end +end diff --git a/scripts/managers/employeemanager.lua b/scripts/managers/employeemanager.lua index 67f00bb..dae04c8 100644 --- a/scripts/managers/employeemanager.lua +++ b/scripts/managers/employeemanager.lua @@ -85,7 +85,7 @@ function EmployeeManager:update(dt) CustomUtils:info("[EmployeeManager] %s stopped working (unpaid)", employee.name) end elseif employee.isHired and employee.currentJob ~= nil then - local effectiveDt = math.min(dt, 200) + local effectiveDt = math.min(dt, 200) -- Cap at 200ms: prevent wage inflation during time acceleration local hoursWorked = employee:updateWorkTime(effectiveDt) if hoursWorked > 0 then local fatigueMult = employee:getFatigueMultiplier() @@ -135,11 +135,12 @@ function EmployeeManager:update(dt) end end + -- Task-queue-based autonomous restart (no targetCrop, uses taskQueue instead) if employee.isHired and not employee.isUnpaid and employee.isAutonomous - and employee.currentJob == nil - and (employee.targetCrop == nil or employee.targetCrop == "") - and employee.taskQueue and #employee.taskQueue > 0 - and employee.targetFieldId ~= nil then + and employee.currentJob == nil + and (employee.targetCrop == nil or employee.targetCrop == "") + and employee.taskQueue and #employee.taskQueue > 0 + and employee.targetFieldId ~= nil then if not employee:canWork() then -- skip: on break or exhausted @@ -164,10 +165,10 @@ function EmployeeManager:update(dt) end end + -- Diagnostic: autonomous employees that SHOULD be working but aren't if employee.isHired and employee.isAutonomous and employee.currentJob == nil - and employee.taskQueue and #employee.taskQueue > 0 then + and employee.taskQueue and #employee.taskQueue > 0 then employee.diagTimer = (employee.diagTimer or 0) + dt - if employee.diagTimer > 60000 then employee.diagTimer = 0 local hour = (g_currentMission and g_currentMission.environment) and g_currentMission.environment.currentHour or 0 @@ -287,6 +288,7 @@ function EmployeeManager:onHourChanged() local currentHour = g_currentMission.environment.currentHour or 0 for _, employee in ipairs(self.employees) do + -- if not employee.isHired then if employee.isOnBreak then if employee.breakEndTime ~= nil and g_currentMission.time >= employee.breakEndTime then employee.isOnBreak = false @@ -320,6 +322,7 @@ function EmployeeManager:onHourChanged() end end + -- Hourly state dump for ALL hired employees if employee.isHired then local jobDesc = "NONE" if employee.currentJob then @@ -1111,12 +1114,13 @@ function EmployeeManager:loadFromXMLFile(xmlFile, key) i = i + 1 end + -- Compute max ID from loaded employees as safety fallback local maxId = 0 for _, emp in ipairs(self.employees) do if emp.id > maxId then maxId = emp.id end end self.nextEmployeeId = Utils.getNoNil(getXMLInt(xmlFile, key .. ".poolState#nextEmployeeId"), maxId + 1) - + -- Ensure nextEmployeeId is always above any loaded ID (guards against stale save data) if self.nextEmployeeId <= maxId then self.nextEmployeeId = maxId + 1 end diff --git a/scripts/managers/jobmanager.lua b/scripts/managers/jobmanager.lua index bbb39dc..217ff81 100644 --- a/scripts/managers/jobmanager.lua +++ b/scripts/managers/jobmanager.lua @@ -82,6 +82,11 @@ function JobManager:startFieldWork(employee, fieldId, workType) workType = workType } + -- Capture vehicle+tools origin snapshot (only on first task, not mid-workflow transitions) + if g_snapshotManager and not g_snapshotManager:getSnapshot(employee.id) then + g_snapshotManager:captureSnapshot(employee, vehicle) + end + CustomUtils:debug("[JobManager] Preparing vehicle %s (ID: %d) for job...", vehicle:getName(), vehicle.id) if vehicle.startMotor and not vehicle:getIsMotorStarted() then @@ -102,6 +107,7 @@ function JobManager:startFieldWork(employee, fieldId, workType) self:ensureEquipment(vehicle, workType, function(result, data) if result == true then + -- Tier 1 (already attached) or close-proximity native attach succeeded CustomUtils:info("[JobManager] Equipment ensured. Deferring start via EQUIPMENT_READY state...") employee.currentJob = { type = "EQUIPMENT_READY", @@ -111,6 +117,7 @@ function JobManager:startFieldWork(employee, fieldId, workType) readyTime = g_currentMission.time + 1000 } elseif result == "DRIVE_TO_TOOL" then + -- Tier 2: owned tool found but needs driving to CustomUtils:info("[JobManager] Owned tool found. Starting drive-to-tool for %s...", employee.name) self:startDriveToTool(employee, vehicle, data.tool, fieldId, workType) else @@ -138,6 +145,7 @@ function JobManager:startFieldWorkJob(employee, vehicle, fieldId, workType) local x, z = field:getCenterOfFieldWorldPosition() + -- Field state snapshot for debugging if field.fieldState == nil then field.fieldState = FieldState.new() end @@ -159,6 +167,10 @@ function JobManager:startFieldWorkJob(employee, vehicle, fieldId, workType) CustomUtils:info(" Stubble: %d | Weed: %d | Roller: %d", fs.stubbleShredLevel, fs.weedState, fs.rollerLevel) + -- Never use isDirectStart for automated workflows. + -- isDirectStart=true skips the drive-to task and starts from the vehicle's + -- current edge position, which causes the AI to terminate instantly when it + -- can't generate valid work rows from there. aiJob.isDirectStart = false aiJob:applyCurrentState(vehicle, g_currentMission, farmId, false) @@ -199,6 +211,7 @@ function JobManager:detachAllImplements(vehicle) if not vehicle or not vehicle.getAttachedImplements then return end local attachedImplements = vehicle:getAttachedImplements() + -- Iterate in reverse to avoid index shifting for i = #attachedImplements, 1, -1 do local implement = attachedImplements[i] if implement and implement.object then @@ -213,6 +226,7 @@ end ---Checks if vehicle has required tool via three-tier search: attached → owned → rental ---Callback receives (true) for ready, ("DRIVE_TO_TOOL", data) for owned tool needing drive, or (false) for failure function JobManager:ensureEquipment(vehicle, workType, callback) + -- Defensive cleanup: return any lingering rental before equipping for new task local employee = g_employeeManager:getEmployeeByVehicle(vehicle) if employee and employee.temporaryRental then CustomUtils:warning("[JobManager] ensureEquipment: cleaning up lingering rental for %s", employee.name) @@ -227,6 +241,7 @@ function JobManager:ensureEquipment(vehicle, workType, callback) local attachedImplements = vehicle:getAttachedImplements() + -- Tier 1: Check if the currently attached tool already supports this work type for _, implement in ipairs(attachedImplements) do local obj = implement.object if obj ~= nil then @@ -239,11 +254,13 @@ function JobManager:ensureEquipment(vehicle, workType, callback) end end + -- Detach any existing implements before attaching a new one (1 tool at a time!) if #attachedImplements > 0 then CustomUtils:info("[JobManager] Detaching existing implements before attaching tool for %s", workType) self:detachAllImplements(vehicle) end + -- Tier 2: Search ALL owned vehicles on the farm for a matching tool local ownedTool = self:findOwnedTool(categoryName, vehicle) if ownedTool then local tx, _, tz = getWorldTranslation(ownedTool.rootNode) @@ -251,10 +268,12 @@ function JobManager:ensureEquipment(vehicle, workType, callback) local dist = MathUtil.vector2Length(vx - tx, vz - tz) if dist < 10 then + -- Tool is close — try native attach directly CustomUtils:info("[JobManager] Tier 2: Owned tool %s is nearby (%.1fm). Attempting native attach...", ownedTool:getName(), dist) self:attemptNativeAttach(vehicle, ownedTool, callback) else + -- Tool is far — employee needs to drive to it CustomUtils:info("[JobManager] Tier 2: Found owned tool %s at %.1fm away. Employee must drive to it.", ownedTool:getName(), dist) callback("DRIVE_TO_TOOL", { tool = ownedTool, toolX = tx, toolZ = tz }) @@ -262,6 +281,7 @@ function JobManager:ensureEquipment(vehicle, workType, callback) return end + -- Tier 3: Rent from store as last resort CustomUtils:info("[JobManager] Tier 3: No owned tool found for %s. Renting equipment...", workType) local storeItem = self:findSuitableTool(categoryName) if storeItem then @@ -328,6 +348,7 @@ end function JobManager:positionToolNearVehicle(tool, vehicle) local vx, vy, vz = getWorldTranslation(vehicle.rootNode) + -- Find the vehicle's rear attacher joint world position local rearJointX, rearJointY, rearJointZ = vx, vy, vz if vehicle.spec_attacherJoints then local joints = vehicle:getAttacherJoints() @@ -344,6 +365,7 @@ function JobManager:positionToolNearVehicle(tool, vehicle) end end + -- Calculate offset from tool's center to its input attacher joint local toolJointOffsetX, toolJointOffsetZ = 0, 0 local inputJoints = tool.getInputAttacherJoints and tool:getInputAttacherJoints() if inputJoints and #inputJoints > 0 then @@ -354,13 +376,17 @@ function JobManager:positionToolNearVehicle(tool, vehicle) end end + -- Get vehicle's backward direction to align tool behind it local backDirX, _, backDirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, -1) local backRightX, _, backRightZ = localDirectionToWorld(vehicle.rootNode, 1, 0, 0) + -- Target position: rear joint position, offset by tool's joint-to-center distance + -- Tool needs to be placed so its input joint aligns with the vehicle's rear joint local targetX = rearJointX - backRightX * toolJointOffsetX - backDirX * toolJointOffsetZ local targetY = rearJointY + 0.3 local targetZ = rearJointZ - backRightZ * toolJointOffsetX - backDirZ * toolJointOffsetZ + -- Get vehicle's Y rotation so tool faces the same direction local vDirX, _, vDirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1) local vehicleRotY = MathUtil.getYRotationFromDirection(vDirX, vDirZ) @@ -383,6 +409,7 @@ end ---@param tool table ---@param callback function function JobManager:attemptNativeAttach(vehicle, tool, callback) + -- Try scanning for attachable using base game system if vehicle.spec_attacherJoints then AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ or 100, @@ -397,6 +424,7 @@ function JobManager:attemptNativeAttach(vehicle, tool, callback) end end + -- Fallback: position and direct attach (legacy method) CustomUtils:info("[JobManager] Native attach scan missed. Falling back to position + attachImplement for %s", tool:getName()) self:positionToolNearVehicle(tool, vehicle) @@ -415,25 +443,28 @@ end function JobManager:calculateApproachPosition(vehicle, tool) local tx, _, tz = getWorldTranslation(tool.rootNode) + -- Try to get orientation from the tool's input attacher joint local inputJoints = tool.getInputAttacherJoints and tool:getInputAttacherJoints() if inputJoints and #inputJoints > 0 then local joint = inputJoints[1] if joint.node then + -- Get the joint's world-space direction (forward = Z axis of the joint) local jdx, _, jdz = localDirectionToWorld(joint.node, 0, 0, 1) local jointLen = math.sqrt(jdx * jdx + jdz * jdz) if jointLen > 0.001 then jdx = jdx / jointLen jdz = jdz / jointLen - + -- Approach point: 8m in front of the joint along its forward direction local approachX = tx + jdx * 8 local approachZ = tz + jdz * 8 - + -- Angle: vehicle should face TOWARD the tool (opposite direction) local approachAngle = MathUtil.getYRotationFromDirection(-jdx, -jdz) return approachX, approachZ, approachAngle end end end + -- Fallback: approach from the vehicle's current direction local vx, _, vz = getWorldTranslation(vehicle.rootNode) local dx = tx - vx local dz = tz - vz @@ -444,7 +475,7 @@ function JobManager:calculateApproachPosition(vehicle, tool) else dx, dz = 0, 1 end - + -- Position 8m before the tool, facing toward it local approachX = tx - dx * 8 local approachZ = tz - dz * 8 local approachAngle = MathUtil.getYRotationFromDirection(dx, dz) @@ -498,6 +529,7 @@ end ---@param fieldId number ---@param workType string function JobManager:tryDirectAttachOrTeleport(employee, vehicle, tool, fieldId, workType) + -- Check if already in attach range if vehicle.spec_attacherJoints then AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ or 100, @@ -518,6 +550,7 @@ function JobManager:tryDirectAttachOrTeleport(employee, vehicle, tool, fieldId, end end + -- Not in range — teleport tool to vehicle's rear CustomUtils:warning("[JobManager] %s: Tool not in attach range. Teleporting tool to vehicle rear.", employee.name) self:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) end @@ -531,12 +564,14 @@ end function JobManager:fallbackTeleportAttach(employee, vehicle, tool, fieldId, workType) CustomUtils:warning("[JobManager] FALLBACK: Teleporting tool %s to vehicle %s", tool:getName(), vehicle:getName()) + -- Remove tool from physics before repositioning to prevent collision forces if tool.removeFromPhysics then tool:removeFromPhysics() end self:positionToolNearVehicle(tool, vehicle) + -- Re-enable tool physics before attaching if tool.addToPhysics then tool:addToPhysics() end @@ -546,6 +581,7 @@ function JobManager:fallbackTeleportAttach(employee, vehicle, tool, fieldId, wor vehicle:attachImplement(tool, tJointIdx, vJointIdx) end + -- Stabilize vehicle to prevent flip/roll from teleport collision self:stabilizeVehicle(vehicle) employee.currentJob = { @@ -568,10 +604,12 @@ function JobManager:stabilizeVehicle(vehicle) local vx, _, vz = getWorldTranslation(vehicle.rootNode) local _, yRot, _ = getWorldRotation(vehicle.rootNode) + -- Remove from physics, reposition upright, re-add if vehicle.removeFromPhysics then vehicle:removeFromPhysics() end + -- setRelativePosition(x, offsetY, z, yRot) auto-gets terrain height, resets pitch/roll to 0 if vehicle.setRelativePosition then vehicle:setRelativePosition(vx, 0.5, vz, yRot) CustomUtils:debug("[JobManager] Stabilized vehicle %s at (%.1f, %.1f) heading %.1f°", vehicle:getName(), vx, vz, math.deg(yRot)) @@ -589,6 +627,8 @@ end ---@param nextFieldId number ---@param nextWorkType string function JobManager:startReturnTool(employee, vehicle, tool, nextFieldId, nextWorkType) + -- Since the tool is attached, we can just detach it in place after the fieldwork is done. + -- The vehicle is already near the field edge. Simply detach and proceed to next task. CustomUtils:info("[JobManager] %s: Detaching tool %s at field edge before switching to %s", employee.name, tool:getName(), nextWorkType) self:detachAllImplements(vehicle) @@ -785,6 +825,11 @@ function JobManager:stopJob(employee) employee.currentJob = nil + -- Clear origin snapshot on manual stop + if g_snapshotManager then + g_snapshotManager:clearSnapshot(employee.id) + end + if employee.temporaryRental then g_employeeManager:returnRentedEquipment(employee) end @@ -794,6 +839,7 @@ function JobManager:stopJob(employee) end function JobManager:handleFieldworkCompletion(employee) + -- Return rented equipment from completed task if employee.temporaryRental then g_employeeManager:returnRentedEquipment(employee) end @@ -802,6 +848,7 @@ function JobManager:handleFieldworkCompletion(employee) g_employeeManager:onJobCompleted(employee) end + -- Task-queue chaining: check for next task local queue = employee.taskQueue or {} local currentIdx = employee.currentTaskIndex or 1 local nextIdx = currentIdx + 1 @@ -810,10 +857,12 @@ function JobManager:handleFieldworkCompletion(employee) local nextTask = queue[nextIdx] local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + -- Check if the current tool works for the next task local nextCategory = JobManager.WORK_TYPE_TO_CATEGORY[nextTask] local currentToolOk = vehicle and self:hasCorrectTool(vehicle, nextCategory) if not currentToolOk and vehicle then + -- Need to swap tools — return the current one first local currentTool = self:getFirstAttachedImplement(vehicle) if currentTool then CustomUtils:info("[JobManager] %s needs tool swap: current tool won't work for %s. Returning tool first.", @@ -824,6 +873,7 @@ function JobManager:handleFieldworkCompletion(employee) end end + -- Tool is already correct or no tool to return — advance directly employee.currentTaskIndex = nextIdx CustomUtils:info("[JobManager] %s advancing to task %d/%d: %s", employee.name, nextIdx, #queue, nextTask) @@ -832,30 +882,64 @@ function JobManager:handleFieldworkCompletion(employee) return end + -- All tasks complete (or no queue) — workflow finished if #queue > 0 then CustomUtils:info("[JobManager] %s completed all %d tasks in workflow", employee.name, #queue) employee.currentTaskIndex = 1 - employee.isAutonomous = false + employee.isAutonomous = false -- Queue exhausted, stop autonomous mode local msg = string.format(g_i18n:getText("em_workflow_queue_complete"), employee.name) g_currentMission:showBlinkingWarning(msg, 5000) end + -- Attempt return-to-origin via snapshot (preferred) or parking spot (fallback) + local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) + local snapshot = g_snapshotManager and g_snapshotManager:getSnapshot(employee.id) + + if snapshot and vehicle and vehicle.rootNode then + -- Detach all tools, then restore them to their original positions + self:detachAllImplements(vehicle) + g_snapshotManager:restoreTools(snapshot) + + -- Create a spot-like object from the snapshot vehicle data for GOTO + local snapshotSpot = { + id = 0, + name = "origin", + x = snapshot.vehicle.x, + z = snapshot.vehicle.z, + angle = snapshot.vehicle.angle + } + + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local dx = vx - snapshotSpot.x + local dz = vz - snapshotSpot.z + local dist = math.sqrt(dx * dx + dz * dz) + + if dist > 20 then + CustomUtils:info("[JobManager] %s returning to origin (%.0fm away)", employee.name, dist) + self:startReturnToParking(employee, vehicle, snapshotSpot) + return + else + -- Already close to origin, just clear and finish + g_snapshotManager:clearSnapshot(employee.id) + employee.currentJob = nil + return + end + end + + -- Fallback: parking spot return (no snapshot available) if g_parkingManager and employee.assignedVehicleId then local spot = g_parkingManager:getSpotForVehicle(employee.assignedVehicleId) - if spot then - local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) - if vehicle and vehicle.rootNode then - local vx, _, vz = getWorldTranslation(vehicle.rootNode) - local dx = vx - spot.x - local dz = vz - spot.z - local dist = math.sqrt(dx * dx + dz * dz) - - if dist > 20 then - CustomUtils:info("[JobManager] %s returning to parking '%s' (%.0fm away)", - employee.name, spot.name, dist) - self:startReturnToParking(employee, vehicle, spot) - return - end + if spot and vehicle and vehicle.rootNode then + local vx, _, vz = getWorldTranslation(vehicle.rootNode) + local dx = vx - spot.x + local dz = vz - spot.z + local dist = math.sqrt(dx * dx + dz * dz) + + if dist > 20 then + CustomUtils:info("[JobManager] %s returning to parking '%s' (%.0fm away)", + employee.name, spot.name, dist) + self:startReturnToParking(employee, vehicle, spot) + return end end end @@ -902,6 +986,11 @@ function JobManager:handleParkingArrival(employee) end end + -- Clear origin snapshot now that we've arrived + if g_snapshotManager then + g_snapshotManager:clearSnapshot(employee.id) + end + employee.currentJob = nil if g_employeeManager then g_employeeManager:onJobCompleted(employee) @@ -944,6 +1033,7 @@ function JobManager:update(dt) readyTime = g_currentMission.time + 500 } elseif employee.currentJob.type == "DRIVING_TO_TOOL" then + -- GOTO completed — try direct attach or teleport (no alignment step) CustomUtils:info("[JobManager] %s arrived near tool. Attempting attach or teleport...", employee.name) local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) local tool = g_employeeManager:getVehicleById(employee.currentJob.targetToolId) @@ -954,6 +1044,7 @@ function JobManager:update(dt) employee.currentJob = nil end elseif employee.currentJob.type == "RETURNING_TOOL" then + -- GOTO to return position completed — detach tool and start next task CustomUtils:info("[JobManager] %s arrived at drop-off point. Detaching tool...", employee.name) local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) if vehicle then @@ -966,6 +1057,7 @@ function JobManager:update(dt) elseif employee.currentJob.type == "RETURN_TO_PARKING" then self:handleParkingArrival(employee) elseif employee.currentJob.type == "FIELDWORK" then + -- Rapid-finish guard: detect instant completions (field already done) local elapsed = g_currentMission.time - (employee.currentJob.startTime or 0) if elapsed < 2000 then employee.rapidFailCount = (employee.rapidFailCount or 0) + 1 @@ -1059,6 +1151,7 @@ function JobManager:update(dt) employee.currentJob = nil end else + -- Debug logging for EQUIPMENT_READY wait employee.debugTimer = (employee.debugTimer or 0) + dt if employee.debugTimer > 5000 then employee.debugTimer = 0 @@ -1067,6 +1160,7 @@ function JobManager:update(dt) end end elseif employee.currentJob and employee.currentJob.type == "ATTACHING_TOOL" then + -- Wait for attachment animation to complete local vehicle = g_employeeManager:getVehicleById(employee.assignedVehicleId) if vehicle then local allDone = true diff --git a/scripts/modcontroller.lua b/scripts/modcontroller.lua index bdc4927..f7c7893 100644 --- a/scripts/modcontroller.lua +++ b/scripts/modcontroller.lua @@ -29,9 +29,10 @@ function ModController:loadMap(name, itemSystem, missionInfo, missionDynamicInfo g_employeeManager = EmployeeManager:new(g_currentMission) g_parkingManager = ParkingManager:new() + g_snapshotManager = VehicleSnapshotManager:new() g_persistenceManager = PersistenceManager:new() - g_persistenceManager:addStrategy(DBAPIPersistence:new()) + g_persistenceManager:addStrategy(SILODBPersistence:new()) g_persistenceManager:addStrategy(XMLPersistence:new()) g_persistenceManager:selectStrategy() @@ -69,7 +70,7 @@ function ModController:loadMap(name, itemSystem, missionInfo, missionDynamicInfo g_employeeManager:generateInitialPool(10) end else - CustomUtils:info("[ModController] Load deferred — DBAPI may not be ready yet") + CustomUtils:info("[ModController] Load deferred — SILODB may not be ready yet") end if rawget(_G, 'g_modGui') ~= nil then @@ -86,14 +87,14 @@ end function ModController:saveEmployees() if g_persistenceManager == nil or g_employeeManager == nil then return end - g_persistenceManager:save(g_employeeManager, g_parkingManager) + g_persistenceManager:save(g_employeeManager, g_parkingManager, g_snapshotManager) end function ModController:loadEmployees(savegameDir) if g_persistenceManager == nil or g_employeeManager == nil then return false end - return g_persistenceManager:load(g_employeeManager, g_parkingManager) + return g_persistenceManager:load(g_employeeManager, g_parkingManager, g_snapshotManager) end function ModController:deleteMap() @@ -107,6 +108,7 @@ function ModController:deleteMap() g_employeeManager = nil end g_parkingManager = nil + g_snapshotManager = nil g_persistenceManager = nil end @@ -114,7 +116,7 @@ function ModController:update(dt) if not self.deferredLoadAttempted and not self.dataLoaded then self.deferredLoadAttempted = true g_persistenceManager:selectStrategy() - self.dataLoaded = g_persistenceManager:load(g_employeeManager, g_parkingManager) + self.dataLoaded = g_persistenceManager:load(g_employeeManager, g_parkingManager, g_snapshotManager) if not self.dataLoaded or #g_employeeManager.employees == 0 then CustomUtils:info("[ModController] Deferred load: no data, generating pool") g_employeeManager:generateInitialPool(10) diff --git a/scripts/persistence/PersistenceManager.lua b/scripts/persistence/PersistenceManager.lua index 1d9b8e2..0eb7566 100644 --- a/scripts/persistence/PersistenceManager.lua +++ b/scripts/persistence/PersistenceManager.lua @@ -29,7 +29,7 @@ function PersistenceManager:selectStrategy() return nil end -function PersistenceManager:save(employeeManager, parkingManager) +function PersistenceManager:save(employeeManager, parkingManager, snapshotManager) if employeeManager == nil then CustomUtils:warning("[PersistenceManager] save() called with nil employeeManager") return false @@ -49,7 +49,7 @@ function PersistenceManager:save(employeeManager, parkingManager) CustomUtils:info("[PersistenceManager] Saving with %s...", strategy:getName()) local ok, result = pcall(function() - return strategy:save(employeeManager, parkingManager) + return strategy:save(employeeManager, parkingManager, snapshotManager) end) if ok and result then @@ -66,7 +66,7 @@ function PersistenceManager:save(employeeManager, parkingManager) return false end -function PersistenceManager:load(employeeManager, parkingManager) +function PersistenceManager:load(employeeManager, parkingManager, snapshotManager) if employeeManager == nil then CustomUtils:warning("[PersistenceManager] load() called with nil employeeManager") return false @@ -86,7 +86,7 @@ function PersistenceManager:load(employeeManager, parkingManager) CustomUtils:info("[PersistenceManager] Loading with %s...", strategy:getName()) local ok, result = pcall(function() - return strategy:load(employeeManager, parkingManager) + return strategy:load(employeeManager, parkingManager, snapshotManager) end) if ok and result then diff --git a/scripts/persistence/PersistenceStrategy.lua b/scripts/persistence/PersistenceStrategy.lua index 3fa6e4e..193e7fc 100644 --- a/scripts/persistence/PersistenceStrategy.lua +++ b/scripts/persistence/PersistenceStrategy.lua @@ -13,12 +13,12 @@ function PersistenceStrategy:isAvailable() return false end -function PersistenceStrategy:save(employeeManager, parkingManager) +function PersistenceStrategy:save(employeeManager, parkingManager, snapshotManager) CustomUtils:warning("[PersistenceStrategy] save() not implemented for %s", self:getName()) return false end -function PersistenceStrategy:load(employeeManager, parkingManager) +function PersistenceStrategy:load(employeeManager, parkingManager, snapshotManager) CustomUtils:warning("[PersistenceStrategy] load() not implemented for %s", self:getName()) return false end diff --git a/scripts/persistence/DBAPIPersistence.lua b/scripts/persistence/SILODBPersistence.lua similarity index 60% rename from scripts/persistence/DBAPIPersistence.lua rename to scripts/persistence/SILODBPersistence.lua index 88af850..3d6c155 100644 --- a/scripts/persistence/DBAPIPersistence.lua +++ b/scripts/persistence/SILODBPersistence.lua @@ -1,26 +1,26 @@ -DBAPIPersistence = {} -DBAPIPersistence.__index = DBAPIPersistence -DBAPIPersistence.NAMESPACE = "FS25_EmployeeManager" -DBAPIPersistence.CURRENT_VERSION = 1 +SILODBPersistence = {} +SILODBPersistence.__index = SILODBPersistence +SILODBPersistence.NAMESPACE = "FS25_EmployeeManager" +SILODBPersistence.CURRENT_VERSION = 1 -function DBAPIPersistence:new() - local self = setmetatable({}, DBAPIPersistence) +function SILODBPersistence:new() + local self = setmetatable({}, SILODBPersistence) self.db = nil return self end -function DBAPIPersistence:getName() - return "DBAPI" +function SILODBPersistence:getName() + return "SILODB" end -function DBAPIPersistence:getAPI() +function SILODBPersistence:getAPI() if g_globalMods then - return g_globalMods["FS25_DBAPI"] + return g_globalMods["FS25_SILODB"] end return nil end -function DBAPIPersistence:getDb() +function SILODBPersistence:getDb() if self.db then return self.db end local api = self:getAPI() @@ -36,22 +36,25 @@ function DBAPIPersistence:getDb() return nil end -function DBAPIPersistence:initModels(db) +function SILODBPersistence:initModels(db) + -- Define Employee model local _, err = db:define("Employee", { fields = { data = { type = "table", required = true } } }) - if err then CustomUtils:error("[DBAPIPersistence] Error defining Employee model: %s", tostring(err)) end + if err then CustomUtils:error("[SILODBPersistence] Error defining Employee model: %s", tostring(err)) end + -- Define Parking model _, err = db:define("Parking", { fields = { spots = { type = "table", required = true }, nextSpotId = { type = "number", default = 1 } } }) - if err then CustomUtils:error("[DBAPIPersistence] Error defining Parking model: %s", tostring(err)) end + if err then CustomUtils:error("[SILODBPersistence] Error defining Parking model: %s", tostring(err)) end + -- Define Settings model _, err = db:define("Settings", { fields = { version = { type = "number", default = 1 }, @@ -60,23 +63,35 @@ function DBAPIPersistence:initModels(db) lastPaymentPeriod = { type = "number", default = 0 } } }) - if err then CustomUtils:error("[DBAPIPersistence] Error defining Settings model: %s", tostring(err)) end + if err then CustomUtils:error("[SILODBPersistence] Error defining Settings model: %s", tostring(err)) end + -- Define VehicleSnapshot model + _, err = db:define("VehicleSnapshot", { + fields = { + snapshots = { type = "table", required = true } + } + }) + if err then CustomUtils:error("[SILODBPersistence] Error defining VehicleSnapshot model: %s", tostring(err)) end + + -- Define FieldConfig model _, err = db:define("FieldConfig", { fields = { fieldId = { type = "number", required = true }, cropName = { type = "string", required = true } } }) - if err then CustomUtils:error("[DBAPIPersistence] Error defining FieldConfig model: %s", tostring(err)) end + if err then CustomUtils:error("[SILODBPersistence] Error defining FieldConfig model: %s", tostring(err)) end end -function DBAPIPersistence:migrate(db) +function SILODBPersistence:migrate(db) local settings, _ = db:find("Settings") local version = settings and settings.version or 0 if version < self.CURRENT_VERSION then - CustomUtils:info("[DBAPIPersistence] Migrating database from version %d to %d", version, self.CURRENT_VERSION) + CustomUtils:info("[SILODBPersistence] Migrating database from version %d to %d", version, self.CURRENT_VERSION) + + -- Add migration logic here when CURRENT_VERSION increases + -- Example: if version < 1 then ... end if settings then db:update("Settings", settings.id, { version = self.CURRENT_VERSION }) @@ -86,7 +101,7 @@ function DBAPIPersistence:migrate(db) end end -function DBAPIPersistence:isAvailable() +function SILODBPersistence:isAvailable() if g_currentMission and g_currentMission.missionDynamicInfo and g_currentMission.missionDynamicInfo.isMultiplayer then return false end @@ -97,20 +112,21 @@ function DBAPIPersistence:isAvailable() end if not api.hasORM or not api.hasORM() then - CustomUtils:warning("[DBAPIPersistence] DBAPI version too old (no ORM support)") + CustomUtils:warning("[SILODBPersistence] SILODB version too old (no ORM support)") return false end return true end -function DBAPIPersistence:save(employeeManager, parkingManager) +function SILODBPersistence:save(employeeManager, parkingManager, snapshotManager) local db = self:getDb() if not db then - CustomUtils:error("[DBAPIPersistence] DBAPI ORM not available for save") + CustomUtils:error("[SILODBPersistence] SILODB ORM not available for save") return false end + -- 1. Save Settings (singleton) local settings = { nextEmployeeId = employeeManager.nextEmployeeId or 1, lastPoolRefreshDay = employeeManager.lastPoolRefreshDay or 0, @@ -123,6 +139,7 @@ function DBAPIPersistence:save(employeeManager, parkingManager) db:create("Settings", settings) end + -- 2. Save Parking (singleton) if parkingManager then local pData = { spots = parkingManager.spots or {}, @@ -136,6 +153,8 @@ function DBAPIPersistence:save(employeeManager, parkingManager) end end + -- 3. Save Employees + -- Clear existing records to avoid duplicates and bloat local existing, _ = db:findAll("Employee") if existing then for _, rec in ipairs(existing) do @@ -149,10 +168,11 @@ function DBAPIPersistence:save(employeeManager, parkingManager) if not err then count = count + 1 else - CustomUtils:error("[DBAPIPersistence] Failed to save employee %d: %s", e.id, tostring(err)) + CustomUtils:error("[SILODBPersistence] Failed to save employee %d: %s", e.id, tostring(err)) end end + -- 4. Save Field Configs local existingFC, _ = db:findAll("FieldConfig") if existingFC then for _, rec in ipairs(existingFC) do @@ -168,17 +188,29 @@ function DBAPIPersistence:save(employeeManager, parkingManager) end end - CustomUtils:info("[DBAPIPersistence] Saved %d employees and %d field configs via DBAPI ORM", count, fcCount) + -- 5. Save Vehicle Snapshots + if snapshotManager then + local snapData = snapshotManager:toTable() + local snapRec, _ = db:find("VehicleSnapshot") + if snapRec then + db:update("VehicleSnapshot", snapRec.id, { snapshots = snapData }) + else + db:create("VehicleSnapshot", { snapshots = snapData }) + end + end + + CustomUtils:info("[SILODBPersistence] Saved %d employees and %d field configs via SILODB ORM", count, fcCount) return true end -function DBAPIPersistence:load(employeeManager, parkingManager) +function SILODBPersistence:load(employeeManager, parkingManager, snapshotManager) local db = self:getDb() if not db then - CustomUtils:error("[DBAPIPersistence] DBAPI ORM not available for load") + CustomUtils:error("[SILODBPersistence] SILODB ORM not available for load") return false end + -- 1. Load Settings local sRec, _ = db:find("Settings") if sRec then employeeManager.nextEmployeeId = sRec.nextEmployeeId or 1 @@ -186,15 +218,17 @@ function DBAPIPersistence:load(employeeManager, parkingManager) employeeManager.lastPaymentPeriod = sRec.lastPaymentPeriod or 0 end + -- 2. Load Parking if parkingManager then local pRec, _ = db:find("Parking") if pRec then parkingManager.spots = pRec.spots or {} parkingManager.nextSpotId = pRec.nextSpotId or 1 - CustomUtils:debug("[DBAPIPersistence] Loaded %d parking spots", #parkingManager.spots) + CustomUtils:debug("[SILODBPersistence] Loaded %d parking spots", #parkingManager.spots) end end + -- 3. Load Employees local emps, _ = db:findAll("Employee") if emps and #emps > 0 then employeeManager.employees = {} @@ -204,6 +238,7 @@ function DBAPIPersistence:load(employeeManager, parkingManager) if rec.data then local emp = Employee.fromTable(rec.data) if emp then + -- Re-assign vehicle if needed if emp.assignedVehicleId and emp.assignedVehicleId ~= 0 then local vehicle = employeeManager:getVehicleById(emp.assignedVehicleId) if vehicle then @@ -217,27 +252,38 @@ function DBAPIPersistence:load(employeeManager, parkingManager) end end + -- Sync nextEmployeeId if needed if employeeManager.nextEmployeeId <= maxId then employeeManager.nextEmployeeId = maxId + 1 end - CustomUtils:info("[DBAPIPersistence] Loaded %d employees (%d hired) via DBAPI ORM", #employeeManager.employees, hiredCount) + CustomUtils:info("[SILODBPersistence] Loaded %d employees (%d hired) via SILODB ORM", #employeeManager.employees, hiredCount) else - CustomUtils:info("[DBAPIPersistence] No employee data found in DBAPI ORM") + CustomUtils:info("[SILODBPersistence] No employee data found in SILODB ORM") end + -- 4. Load Field Configs local fcs, _ = db:findAll("FieldConfig") employeeManager.fieldConfigs = {} if fcs then for _, rec in ipairs(fcs) do employeeManager.fieldConfigs[rec.fieldId] = { cropName = rec.cropName } end - CustomUtils:debug("[DBAPIPersistence] Loaded %d field configs", #fcs) + CustomUtils:debug("[SILODBPersistence] Loaded %d field configs", #fcs) + end + + -- 5. Load Vehicle Snapshots + if snapshotManager then + local snapRec, _ = db:find("VehicleSnapshot") + if snapRec and snapRec.snapshots then + snapshotManager:fromTable(snapRec.snapshots) + end end + -- Fill pool if empty or low local numToGenerate = 10 - #employeeManager.employees if numToGenerate > 0 then - CustomUtils:info("[DBAPIPersistence] Filling pool: generating %d candidates", numToGenerate) + CustomUtils:info("[SILODBPersistence] Filling pool: generating %d candidates", numToGenerate) employeeManager:generateInitialPool(numToGenerate) end diff --git a/scripts/persistence/XMLPersistence.lua b/scripts/persistence/XMLPersistence.lua index 59f969e..1fa5e6f 100644 --- a/scripts/persistence/XMLPersistence.lua +++ b/scripts/persistence/XMLPersistence.lua @@ -20,7 +20,7 @@ function XMLPersistence:getSavegameDirectory() return nil end -function XMLPersistence:save(employeeManager, parkingManager) +function XMLPersistence:save(employeeManager, parkingManager, snapshotManager) local dir = self:getSavegameDirectory() if dir == nil then CustomUtils:warning("[XMLPersistence] No savegame directory, cannot save") @@ -36,6 +36,7 @@ function XMLPersistence:save(employeeManager, parkingManager) employeeManager:saveToXMLFile(xmlFile, "employeeManager") + -- Save Field Configs local fieldKey = "employeeManager.fieldConfigs" local fIdx = 0 for fieldId, config in pairs(employeeManager.fieldConfigs or {}) do @@ -45,6 +46,33 @@ function XMLPersistence:save(employeeManager, parkingManager) fIdx = fIdx + 1 end + -- Save Vehicle Snapshots + if snapshotManager then + local snapData = snapshotManager:toTable() + local sIdx = 0 + for empIdStr, snap in pairs(snapData) do + local base = string.format("employeeManager.vehicleSnapshots.snapshot(%d)", sIdx) + setXMLString(xmlFile, base .. "#employeeId", empIdStr) + setXMLFloat(xmlFile, base .. ".vehicle#id", snap.vehicle.id or 0) + setXMLString(xmlFile, base .. ".vehicle#name", snap.vehicle.name or "") + setXMLFloat(xmlFile, base .. ".vehicle#x", snap.vehicle.x) + setXMLFloat(xmlFile, base .. ".vehicle#y", snap.vehicle.y) + setXMLFloat(xmlFile, base .. ".vehicle#z", snap.vehicle.z) + setXMLFloat(xmlFile, base .. ".vehicle#angle", snap.vehicle.angle or 0) + + for tIdx, tool in ipairs(snap.tools or {}) do + local tBase = string.format("%s.tool(%d)", base, tIdx - 1) + setXMLFloat(xmlFile, tBase .. "#id", tool.id or 0) + setXMLString(xmlFile, tBase .. "#name", tool.name or "") + setXMLFloat(xmlFile, tBase .. "#x", tool.x) + setXMLFloat(xmlFile, tBase .. "#y", tool.y) + setXMLFloat(xmlFile, tBase .. "#z", tool.z) + setXMLFloat(xmlFile, tBase .. "#angle", tool.angle or 0) + end + sIdx = sIdx + 1 + end + end + saveXMLFile(xmlFile) delete(xmlFile) @@ -56,7 +84,7 @@ function XMLPersistence:save(employeeManager, parkingManager) return true end -function XMLPersistence:load(employeeManager, parkingManager) +function XMLPersistence:load(employeeManager, parkingManager, snapshotManager) local dir = self:getSavegameDirectory() if dir == nil then CustomUtils:warning("[XMLPersistence] No savegame directory available for loading") @@ -78,6 +106,7 @@ function XMLPersistence:load(employeeManager, parkingManager) employeeManager:loadFromXMLFile(xmlFile, "employeeManager") + -- Load Field Configs employeeManager.fieldConfigs = {} local fieldKey = "employeeManager.fieldConfigs" local fIdx = 0 @@ -90,6 +119,51 @@ function XMLPersistence:load(employeeManager, parkingManager) fIdx = fIdx + 1 end + -- Load Vehicle Snapshots + if snapshotManager then + local snapData = {} + local sIdx = 0 + while true do + local base = string.format("employeeManager.vehicleSnapshots.snapshot(%d)", sIdx) + local empIdStr = getXMLString(xmlFile, base .. "#employeeId") + if not empIdStr then break end + + local snap = { + employeeId = tonumber(empIdStr), + timestamp = 0, + vehicle = { + id = getXMLFloat(xmlFile, base .. ".vehicle#id") or 0, + name = getXMLString(xmlFile, base .. ".vehicle#name") or "", + x = getXMLFloat(xmlFile, base .. ".vehicle#x") or 0, + y = getXMLFloat(xmlFile, base .. ".vehicle#y") or 0, + z = getXMLFloat(xmlFile, base .. ".vehicle#z") or 0, + angle = getXMLFloat(xmlFile, base .. ".vehicle#angle") or 0 + }, + tools = {} + } + + local tIdx = 0 + while true do + local tBase = string.format("%s.tool(%d)", base, tIdx) + local tId = getXMLFloat(xmlFile, tBase .. "#id") + if not tId then break end + table.insert(snap.tools, { + id = tId, + name = getXMLString(xmlFile, tBase .. "#name") or "", + x = getXMLFloat(xmlFile, tBase .. "#x") or 0, + y = getXMLFloat(xmlFile, tBase .. "#y") or 0, + z = getXMLFloat(xmlFile, tBase .. "#z") or 0, + angle = getXMLFloat(xmlFile, tBase .. "#angle") or 0 + }) + tIdx = tIdx + 1 + end + + snapData[empIdStr] = snap + sIdx = sIdx + 1 + end + snapshotManager:fromTable(snapData) + end + delete(xmlFile) local hiredCount = 0 diff --git a/scripts/types/employee.lua b/scripts/types/employee.lua index 0a57704..4a8d202 100644 --- a/scripts/types/employee.lua +++ b/scripts/types/employee.lua @@ -310,8 +310,10 @@ function Employee:writeStream(streamId, connection) streamWriteFloat32(streamId, self.milestoneWageMult or 1.0) + -- v4: autonomous state streamWriteBool(streamId, self.isAutonomous or false) + -- v3: personal info streamWriteInt8(streamId, self.age or 30) streamWriteString(streamId, self.nationality or "FR") streamWriteString(streamId, self.gender or "male") diff --git a/xml/gui/EMFieldFrame.xml b/xml/gui/EMFieldFrame.xml index 6b3c896..bd43846 100644 --- a/xml/gui/EMFieldFrame.xml +++ b/xml/gui/EMFieldFrame.xml @@ -10,121 +10,120 @@ <GuiElement profile="emptyPanel" id="mainBox" position="0px -80px"> - <GuiElement profile="emMenuLeftColumn"> - <Text profile="EM_SectionTitle" position="20px -10px" text="$l10n_em_field_list_title" /> - - <SmoothList id="fieldList" profile="emMenuList" position="10px -40px" width="300px" height="700px" - selectedWithoutFocus="true"> - <ListItem profile="emMenuListItem"> - <Bitmap profile="fs25_subCategoryListItemIcon" name="icon"/> - <Text profile="emMenuListItemTitle" name="title"/> - <Text profile="emMenuListItemSubtitle" name="subtitle"/> - </ListItem> - </SmoothList> - - <ThreePartBitmap profile="emMenuListSliderBox"> - <Slider profile="fs25_listSlider" dataElementId="fieldList" /> - </ThreePartBitmap> + <GuiElement profile="emMenuLeftColumn" width="400px" debugEnabled="true"> + <Text profile="EM_SectionTitle" position="20px -20px" text="$l10n_em_field_list_title" /> + + <GuiElement profile="fs25_subCategoryListContainer" id="fieldsContainer" position="0px -60px" width="400px" height="700px"> + <SmoothList id="fieldList" profile="EM_List" startClipperElementName="startClipper" endClipperElementName="endClipper"> + <ListItem profile="EM_ListItem" width="400px"> + <!-- <Bitmap profile="EM_ListItemAvatar" name="avatar"/> --> + <Bitmap profile="fs25_subCategoryListItemIcon" name="icon"/> + <Text profile="EM_ListItemTitle" name="title" width="100"/> + <Text profile="EM_ListItemSubtitle" name="subtitle" width="50"/> + </ListItem> + </SmoothList> + + <ThreePartBitmap profile="fs25_subCategoryListSliderBox" debugEnabled="true"> + <Slider profile="fs25_listSlider" dataElementId="employeeList"/> + </ThreePartBitmap> + </GuiElement> </GuiElement> - <GuiElement profile="emMenuRightColumn" id="detailPanel"> + <Bitmap profile="fs25_settingsTooltipSeparator" id="columnSeparator" position="420px 0px"/> + + <GuiElement profile="emMenuRightColumn" id="detailPanel" position="440px 0px"> <Text profile="EM_DetailTitle" id="txtFieldId" position="10px -10px"/> - <BoxLayout profile="EM_ColBoxV" position="10px -55px" width="390px"> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_field_area" width="160px"/> - <Text profile="EM_RowValue" id="txtFieldArea" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_field_crop" width="160px"/> - <Text profile="EM_RowValue" id="txtCurrentCrop" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_field_growth" width="160px"/> - <Text profile="EM_RowValue" id="txtGrowthState" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_field_target_crop" width="160px"/> - <CheckedOptionElement id="targetCropSelector" profile="emMenuCheckedOption" width="220px" onClick="onTargetCropChanged" /> - </BoxLayout> - </BoxLayout> - - <ThreePartBitmap profile="fs25_lineSeparatorTop" position="10px -165px" width="390px"/> - - <Text profile="EM_SectionTitle" position="10px -185px" text="$l10n_em_field_condition"/> - <Text profile="EM_RowLabel" id="txtFieldCondition" position="10px -210px" width="390px"/> - - <ThreePartBitmap profile="fs25_lineSeparatorTop" position="10px -245px" width="390px"/> - - <Text profile="EM_SectionTitle" position="10px -265px" text="$l10n_em_field_assigned_employee"/> + <!-- Field info: 2-column layout (label X=10, value X=170) --> + <Text profile="EM_RowLabel" position="10px -55px" text="Area:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtFieldArea" position="170px -55px" width="200px"/> + + <Text profile="EM_RowLabel" position="10px -80px" text="Current Crop:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtCurrentCrop" position="170px -80px" width="200px"/> + + <Text profile="EM_RowLabel" position="10px -105px" text="Growth State:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtGrowthState" position="170px -105px" width="200px"/> + + <ThreePartBitmap profile="fs25_lineSeparatorTop" position="10px -135px" width="370px"/> + + <!-- Target Crop selector --> + <Text profile="EM_SectionTitle" position="10px -150px" text="$l10n_em_field_target_crop"/> + <MultiTextOption profile="EM_InfoSelector" id="targetCropSelector" onClick="onTargetCropChanged" size="370px 32px" position="10px -175px"/> + + <!-- Save button --> + <Button profile="buttonActivate" id="btnSaveFieldConfig" text="$l10n_em_btn_save" onClick="onSaveFieldConfig" position="10px -220px" size="120px 32px"> + <Bitmap profile="fs25_dialogButtonBoxSeparator"/> + </Button> + + <ThreePartBitmap profile="fs25_lineSeparatorTop" position="10px -260px" width="370px"/> + + <!-- Condition --> + <Text profile="EM_SectionTitle" position="10px -275px" text="$l10n_em_field_condition"/> + <Text profile="EM_RowLabel" id="txtFieldCondition" position="10px -300px" width="370px"/> + + <ThreePartBitmap profile="fs25_lineSeparatorTop" position="10px -330px" width="370px"/> + + <!-- Assigned Employee --> + <Text profile="EM_SectionTitle" position="10px -345px" text="$l10n_em_field_assigned_employee"/> <!-- Shown when NO employee is assigned --> - <Text profile="EM_RowValue" id="txtNoEmployee" position="10px -290px" width="390px"/> + <Text profile="EM_RowValue_Left" id="txtNoEmployee" position="10px -370px" width="370px"/> <!-- Shown when an employee IS assigned --> - <GuiElement id="assignedEmployeeSection" position="10px -290px" width="390px" visible="false"> + <GuiElement id="assignedEmployeeSection" position="10px -370px" width="370px" visible="false"> <!-- Name --> - <Text profile="EM_RowValue" id="txtAssignedEmployee" position="0px 0px" width="390px"/> - - <!-- Basic info rows --> - <BoxLayout profile="EM_ColBoxV" position="0px -28px" width="390px"> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_employee_status_label" width="160px"/> - <Text profile="EM_RowValue" id="txtEmpStatus" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_assigned_vehicle" width="160px"/> - <Text profile="EM_RowValue" id="txtEmpVehicle" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_shift_label" width="160px"/> - <Text profile="EM_RowValue" id="txtEmpShift" width="220px"/> - </BoxLayout> - <BoxLayout profile="EM_RowBoxH" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_stat_fatigue" width="160px"/> - <Text profile="EM_RowValue" id="txtEmpFatigue" width="220px"/> - </BoxLayout> - </BoxLayout> + <Text profile="EM_RowValue_Left" id="txtAssignedEmployee" position="0px 0px" width="370px"/> + + <!-- Basic info: 2-column (label X=0, value X=160) --> + <Text profile="EM_RowLabel" position="0px -28px" text="Status:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtEmpStatus" position="160px -28px" width="200px"/> + + <Text profile="EM_RowLabel" position="0px -53px" text="Vehicle:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtEmpVehicle" position="160px -53px" width="200px"/> + + <Text profile="EM_RowLabel" position="0px -78px" text="Shift:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtEmpShift" position="160px -78px" width="200px"/> + + <Text profile="EM_RowLabel" position="0px -103px" text="Fatigue:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtEmpFatigue" position="160px -103px" width="200px"/> <!-- Separator: Info -> Skills --> - <ThreePartBitmap profile="fs25_lineSeparatorTop" position="0px -160px" width="390px"/> + <ThreePartBitmap profile="fs25_lineSeparatorTop" position="0px -133px" width="370px"/> <!-- Skills section --> - <Text profile="EM_SectionTitle" position="0px -175px" text="$l10n_em_employee_skills_title"/> + <Text profile="EM_SectionTitle" position="0px -148px" text="$l10n_em_employee_skills_title"/> <!-- Driving --> - <Text profile="EM_SkillLabel" position="0px -200px" text="$l10n_em_skill_driving" width="100px"/> - <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -205px" width="210px"> + <Text profile="EM_SkillLabel" position="0px -173px" text="$l10n_em_skill_driving" width="100px"/> + <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -178px" width="200px"> <ThreePartBitmap profile="EM_SkillBar" id="barEmpDriving"/> </ThreePartBitmap> - <Text profile="EM_SkillLevel" id="txtEmpDrivingLevel" position="318px -200px" width="62px"/> + <Text profile="EM_SkillLevel" id="txtEmpDrivingLevel" position="308px -173px" width="62px"/> <!-- Harvesting --> - <Text profile="EM_SkillLabel" position="0px -225px" text="$l10n_em_skill_harvesting" width="100px"/> - <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -230px" width="210px"> + <Text profile="EM_SkillLabel" position="0px -198px" text="$l10n_em_skill_harvesting" width="100px"/> + <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -203px" width="200px"> <ThreePartBitmap profile="EM_SkillBar" id="barEmpHarvesting"/> </ThreePartBitmap> - <Text profile="EM_SkillLevel" id="txtEmpHarvestingLevel" position="318px -225px" width="62px"/> + <Text profile="EM_SkillLevel" id="txtEmpHarvestingLevel" position="308px -198px" width="62px"/> <!-- Technical --> - <Text profile="EM_SkillLabel" position="0px -250px" text="$l10n_em_skill_technical" width="100px"/> - <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -255px" width="210px"> + <Text profile="EM_SkillLabel" position="0px -223px" text="$l10n_em_skill_technical" width="100px"/> + <ThreePartBitmap profile="EM_SkillBarBackground" position="100px -228px" width="200px"> <ThreePartBitmap profile="EM_SkillBar" id="barEmpTechnical"/> </ThreePartBitmap> - <Text profile="EM_SkillLevel" id="txtEmpTechnicalLevel" position="318px -250px" width="62px"/> + <Text profile="EM_SkillLevel" id="txtEmpTechnicalLevel" position="308px -223px" width="62px"/> <!-- Separator: Skills -> Workflow --> - <ThreePartBitmap profile="fs25_lineSeparatorTop" position="0px -275px" width="390px"/> + <ThreePartBitmap profile="fs25_lineSeparatorTop" position="0px -253px" width="370px"/> <!-- Workflow section --> - <Text profile="EM_SectionTitle" position="0px -290px" text="$l10n_em_workflow_builder"/> - <Text profile="EM_RowLabel" id="txtEmpWorkflow" position="0px -312px" width="390px"/> + <Text profile="EM_SectionTitle" position="0px -268px" text="$l10n_em_workflow_builder"/> + <Text profile="EM_RowLabel" id="txtEmpWorkflow" position="0px -290px" width="370px"/> - <BoxLayout profile="EM_RowBoxH" position="0px -340px" width="390px"> - <Text profile="EM_RowLabel" text="$l10n_em_stat_current_job" width="160px"/> - <Text profile="EM_RowValue" id="txtEmpCurrentTask" width="220px"/> - </BoxLayout> + <Text profile="EM_RowLabel" position="0px -318px" text="Current Job:" width="160px"/> + <Text profile="EM_RowValue_Left" id="txtEmpCurrentTask" position="160px -318px" width="200px"/> </GuiElement> </GuiElement> </GuiElement> diff --git a/xml/gui/guiProfiles.xml b/xml/gui/guiProfiles.xml index c2d78e6..da5b69c 100644 --- a/xml/gui/guiProfiles.xml +++ b/xml/gui/guiProfiles.xml @@ -138,6 +138,15 @@ <width value="250px"/> </Profile> + <!-- Row value: left-aligned (for absolute-positioned 2-column layouts) --> + <Profile name="EM_RowValue_Left" extends="fs25_textDefault" with="anchorTopLeft"> + <textSize value="18px"/> + <textBold value="true"/> + <textColor value="1 1 1 1"/> + <textAlignment value="left"/> + <width value="200px"/> + </Profile> + <!-- Skill bars --> <Profile name="EM_SkillLabel" extends="fs25_textDefault" with="anchorTopLeft"> <textSize value="18px"/> @@ -335,11 +344,11 @@ </Profile> <Profile name="emMenuLeftColumn" extends="emptyPanel" with="anchorTopLeft"> - <size value="320px 780px"/> + <size value="400px 780px"/> </Profile> <Profile name="emMenuRightColumn" extends="emptyPanel" with="anchorTopLeft"> - <size value="420px 780px"/> + <size value="400px 780px"/> <position value="340px 0px"/> </Profile> From 72ed2684ce47563a3ceb5e6810fe08d394d373c7 Mon Sep 17 00:00:00 2001 From: LeGrizzly <41363694+LeGrizzly@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:18:16 +0200 Subject: [PATCH 10/10] Delete .vscode/settings.json --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9b2556d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "DBAPI.dataPath": "C:\\Users\\xalsi\\Documents\\My Games\\FarmingSimulator2025\\savegame2\\DBAPI_data" -} \ No newline at end of file