diff --git a/FS25_YieldTracker/gui/guiProfiles.xml b/FS25_YieldTracker/gui/guiProfiles.xml
index 50de208..5da488b 100644
--- a/FS25_YieldTracker/gui/guiProfiles.xml
+++ b/FS25_YieldTracker/gui/guiProfiles.xml
@@ -1,147 +1,151 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/gui/yieldTrackerPage.xml b/FS25_YieldTracker/gui/yieldTrackerPage.xml
index e9315d1..0e32f35 100644
--- a/FS25_YieldTracker/gui/yieldTrackerPage.xml
+++ b/FS25_YieldTracker/gui/yieldTrackerPage.xml
@@ -1,6 +1,6 @@
-
+
@@ -11,123 +11,101 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
-
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
+
diff --git a/FS25_YieldTracker/main.lua b/FS25_YieldTracker/main.lua
index 23ecc05..6d3b59a 100644
--- a/FS25_YieldTracker/main.lua
+++ b/FS25_YieldTracker/main.lua
@@ -1,44 +1,44 @@
--- Author: BitBarn Mods
--- Date: 06-11-2025
--- Version: 1.0.0.0
--- main.lua
-
----Main handler table for YieldTracker mod.
----Controls mod initialization, update cycle, and cleanup.
-MainHandler = {
- dir = g_currentModDirectory, -- Directory where the mod is located
- enabled = true -- Whether the mod is active
-}
-
--- Load core mod logic and GUI scripts
-source(MainHandler.dir .. "scripts/yieldTracker.lua")
-source(MainHandler.dir .. "scripts/gui/yieldTrackerGUI.lua")
-
----Called once when the map is loaded.
----Disables the mod in multiplayer mode; otherwise, initializes tracking systems.
-function MainHandler:loadMap(name)
- if g_currentMission and g_currentMission.missionDynamicInfo and g_currentMission.missionDynamicInfo.isMultiplayer then
- print("[YieldTracker] Multiplayer detected. Disabling mod.")
- self.enabled = false
- g_currentMission:showBlinkingWarning("YieldTracker disabled in multiplayer mode.", 5000)
- return
- end
-
- YieldTracker:setup() -- Initialize yield tracking logic
- YieldTrackerGUI:setup() -- Initialize the GUI components
-end
-
----Called every frame to perform per-tick updates.
----Only runs if mod is enabled. Delegates update logic to YieldTracker.
-function MainHandler:update(dt)
- if not self.enabled then return end
- YieldTracker:updateByFrame()
-end
-
----Called when the map is being deleted/unloaded.
----Stub provided for potential future cleanup logic.
-function MainHandler:deleteMap()
-end
-
--- Register MainHandler to receive game loop events (loadMap, update, etc.)
+-- Author: BitBarn Mods
+-- Date: 06-11-2025
+-- Version: 1.0.0.0
+-- main.lua
+
+---Main handler table for YieldTracker mod.
+---Controls mod initialization, update cycle, and cleanup.
+MainHandler = {
+ dir = g_currentModDirectory, -- Directory where the mod is located
+ enabled = true -- Whether the mod is active
+}
+
+-- Load core mod logic and GUI scripts
+source(MainHandler.dir .. "scripts/yieldTracker.lua")
+source(MainHandler.dir .. "scripts/gui/yieldTrackerGUI.lua")
+
+---Called once when the map is loaded.
+---Disables the mod in multiplayer mode; otherwise, initializes tracking systems.
+function MainHandler:loadMap(name)
+ if g_currentMission and g_currentMission.missionDynamicInfo and g_currentMission.missionDynamicInfo.isMultiplayer then
+ print("[YieldTracker] Multiplayer detected. Disabling mod.")
+ self.enabled = false
+ g_currentMission:showBlinkingWarning("YieldTracker disabled in multiplayer mode.", 5000)
+ return
+ end
+
+ YieldTracker:setup() -- Initialize yield tracking logic
+ YieldTrackerGUI:setup() -- Initialize the GUI components
+end
+
+---Called every frame to perform per-tick updates.
+---Only runs if mod is enabled. Delegates update logic to YieldTracker.
+function MainHandler:update(dt)
+ if not self.enabled then return end
+ YieldTracker:updateByFrame()
+end
+
+---Called when the map is being deleted/unloaded.
+---Stub provided for potential future cleanup logic.
+function MainHandler:deleteMap()
+end
+
+-- Register MainHandler to receive game loop events (loadMap, update, etc.)
addModEventListener(MainHandler)
\ No newline at end of file
diff --git a/FS25_YieldTracker/modDesc.xml b/FS25_YieldTracker/modDesc.xml
index fa071ab..dca6a55 100644
--- a/FS25_YieldTracker/modDesc.xml
+++ b/FS25_YieldTracker/modDesc.xml
@@ -1,144 +1,144 @@
-
-
-BitBarn Mods
-1.0.0.0
-
-Yield Tracker
-Ertrags-Tracker
-Suivi Du Rendement
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-icon_YieldTracker.dds
-
-
-
-
-
-
+
+
+BitBarn Mods / Knowen
+1.0.1.4
+
+Yield Tracker
+Ertrags-Tracker
+Suivi Du Rendement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+icon_YieldTracker.dds
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/scripts/gui/yieldTrackerGUI.lua b/FS25_YieldTracker/scripts/gui/yieldTrackerGUI.lua
index 9dd675a..d7b0cf5 100644
--- a/FS25_YieldTracker/scripts/gui/yieldTrackerGUI.lua
+++ b/FS25_YieldTracker/scripts/gui/yieldTrackerGUI.lua
@@ -1,108 +1,108 @@
--- Author: BitBarn Mods
--- Date: 06-11-2025
--- Version: 1.0.0.0
--- yieldTrackerGUI.lua
-
----Handles GUI integration for the YieldTracker mod.
----Responsible for loading GUI pages, registering with InGameMenu, and managing page layout.
-YieldTrackerGUI = {
- dir = g_currentModDirectory, -- Mod directory path
- modName = g_currentModName -- Name of the current mod
-}
-
--- Load the YieldTrackerPage class for GUI logic
-source(YieldTrackerGUI.dir .. "scripts/gui/yieldTrackerPage.lua") -- > YieldTrackerPage
-
----Helper function: Finds the index of a specific value in a sequential table.
----@param tbl table The table to search
----@param value any The value to find
----@return integer? Index of the value or nil if not found
-local function findIndex(tbl, value)
- for i, v in ipairs(tbl) do
- if v == value then
- return i
- end
- end
- return nil
-end
-
----Helper function: Moves an element to a new position within a table.
----@param tbl table The table to modify
----@param element any The element to move
----@param position integer The new index position
-local function moveElement(tbl, element, position)
- for i, v in ipairs(tbl) do
- if v == element then
- table.remove(tbl, i)
- table.insert(tbl, position, element)
- break
- end
- end
-end
-
----Initializes the YieldTracker GUI when the map loads.
----Loads GUI profiles, creates the GUI page, and inserts it into the in-game menu.
-function YieldTrackerGUI:setup()
- local inGameMenu = g_gui.screenControllers[InGameMenu]
-
- -- Load custom GUI styles from XML
- g_gui:loadProfiles(self.dir .. "gui/guiProfiles.xml")
-
- -- Instantiate and load the YieldTracker GUI page
- local yieldPage = YieldTrackerPage.new(g_i18n, g_messageCenter)
- g_gui:loadGui(self.dir .. "gui/yieldTrackerPage.xml", "yieldTrackerPage", yieldPage, true)
-
- -- Add the new page to the in-game menu using the helper
- self.fixInGameMenu(yieldPage, "yieldTrackerPage", {0, 0, 1024, 1024}, 2, nil)
-end
-
----Registers a custom GUI page with the in-game menu.
----Ensures correct ordering, visual integration, and control registration.
----@param frame table The GUI frame to add
----@param pageName string Unique name for the page
----@param uvs table UV coordinates for the page tab icon
----@param position integer Index position for the new page
----@param predicateFunc function? Optional function to determine page visibility
-function YieldTrackerGUI.fixInGameMenu(frame, pageName, uvs, position, predicateFunc)
- local inGameMenu = g_gui.screenControllers[InGameMenu]
-
- -- Avoid duplicate control ID conflicts
- inGameMenu.controlIDs[pageName] = nil
-
- -- Find insertion point based on existing pages (e.g. above pageStatistics)
- local abovePrices = findIndex(inGameMenu.pagingElement.elements, inGameMenu.pageStatistics) or position
-
- -- Register the frame and expose it in the menu
- inGameMenu[pageName] = frame
- inGameMenu.pagingElement:addElement(frame)
- inGameMenu:exposeControlsAsFields(pageName)
-
- -- Move the new frame into the desired slot in the page list
- moveElement(inGameMenu.pagingElement.elements, frame, abovePrices)
-
- -- Adjust internal page references accordingly
- for i, page in ipairs(inGameMenu.pagingElement.pages) do
- if page.element == frame then
- table.remove(inGameMenu.pagingElement.pages, i)
- table.insert(inGameMenu.pagingElement.pages, abovePrices, page)
- break
- end
- end
-
- -- Refresh layout and page mappings after changes
- inGameMenu.pagingElement:updateAbsolutePosition()
- inGameMenu.pagingElement:updatePageMapping()
-
- -- Register logic to determine when the page is enabled
- inGameMenu:registerPage(frame, position, predicateFunc)
-
- -- Add tab icon to the menu bar
- local iconPath = Utils.getFilename("images/yieldTrackerMenuIcon.dds", YieldTrackerGUI.dir)
- inGameMenu:addPageTab(frame, iconPath, GuiUtils.getUVs(uvs))
-
- -- Ensure the new page appears in the correct order in pageFrames
- moveElement(inGameMenu.pageFrames, frame, abovePrices)
-
- -- Rebuild tab list to include the new page
- inGameMenu:rebuildTabList()
-end
+-- Author: BitBarn Mods
+-- Date: 06-11-2025
+-- Version: 1.0.0.0
+-- yieldTrackerGUI.lua
+
+---Handles GUI integration for the YieldTracker mod.
+---Responsible for loading GUI pages, registering with InGameMenu, and managing page layout.
+YieldTrackerGUI = {
+ dir = g_currentModDirectory, -- Mod directory path
+ modName = g_currentModName -- Name of the current mod
+}
+
+-- Load the YieldTrackerPage class for GUI logic
+source(YieldTrackerGUI.dir .. "scripts/gui/yieldTrackerPage.lua") -- > YieldTrackerPage
+
+---Helper function: Finds the index of a specific value in a sequential table.
+---@param tbl table The table to search
+---@param value any The value to find
+---@return integer? Index of the value or nil if not found
+local function findIndex(tbl, value)
+ for i, v in ipairs(tbl) do
+ if v == value then
+ return i
+ end
+ end
+ return nil
+end
+
+---Helper function: Moves an element to a new position within a table.
+---@param tbl table The table to modify
+---@param element any The element to move
+---@param position integer The new index position
+local function moveElement(tbl, element, position)
+ for i, v in ipairs(tbl) do
+ if v == element then
+ table.remove(tbl, i)
+ table.insert(tbl, position, element)
+ break
+ end
+ end
+end
+
+---Initializes the YieldTracker GUI when the map loads.
+---Loads GUI profiles, creates the GUI page, and inserts it into the in-game menu.
+function YieldTrackerGUI:setup()
+ local inGameMenu = g_gui.screenControllers[InGameMenu]
+
+ -- Load custom GUI styles from XML
+ g_gui:loadProfiles(self.dir .. "gui/guiProfiles.xml")
+
+ -- Instantiate and load the YieldTracker GUI page
+ local yieldPage = YieldTrackerPage.new(g_i18n, g_messageCenter)
+ g_gui:loadGui(self.dir .. "gui/yieldTrackerPage.xml", "yieldTrackerPage", yieldPage, true)
+
+ -- Add the new page to the in-game menu using the helper
+ self.fixInGameMenu(yieldPage, "yieldTrackerPage", {0, 0, 1024, 1024}, 2, nil)
+end
+
+---Registers a custom GUI page with the in-game menu.
+---Ensures correct ordering, visual integration, and control registration.
+---@param frame table The GUI frame to add
+---@param pageName string Unique name for the page
+---@param uvs table UV coordinates for the page tab icon
+---@param position integer Index position for the new page
+---@param predicateFunc function? Optional function to determine page visibility
+function YieldTrackerGUI.fixInGameMenu(frame, pageName, uvs, position, predicateFunc)
+ local inGameMenu = g_gui.screenControllers[InGameMenu]
+
+ -- Avoid duplicate control ID conflicts
+ inGameMenu.controlIDs[pageName] = nil
+
+ -- Find insertion point based on existing pages (e.g. above pageStatistics)
+ local abovePrices = findIndex(inGameMenu.pagingElement.elements, inGameMenu.pageStatistics) or position
+
+ -- Register the frame and expose it in the menu
+ inGameMenu[pageName] = frame
+ inGameMenu.pagingElement:addElement(frame)
+ inGameMenu:exposeControlsAsFields(pageName)
+
+ -- Move the new frame into the desired slot in the page list
+ moveElement(inGameMenu.pagingElement.elements, frame, abovePrices)
+
+ -- Adjust internal page references accordingly
+ for i, page in ipairs(inGameMenu.pagingElement.pages) do
+ if page.element == frame then
+ table.remove(inGameMenu.pagingElement.pages, i)
+ table.insert(inGameMenu.pagingElement.pages, abovePrices, page)
+ break
+ end
+ end
+
+ -- Refresh layout and page mappings after changes
+ inGameMenu.pagingElement:updateAbsolutePosition()
+ inGameMenu.pagingElement:updatePageMapping()
+
+ -- Register logic to determine when the page is enabled
+ inGameMenu:registerPage(frame, position, predicateFunc)
+
+ -- Add tab icon to the menu bar
+ local iconPath = Utils.getFilename("images/yieldTrackerMenuIcon.dds", YieldTrackerGUI.dir)
+ inGameMenu:addPageTab(frame, iconPath, GuiUtils.getUVs(uvs))
+
+ -- Ensure the new page appears in the correct order in pageFrames
+ moveElement(inGameMenu.pageFrames, frame, abovePrices)
+
+ -- Rebuild tab list to include the new page
+ inGameMenu:rebuildTabList()
+end
diff --git a/FS25_YieldTracker/scripts/gui/yieldTrackerPage.lua b/FS25_YieldTracker/scripts/gui/yieldTrackerPage.lua
index 899843b..648db7f 100644
--- a/FS25_YieldTracker/scripts/gui/yieldTrackerPage.lua
+++ b/FS25_YieldTracker/scripts/gui/yieldTrackerPage.lua
@@ -12,11 +12,6 @@ YieldTrackerPage.debug = false
YieldTrackerPage.dir = g_currentModDirectory
-- source(g_currentModDirectory .. "scripts/yieldTrackerPage.lua") --> YieldTracker.yieldData -- BUG -- CANNOT LOAD RESOURCE, THIS IS NOT THE CORRECT PATH FOR THE DATA. TESTING NEEDED TO SEE IF REQUIRED AND IF SO CHANGE PATH
-source(g_currentModDirectory .. "scripts/trackedCropTypes.lua") --> TrackedCropTypes
-
-local trackedCropTypes = TrackedCropTypes
-
-
function YieldTrackerPage.new(i18n, messageCenter)
local self = YieldTrackerPage:superClass().new(nil, YieldTrackerPage._mt)
self.name = "YieldTrackerPage"
@@ -26,8 +21,9 @@ function YieldTrackerPage.new(i18n, messageCenter)
self.dataBindings = {}
self.farmlandIDs = {}
self.yieldYears = {}
- self.cropInfo = trackedCropTypes
- self.selectedCrop = trackedCropTypes[1].fillTypeIndex
+ -- Dynamically filled from g_fruitTypeManager in onGuiSetupFinished/onFrameOpen
+ self.cropInfo = {}
+ self.selectedCrop = nil
self.selectedField = 1
self.showingOwnedFields = false
@@ -48,6 +44,202 @@ local function formatWithCommas(n)
end
+function YieldTrackerPage:getLocalizedTextOrFallback(key, fallback)
+ if key ~= nil and key ~= "" then
+ local text = self.i18n:getText(key)
+ if text ~= nil and text ~= key and text ~= "" then
+ return text
+ end
+ end
+
+ return fallback or ""
+end
+
+
+function YieldTrackerPage:getFruitTypeDisplayName(fruitType, fillType)
+ local rawTitle = nil
+
+ if fillType ~= nil then
+ rawTitle = fillType.title or fillType.name
+ end
+
+ rawTitle = rawTitle or fruitType.title or fruitType.name or ""
+
+ if type(rawTitle) == "string" and rawTitle:sub(1, 6) == "$l10n_" then
+ return self:getLocalizedTextOrFallback(rawTitle:sub(7), fruitType.name or rawTitle)
+ end
+
+ return rawTitle
+end
+
+
+function YieldTrackerPage:getFillTypeIndexByNameSafe(fillTypeName)
+ if fillTypeName == nil or g_fillTypeManager == nil then
+ return nil
+ end
+
+ if g_fillTypeManager.getFillTypeIndexByName ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeIndexByName(fillTypeName) end)
+ if ok and result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.nameToIndex ~= nil then
+ local result = g_fillTypeManager.nameToIndex[fillTypeName]
+ if result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.nameToFillType ~= nil and g_fillTypeManager.nameToFillType[fillTypeName] ~= nil then
+ local desc = g_fillTypeManager.nameToFillType[fillTypeName]
+ local result = desc.index or desc.fillTypeIndex
+ if result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.indexToFillType ~= nil then
+ for index, desc in pairs(g_fillTypeManager.indexToFillType) do
+ if desc ~= nil and desc.name == fillTypeName then
+ return index
+ end
+ end
+ end
+
+ return nil
+end
+
+function YieldTrackerPage:getFillTypeDescByIndexSafe(fillTypeIndex)
+ if fillTypeIndex == nil or g_fillTypeManager == nil then
+ return nil
+ end
+
+ if g_fillTypeManager.getFillTypeByIndex ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeByIndex(fillTypeIndex) end)
+ if ok and result ~= nil then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.indexToFillType ~= nil then
+ return g_fillTypeManager.indexToFillType[fillTypeIndex]
+ end
+
+ return nil
+end
+
+function YieldTrackerPage:getFillTypeDisplayName(fillTypeIndex, fallbackName)
+ local desc = self:getFillTypeDescByIndexSafe(fillTypeIndex)
+ local rawTitle = fallbackName
+
+ if desc ~= nil then
+ rawTitle = desc.title or desc.name or fallbackName
+ end
+
+ if type(rawTitle) == "string" and rawTitle:sub(1, 6) == "$l10n_" then
+ return self:getLocalizedTextOrFallback(rawTitle:sub(7), fallbackName or rawTitle)
+ end
+
+ return rawTitle or tostring(fillTypeIndex)
+end
+
+function YieldTrackerPage:addExtraFillTypeToCropList(crops, usedFillTypes, fillTypeName, fallbackTitle)
+ local fillTypeIndex = self:getFillTypeIndexByNameSafe(fillTypeName)
+ if fillTypeIndex == nil or usedFillTypes[fillTypeIndex] ~= nil then
+ return false
+ end
+
+ local desc = self:getFillTypeDescByIndexSafe(fillTypeIndex)
+ local icon = ""
+
+ if desc ~= nil then
+ icon = desc.hudOverlayFilename or desc.iconFilename or ""
+ end
+
+ local title = self:getFillTypeDisplayName(fillTypeIndex, fallbackTitle or fillTypeName)
+
+ table.insert(crops, {
+ fruitTypeIndex = nil,
+ fillTypeIndex = fillTypeIndex,
+ name = fillTypeName,
+ title = title,
+ hudOverlayFilename = icon,
+ isExtraFillType = true
+ })
+
+ usedFillTypes[fillTypeIndex] = true
+ return true
+end
+
+
+
+function YieldTrackerPage:refreshCropInfoFromFruitTypeManager()
+ local crops = {}
+ local usedFillTypes = {}
+
+ if g_fruitTypeManager == nil or g_fruitTypeManager.getFruitTypes == nil then
+ self:debugMessage("FruitTypeManager not available yet; crop list remains empty")
+ self.cropInfo = crops
+ self.selectedCrop = nil
+ return crops
+ end
+
+ for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do
+ if fruitType ~= nil then
+ local fillType = fruitType.fillType
+ local fillTypeIndex = nil
+
+ if fillType ~= nil then
+ fillTypeIndex = fillType.index or fillType.fillTypeIndex
+ end
+
+ fillTypeIndex = fillTypeIndex or fruitType.fillTypeIndex
+
+ if fillTypeIndex ~= nil and usedFillTypes[fillTypeIndex] == nil then
+ usedFillTypes[fillTypeIndex] = true
+
+ local title = self:getFruitTypeDisplayName(fruitType, fillType)
+ local icon = nil
+
+ if fillType ~= nil then
+ icon = fillType.hudOverlayFilename or fillType.iconFilename
+ end
+
+ icon = icon or fruitType.hudOverlayFilename or fruitType.iconFilename or ""
+
+ table.insert(crops, {
+ fruitTypeIndex = fruitType.index,
+ fillTypeIndex = fillTypeIndex,
+ name = fruitType.name or title,
+ title = title,
+ hudOverlayFilename = icon
+ })
+ end
+ end
+ end
+
+ -- Extra fill types that are not always exposed as FruitTypes, but are relevant for yield tracking.
+ self:addExtraFillTypeToCropList(crops, usedFillTypes, "DRYGRASS_WINDROW", "Heu")
+ self:addExtraFillTypeToCropList(crops, usedFillTypes, "CHAFF", "Häckselgut")
+ self:addExtraFillTypeToCropList(crops, usedFillTypes, "STRAW", "Stroh")
+ self:addExtraFillTypeToCropList(crops, usedFillTypes, "STRAW_WINDROW", "Stroh")
+
+ table.sort(crops, function(a, b)
+ return tostring(a.title):lower() < tostring(b.title):lower()
+ end)
+
+ self.cropInfo = crops
+
+ if self.selectedCrop == nil or usedFillTypes[self.selectedCrop] == nil then
+ self.selectedCrop = crops[1] ~= nil and crops[1].fillTypeIndex or nil
+ end
+
+ return crops
+end
+
+
function YieldTrackerPage:setupCustomButtons()
self.backButtonInfo = {
@@ -119,22 +311,38 @@ function YieldTrackerPage:onGuiSetupFinished()
-- farm id list handling
self.farmlandList = self:getDescendantById("farmlandList")
- self.farmlandList:setDataSource(self)
- self.farmlandList:setDelegate(self)
+ if self.farmlandList ~= nil then
+ self.farmlandList:setDataSource(self)
+ self.farmlandList:setDelegate(self)
+ end
-- crop list/fill type handling
self.cropList = self:getDescendantById("cropList")
- self.cropList:setDataSource(self)
- self.cropList:setDelegate(self)
-
- if self.cropList then
+ self:refreshCropInfoFromFruitTypeManager()
+ if self.cropList ~= nil then
+ self.cropList:setDataSource(self)
+ self.cropList:setDelegate(self)
self.cropList:reloadData()
end
-- users yield history list
self.yieldPerYearList = self:getDescendantById("yieldPerYearList")
- self.yieldPerYearList:setDataSource(self)
- self.yieldPerYearList:setDelegate(self)
+ if self.yieldPerYearList ~= nil then
+ self.yieldPerYearList:setDataSource(self)
+ self.yieldPerYearList:setDelegate(self)
+ end
+ self.noYieldHistoryText = self:getDescendantById("noYieldHistoryText")
+ self.yieldStatsInformation = self:getDescendantById("yieldStatsInformation")
+ self.yieldListMode = self:getDescendantById("yieldListMode")
+ self.yieldDetailMode = self:getDescendantById("yieldDetailMode")
+
+ -- Additional GUI elements used by the redesigned XML layout
+ self.yieldListMode = self:getDescendantById("yieldListMode")
+ self.yieldDetailMode = self:getDescendantById("yieldDetailMode")
+ self.noYieldHistoryText = self:getDescendantById("noYieldHistoryText")
+ self.yieldStatsInformation = self:getDescendantById("yieldStatsInformation")
+ self.graphYearTickTemplate = self:getDescendantById("graphYearTickTemplate")
+ self.graphCandleTemplate = self:getDescendantById("graphCandleTemplate")
-- debuggers
self:debugMessage("YieldPerYearList found: " .. tostring(self.yieldPerYearList ~= nil))
@@ -150,6 +358,11 @@ end
function YieldTrackerPage:onFrameOpen()
YieldTrackerPage:superClass().onFrameOpen(self)
self:getFarmlandIDs()
+ self:refreshCropInfoFromFruitTypeManager()
+
+ if self.cropList then
+ self.cropList:reloadData()
+ end
if self.farmlandList then
self.farmlandList:reloadData()
@@ -187,18 +400,45 @@ end
function YieldTrackerPage:onClickCropSelected(element)
local selected = self.cropInfo[element.indexInSection]
+ if selected == nil then
+ return
+ end
+
self.selectedCrop = selected.fillTypeIndex
self:setSelectedPageData()
+
+ -- NEW: switch to detail view when both selected
+ if self.selectedField ~= nil and self.selectedCrop ~= nil then
+ if self.yieldListMode ~= nil then
+ self.yieldListMode:setVisible(false)
+ end
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(true)
+ end
+ end
end
function YieldTrackerPage:onClickFieldSelected(element)
self.selectedField = self.farmlandIDs[element.indexInSection]
self:setSelectedPageData()
+
+ -- NEW: stay in list until crop is selected
+ if self.yieldListMode ~= nil then
+ self.yieldListMode:setVisible(true)
+ end
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(false)
+ end
end
function YieldTrackerPage:setSelectedPageData()
+ if self.selectedCrop == nil then
+ self.yieldYears = {}
+ return
+ end
+
self:initiateFieldCropStatsSearch()
self:setOverviewStatsText()
@@ -214,6 +454,10 @@ function YieldTrackerPage:initiateFieldCropStatsSearch()
local foundData = false
self.yieldYears = {} -- Clear existing data
+ if selectedFillTypeIndex == nil or selectedField == nil then
+ return
+ end
+
self:debugMessage(string.format("Search stats for fill type index %d on field %d", selectedFillTypeIndex, selectedField))
local yearAmountMap = {}
@@ -272,8 +516,18 @@ function YieldTrackerPage:initiateFieldCropStatsSearch()
end
-- Show message only if no data found
- self.noYieldHistoryText:setVisible(not foundData)
- self.yieldStatsInformation:setVisible(foundData)
+ if self.noYieldHistoryText ~= nil then
+ self.noYieldHistoryText:setVisible(not foundData)
+ end
+
+ -- Keep the right detail/graph panel visible even when no data is found.
+ if self.yieldStatsInformation ~= nil then
+ self.yieldStatsInformation:setVisible(true)
+ end
+
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(true)
+ end
end
@@ -306,15 +560,17 @@ function YieldTrackerPage:setOverviewStatsText()
if selectedFieldNumText and selectedCropTypeText and selectedCropTypeIcon and selectedCropTotalHarvests and selectedCropAverageYield and selectedCropBestYear then
selectedFieldNumText:setText(string.format("%s %d", self.i18n:getText("yt_fieldIDLabel"), fieldID))
- local cropKey = cropName:lower():gsub(" ", "") .. "Label"
- selectedCropTypeText:setText(self.i18n:getText(cropKey) or cropName)
+ -- Use the already resolved registered fill type title instead of building a custom l10n key.
+ selectedCropTypeText:setText(cropName or "")
selectedCropTotalHarvests:setText(selectedOverviewData.totalYears)
selectedCropAverageYield:setText(string.format("%s l", formatWithCommas(selectedOverviewData.averageYield)))
selectedCropBestYear:setText(string.format("%s %d --> %s l", self.i18n:getText("yt_yearLabel"), selectedOverviewData.highestYear, formatWithCommas(selectedOverviewData.highestYieldAmount)))
- selectedCropTypeIcon:setImageFilename(cropIcon)
+ if cropIcon ~= nil and cropIcon ~= "" then
+ selectedCropTypeIcon:setImageFilename(cropIcon)
+ end
end
end
@@ -429,6 +685,11 @@ function YieldTrackerPage:setGraphData()
local tickContainer = self:getDescendantById("graphYearTickContainer")
local barGraph = self:getDescendantById("barGraph")
+ if tickContainer == nil or barGraph == nil or self.graphYearTickTemplate == nil or self.graphCandleTemplate == nil then
+ self:debugMessage("Graph elements are missing; skipping graph update")
+ return
+ end
+
self:clearOldElements(tickContainer, self.graphYearTickTemplate)
self:clearOldElements(barGraph, self.graphCandleTemplate)
@@ -439,42 +700,40 @@ function YieldTrackerPage:setGraphData()
local actualMaxYear = years[#years]
local displayMinYear, displayMaxYear = self:calculateDisplayRange(actualMinYear, actualMaxYear)
-
local harvestByYear = self:createHarvestLookup(self.yieldYears, displayMinYear, displayMaxYear)
local maxYield = self:findMaxYield(harvestByYear, displayMinYear, displayMaxYear)
- local minYield = self:findMinYield(harvestByYear, displayMinYear, displayMaxYear)
- local yieldRange = math.max(1, maxYield - minYield)
local numTicks = displayMaxYear - displayMinYear + 1
- local totalGraphWidth = 0.42
- local spacing = totalGraphWidth / math.max(1, numTicks - 1)
-
+ -- Matches the cleaned right-side XML graph width. Keep left selection layout untouched.
+ local graphPaddingX = 0.012
+ local graphContentWidth = 0.376
+ local spacing = graphContentWidth / math.max(1, numTicks - 1)
+ local candleWidth = 0.011
+ local minHeight = 0.035
+ local maxHeight = 0.215
for year = displayMinYear, displayMaxYear do
- local tickX = (year - displayMinYear) * spacing
+ local tickX = graphPaddingX + (year - displayMinYear) * spacing
- -- Year tick label
local tickClone = self.graphYearTickTemplate:clone(tickContainer)
tickClone:setText(tostring(year))
tickClone:setPosition(tickX, 0)
tickClone:setVisible(true)
- -- Yield candle if data exists for this year
local harvest = harvestByYear[year]
if harvest then
local yearYieldAmount = harvest.amount or 0
- if yearYieldAmount and yearYieldAmount > 0 then
+ if yearYieldAmount > 0 then
local candleClone = self.graphCandleTemplate:clone(barGraph)
- local minHeight = 0.08
- local heightRatio = (yearYieldAmount - minYield) / yieldRange
- -- heightRatio = math.sqrt(heightRatio) -- apply easing
- local candleHeight = minHeight + heightRatio * (0.27 - minHeight)
+ local heightRatio = maxYield > 0 and (yearYieldAmount / maxYield) or 0
+ heightRatio = math.max(0, math.min(1, heightRatio))
+ local candleHeight = minHeight + heightRatio * (maxHeight - minHeight)
- candleClone:setSize(0.01, candleHeight)
- candleClone:setPosition(tickX + 0.0185, 0)
+ candleClone:setSize(candleWidth, candleHeight)
+ candleClone:setPosition(tickX - (candleWidth * 0.5), 0)
candleClone:setVisible(true)
end
end
@@ -497,11 +756,11 @@ function YieldTrackerPage:populateCellForItemInSection(list, sectionIndex, index
local icon = listItem:getAttribute("icon")
if cropListText then
- local cropKey = crop.title:lower():gsub(" ", "") .. "Label"
- cropListText:setText(self.i18n:getText(cropKey) or crop.title)
+ -- Use the registered fill type title directly. No custom translation entry required.
+ cropListText:setText(crop.title or crop.name or "")
end
- if icon then
+ if icon and crop.hudOverlayFilename ~= nil and crop.hudOverlayFilename ~= "" then
icon:setImageFilename(crop.hudOverlayFilename)
end
end
diff --git a/FS25_YieldTracker/scripts/gui/yieldTrackerPage_backup.lua b/FS25_YieldTracker/scripts/gui/yieldTrackerPage_backup.lua
new file mode 100644
index 0000000..5e06caa
--- /dev/null
+++ b/FS25_YieldTracker/scripts/gui/yieldTrackerPage_backup.lua
@@ -0,0 +1,723 @@
+-- Author: BitBarn Mods
+-- Date: 06-11-2025
+-- Version: 1.0.0.0
+-- yieldTrackerPage.lua
+
+PRINT_PREFIX = "[ YieldTracker ] - "
+
+
+YieldTrackerPage = {}
+YieldTrackerPage._mt = Class(YieldTrackerPage, TabbedMenuFrameElement)
+YieldTrackerPage.debug = false
+YieldTrackerPage.dir = g_currentModDirectory
+
+-- source(g_currentModDirectory .. "scripts/yieldTrackerPage.lua") --> YieldTracker.yieldData -- BUG -- CANNOT LOAD RESOURCE, THIS IS NOT THE CORRECT PATH FOR THE DATA. TESTING NEEDED TO SEE IF REQUIRED AND IF SO CHANGE PATH
+function YieldTrackerPage.new(i18n, messageCenter)
+ local self = YieldTrackerPage:superClass().new(nil, YieldTrackerPage._mt)
+ self.name = "YieldTrackerPage"
+ self.hasCustomMenuButtons = true
+ self.i18n = i18n
+ self.messageCenter = messageCenter
+ self.dataBindings = {}
+ self.farmlandIDs = {}
+ self.yieldYears = {}
+ -- Dynamically filled from g_fruitTypeManager in onGuiSetupFinished/onFrameOpen
+ self.cropInfo = {}
+ self.selectedCrop = nil
+ self.selectedField = 1
+ self.showingOwnedFields = false
+
+
+ return self
+end
+
+
+local function formatWithCommas(n)
+ n = math.floor(n + 0.5) -- Round to nearest whole number
+ local formatted = tostring(n)
+ local k
+ while true do
+ formatted, k = formatted:gsub("^(-?%d+)(%d%d%d)", "%1,%2")
+ if k == 0 then break end
+ end
+ return formatted
+end
+
+
+function YieldTrackerPage:getLocalizedTextOrFallback(key, fallback)
+ if key ~= nil and key ~= "" then
+ local text = self.i18n:getText(key)
+ if text ~= nil and text ~= key and text ~= "" then
+ return text
+ end
+ end
+
+ return fallback or ""
+end
+
+
+function YieldTrackerPage:getFruitTypeDisplayName(fruitType, fillType)
+ local rawTitle = nil
+
+ if fillType ~= nil then
+ rawTitle = fillType.title or fillType.name
+ end
+
+ rawTitle = rawTitle or fruitType.title or fruitType.name or ""
+
+ if type(rawTitle) == "string" and rawTitle:sub(1, 6) == "$l10n_" then
+ return self:getLocalizedTextOrFallback(rawTitle:sub(7), fruitType.name or rawTitle)
+ end
+
+ return rawTitle
+end
+
+
+function YieldTrackerPage:refreshCropInfoFromFruitTypeManager()
+ local crops = {}
+ local usedFillTypes = {}
+
+ if g_fruitTypeManager == nil or g_fruitTypeManager.getFruitTypes == nil then
+ self:debugMessage("FruitTypeManager not available yet; crop list remains empty")
+ self.cropInfo = crops
+ self.selectedCrop = nil
+ return crops
+ end
+
+ for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do
+ if fruitType ~= nil then
+ local fillType = fruitType.fillType
+ local fillTypeIndex = nil
+
+ if fillType ~= nil then
+ fillTypeIndex = fillType.index or fillType.fillTypeIndex
+ end
+
+ fillTypeIndex = fillTypeIndex or fruitType.fillTypeIndex
+
+ if fillTypeIndex ~= nil and usedFillTypes[fillTypeIndex] == nil then
+ usedFillTypes[fillTypeIndex] = true
+
+ local title = self:getFruitTypeDisplayName(fruitType, fillType)
+ local icon = nil
+
+ if fillType ~= nil then
+ icon = fillType.hudOverlayFilename or fillType.iconFilename
+ end
+
+ icon = icon or fruitType.hudOverlayFilename or fruitType.iconFilename or ""
+
+ table.insert(crops, {
+ fruitTypeIndex = fruitType.index,
+ fillTypeIndex = fillTypeIndex,
+ name = fruitType.name or title,
+ title = title,
+ hudOverlayFilename = icon
+ })
+ end
+ end
+ end
+
+ table.sort(crops, function(a, b)
+ return tostring(a.title):lower() < tostring(b.title):lower()
+ end)
+
+ self.cropInfo = crops
+
+ if self.selectedCrop == nil or usedFillTypes[self.selectedCrop] == nil then
+ self.selectedCrop = crops[1] ~= nil and crops[1].fillTypeIndex or nil
+ end
+
+ return crops
+end
+
+
+function YieldTrackerPage:setupCustomButtons()
+
+ self.backButtonInfo = {
+ inputAction = InputAction.MENU_BACK
+ }
+
+ self.showAllFieldButton = {
+ inputAction = InputAction.MENU_ACTIVATE,
+ text = "Show Owned Fields",
+ callback = function ()
+ self.showingOwnedFields = not self.showingOwnedFields
+
+ if self.showingOwnedFields then
+ self.showAllFieldButton.text = "Show All Fields"
+ self:onShowOwnedFieldList() -- Call your actual method
+ else
+ self.showAllFieldButton.text = "Show Owned Fields"
+ self:onShowAllFieldList()
+ end
+
+ self:setMenuButtonInfoDirty() -- Refresh the button text
+ end
+ }
+
+ local info = {
+ self.backButtonInfo,
+ self.showAllFieldButton,
+ }
+
+ self.menuButtons = info
+
+ self:setMenuButtonInfo(self.menuButtons)
+end
+
+
+function YieldTrackerPage:onShowOwnedFieldList()
+ self:getFarmlandIDs()
+ if self.farmlandList then
+ self.farmlandList:reloadData()
+ end
+end
+
+
+function YieldTrackerPage:onShowAllFieldList()
+ self:getFarmlandIDs()
+ if self.farmlandList then
+ self.farmlandList:reloadData()
+ end
+end
+
+
+function YieldTrackerPage:setButtons()
+ local info = {
+ self.backButtonInfo,
+ self.showAllFieldButton,
+ }
+
+ self.menuButtons = info
+
+ self:setMenuButtonInfoDirty()
+end
+
+
+function YieldTrackerPage:onGuiSetupFinished()
+ YieldTrackerPage:superClass().onGuiSetupFinished(self)
+
+ -- custom menu buttons
+ self:setupCustomButtons()
+
+ -- farm id list handling
+ self.farmlandList = self:getDescendantById("farmlandList")
+ if self.farmlandList ~= nil then
+ self.farmlandList:setDataSource(self)
+ self.farmlandList:setDelegate(self)
+ end
+
+ -- crop list/fill type handling
+ self.cropList = self:getDescendantById("cropList")
+ self:refreshCropInfoFromFruitTypeManager()
+ if self.cropList ~= nil then
+ self.cropList:setDataSource(self)
+ self.cropList:setDelegate(self)
+ self.cropList:reloadData()
+ end
+
+ -- users yield history list
+ self.yieldPerYearList = self:getDescendantById("yieldPerYearList")
+ if self.yieldPerYearList ~= nil then
+ self.yieldPerYearList:setDataSource(self)
+ self.yieldPerYearList:setDelegate(self)
+ end
+ self.noYieldHistoryText = self:getDescendantById("noYieldHistoryText")
+ self.yieldStatsInformation = self:getDescendantById("yieldStatsInformation")
+ self.yieldListMode = self:getDescendantById("yieldListMode")
+ self.yieldDetailMode = self:getDescendantById("yieldDetailMode")
+
+ -- Additional GUI elements used by the redesigned XML layout
+ self.yieldListMode = self:getDescendantById("yieldListMode")
+ self.yieldDetailMode = self:getDescendantById("yieldDetailMode")
+ self.noYieldHistoryText = self:getDescendantById("noYieldHistoryText")
+ self.yieldStatsInformation = self:getDescendantById("yieldStatsInformation")
+ self.graphYearTickTemplate = self:getDescendantById("graphYearTickTemplate")
+ self.graphCandleTemplate = self:getDescendantById("graphCandleTemplate")
+
+ -- debuggers
+ self:debugMessage("YieldPerYearList found: " .. tostring(self.yieldPerYearList ~= nil))
+ self:debugMessage("CropList found: " .. tostring(self.cropList ~= nil))
+ self:debugMessage("FarmlandList found: " .. tostring(self.farmlandList ~= nil))
+
+ -- Set initial search param based on self.selectedField and self.selectedCrop
+ self:setSelectedPageData()
+end
+
+
+
+function YieldTrackerPage:onFrameOpen()
+ YieldTrackerPage:superClass().onFrameOpen(self)
+ self:getFarmlandIDs()
+ self:refreshCropInfoFromFruitTypeManager()
+
+ if self.cropList then
+ self.cropList:reloadData()
+ end
+
+ if self.farmlandList then
+ self.farmlandList:reloadData()
+ FocusManager:setFocus(self.farmlandList)
+ else
+ self:debugMessage("Error: farmlandList not found in onFrameOpen")
+ end
+
+ if self.yieldPerYearList then
+ self:setSelectedPageData()
+ self.yieldPerYearList:reloadData()
+ end
+end
+
+
+function YieldTrackerPage:getFarmlandIDs()
+ local farmlandIDs = {}
+ for id, farmland in pairs(g_farmlandManager.farmlands) do
+ if farmland.showOnFarmlandsScreen then
+ local isOwned = farmland.farmId == g_currentMission:getFarmId()
+ if self.showingOwnedFields then
+ if isOwned then
+ table.insert(farmlandIDs, id)
+ end
+ else
+ table.insert(farmlandIDs, id)
+ end
+ end
+ end
+ table.sort(farmlandIDs)
+ self.farmlandIDs = farmlandIDs
+ return farmlandIDs
+end
+
+
+function YieldTrackerPage:onClickCropSelected(element)
+ local selected = self.cropInfo[element.indexInSection]
+ if selected == nil then
+ return
+ end
+
+ self.selectedCrop = selected.fillTypeIndex
+ self:setSelectedPageData()
+
+ -- NEW: switch to detail view when both selected
+ if self.selectedField ~= nil and self.selectedCrop ~= nil then
+ if self.yieldListMode ~= nil then
+ self.yieldListMode:setVisible(false)
+ end
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(true)
+ end
+ end
+end
+
+
+function YieldTrackerPage:onClickFieldSelected(element)
+ self.selectedField = self.farmlandIDs[element.indexInSection]
+ self:setSelectedPageData()
+
+ -- NEW: stay in list until crop is selected
+ if self.yieldListMode ~= nil then
+ self.yieldListMode:setVisible(true)
+ end
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(false)
+ end
+end
+
+
+function YieldTrackerPage:setSelectedPageData()
+ if self.selectedCrop == nil then
+ self.yieldYears = {}
+ return
+ end
+
+ self:initiateFieldCropStatsSearch()
+ self:setOverviewStatsText()
+
+ self:setGraphData()
+end
+
+
+function YieldTrackerPage:initiateFieldCropStatsSearch()
+ local selectedField = self.selectedField
+ local selectedFillTypeIndex = self.selectedCrop
+ local yieldData = YieldTracker.yieldData
+ local currentYear = g_currentMission.environment.currentYear
+ local foundData = false
+ self.yieldYears = {} -- Clear existing data
+
+ if selectedFillTypeIndex == nil or selectedField == nil then
+ return
+ end
+
+ self:debugMessage(string.format("Search stats for fill type index %d on field %d", selectedFillTypeIndex, selectedField))
+
+ local yearAmountMap = {}
+ local minYear = nil
+
+ -- Step 1: Collect data and find min year
+ for year, yearData in pairs(yieldData) do
+ local fieldData = yearData[selectedField]
+ if fieldData then
+ local cropData = fieldData[selectedFillTypeIndex]
+ if cropData then
+ yearAmountMap[year] = cropData.amount
+ minYear = (minYear == nil or year < minYear) and year or minYear
+ foundData = true
+
+ self:debugMessage(string.format(
+ "Year %d - Field %d - Crop: %s - Amount: %d",
+ year, selectedField, cropData.fillTypeTitle, cropData.amount
+ ))
+ else
+ self:debugMessage(string.format("Year %d - Field %d - No data for fill type index %d", year, selectedField, selectedFillTypeIndex))
+ end
+ else
+ self:debugMessage(string.format("Year %d - No data for field %d", year, selectedField))
+ end
+ end
+
+
+ -- Step 2: Fill in all years from startYear (e.g., 1) to maxYear
+ local startYear = 1 -- You can change this to the actual first possible year
+ if minYear ~= nil then
+ local maxYear = currentYear
+ -- Extend maxYear to the latest year found in data if it's greater
+ for year in pairs(yearAmountMap) do
+ if year > maxYear then
+ maxYear = year
+ end
+ end
+
+ for year = startYear, maxYear do
+ local amount = yearAmountMap[year] or 0
+ table.insert(self.yieldYears, {
+ year = year,
+ amount = amount
+ })
+ end
+ end
+
+ -- -- Sort by year ascending
+ -- table.sort(self.yieldYears, function(a, b) return a.year < b.year end)
+ -- Sort by year descending
+ table.sort(self.yieldYears, function(a, b) return a.year > b.year end)
+
+ if self.yieldPerYearList then
+ self.yieldPerYearList:reloadData()
+ end
+
+ -- Show message only if no data found
+ if self.noYieldHistoryText ~= nil then
+ self.noYieldHistoryText:setVisible(not foundData)
+ end
+
+ -- Keep the right detail/graph panel visible even when no data is found.
+ if self.yieldStatsInformation ~= nil then
+ self.yieldStatsInformation:setVisible(true)
+ end
+
+ if self.yieldDetailMode ~= nil then
+ self.yieldDetailMode:setVisible(true)
+ end
+end
+
+
+function YieldTrackerPage:setOverviewStatsText()
+ local cropName = ""
+ local cropIcon = ""
+ local fieldID = self.selectedField or 0
+
+ -- Get crop name from fillTypeIndex
+ for _, crop in ipairs(self.cropInfo) do
+ if crop.fillTypeIndex == self.selectedCrop then
+ cropName = crop.title
+ cropIcon = crop.hudOverlayFilename
+ break
+ end
+ end
+
+ local pageIcon = Utils.getFilename("images/page_header_icon.dds", YieldTrackerPage.dir)
+ local pageHeaderIcon = self:getDescendantById("pageHeaderIcon")
+ pageHeaderIcon:setImageFilename(pageIcon)
+
+ local selectedOverviewData = self:fetchOverviewStats()
+ local selectedFieldNumText = self:getDescendantById("selectedFieldNumText")
+ local selectedCropTypeText = self:getDescendantById("selectedCropTypeText")
+ local selectedCropTypeIcon = self:getDescendantById("selectedCropicon")
+ local selectedCropTotalHarvests = self:getDescendantById("selectedCropTotalHarvests")
+ local selectedCropAverageYield = self:getDescendantById("selectedCropAverageYield")
+ local selectedCropBestYear = self:getDescendantById("selectedCropBestYear")
+
+ if selectedFieldNumText and selectedCropTypeText and selectedCropTypeIcon and selectedCropTotalHarvests and selectedCropAverageYield and selectedCropBestYear then
+ selectedFieldNumText:setText(string.format("%s %d", self.i18n:getText("yt_fieldIDLabel"), fieldID))
+
+ -- Use the already resolved registered fill type title instead of building a custom l10n key.
+ selectedCropTypeText:setText(cropName or "")
+
+ selectedCropTotalHarvests:setText(selectedOverviewData.totalYears)
+ selectedCropAverageYield:setText(string.format("%s l", formatWithCommas(selectedOverviewData.averageYield)))
+
+ selectedCropBestYear:setText(string.format("%s %d --> %s l", self.i18n:getText("yt_yearLabel"), selectedOverviewData.highestYear, formatWithCommas(selectedOverviewData.highestYieldAmount)))
+
+ if cropIcon ~= nil and cropIcon ~= "" then
+ selectedCropTypeIcon:setImageFilename(cropIcon)
+ end
+ end
+end
+
+
+function YieldTrackerPage:fetchOverviewStats()
+ local totalYears = 0
+ local totalAmount = 0
+ local totalEntries = 0
+ local highestYieldAmount = 0
+ local highestYear = 0
+
+ for _, entry in ipairs(self.yieldYears) do
+ local yearYieldAmount = entry.amount or 0
+
+ if yearYieldAmount > 0 then
+ totalAmount = totalAmount + yearYieldAmount
+ totalEntries = totalEntries + 1
+ totalYears = totalYears + 1
+
+ if yearYieldAmount > highestYieldAmount then
+ highestYieldAmount = yearYieldAmount
+ highestYear = entry.year
+ end
+ end
+ end
+
+ local averageYield = totalEntries > 0 and (totalAmount / totalEntries) or 0
+
+ return {
+ totalYears = totalYears,
+ averageYield = averageYield,
+ highestYear = highestYear,
+ highestYieldAmount = highestYieldAmount
+ }
+end
+
+
+-- SET GRAPH FUNCTIONALITY
+function YieldTrackerPage:clearOldElements(container, template)
+ for i = #container.elements, 1, -1 do
+ local child = container.elements[i]
+ if child ~= template then
+ container:removeElement(child)
+ end
+ end
+end
+
+
+function YieldTrackerPage:extractYears(data)
+ local years = {}
+ for _, harvest in ipairs(data) do
+ if harvest.year then
+ table.insert(years, harvest.year)
+ end
+ end
+ table.sort(years)
+ return years
+end
+
+
+function YieldTrackerPage:calculateDisplayRange(minYear, maxYear)
+ local rangeYears = maxYear - minYear + 1
+ local displayMinYear, displayMaxYear
+ if rangeYears >= 20 then
+ displayMaxYear = maxYear
+ displayMinYear = displayMaxYear - 19
+ else
+ displayMinYear = minYear
+ displayMaxYear = displayMinYear + 19
+ end
+ return displayMinYear, displayMaxYear
+end
+
+
+function YieldTrackerPage:createHarvestLookup(data, minYear, maxYear)
+ local lookup = {}
+ for _, harvest in ipairs(data) do
+ if harvest.year and harvest.year >= minYear and harvest.year <= maxYear then
+ lookup[harvest.year] = harvest
+ end
+ end
+ return lookup
+end
+
+
+function YieldTrackerPage:findMaxYield(harvestByYear, minYear, maxYear)
+ local maxYield = 0
+ for year = minYear, maxYear do
+ local h = harvestByYear[year]
+ if h and h.amount and h.amount > maxYield then
+ maxYield = h.amount
+ end
+ end
+ return maxYield
+end
+
+function YieldTrackerPage:findMinYield(harvestByYear, startYear, endYear)
+ local min = math.huge
+ for year = startYear, endYear do
+ local data = harvestByYear[year]
+ if data and data.amount and data.amount < min then
+ min = data.amount
+ end
+ end
+ return min
+end
+
+
+function YieldTrackerPage:setGraphData()
+ if not self.yieldYears or #self.yieldYears == 0 then return end
+
+ local tickContainer = self:getDescendantById("graphYearTickContainer")
+ local barGraph = self:getDescendantById("barGraph")
+
+ if tickContainer == nil or barGraph == nil or self.graphYearTickTemplate == nil or self.graphCandleTemplate == nil then
+ self:debugMessage("Graph elements are missing; skipping graph update")
+ return
+ end
+
+ self:clearOldElements(tickContainer, self.graphYearTickTemplate)
+ self:clearOldElements(barGraph, self.graphCandleTemplate)
+
+ local years = self:extractYears(self.yieldYears)
+ if #years == 0 then return end
+
+ local actualMinYear = years[1]
+ local actualMaxYear = years[#years]
+
+ local displayMinYear, displayMaxYear = self:calculateDisplayRange(actualMinYear, actualMaxYear)
+ local harvestByYear = self:createHarvestLookup(self.yieldYears, displayMinYear, displayMaxYear)
+
+ local maxYield = self:findMaxYield(harvestByYear, displayMinYear, displayMaxYear)
+
+ local numTicks = displayMaxYear - displayMinYear + 1
+
+ -- Matches the cleaned right-side XML graph width. Keep left selection layout untouched.
+ local graphPaddingX = 0.012
+ local graphContentWidth = 0.376
+ local spacing = graphContentWidth / math.max(1, numTicks - 1)
+ local candleWidth = 0.011
+ local minHeight = 0.035
+ local maxHeight = 0.215
+
+ for year = displayMinYear, displayMaxYear do
+ local tickX = graphPaddingX + (year - displayMinYear) * spacing
+
+ local tickClone = self.graphYearTickTemplate:clone(tickContainer)
+ tickClone:setText(tostring(year))
+ tickClone:setPosition(tickX, 0)
+ tickClone:setVisible(true)
+
+ local harvest = harvestByYear[year]
+ if harvest then
+ local yearYieldAmount = harvest.amount or 0
+
+ if yearYieldAmount > 0 then
+ local candleClone = self.graphCandleTemplate:clone(barGraph)
+ local heightRatio = maxYield > 0 and (yearYieldAmount / maxYield) or 0
+ heightRatio = math.max(0, math.min(1, heightRatio))
+ local candleHeight = minHeight + heightRatio * (maxHeight - minHeight)
+
+ candleClone:setSize(candleWidth, candleHeight)
+ candleClone:setPosition(tickX - (candleWidth * 0.5), 0)
+ candleClone:setVisible(true)
+ end
+ end
+ end
+end
+-- SET GRAPH FUNCTIONALITY - END
+
+
+function YieldTrackerPage:populateCellForItemInSection(list, sectionIndex, index, listItem)
+ if list == self.farmlandList then
+ local farmlandID = self.farmlandIDs[index]
+ local fieldIDListText = listItem:getAttribute("fieldIDListText")
+ if fieldIDListText then
+ fieldIDListText:setText(string.format("%s %d", self.i18n:getText("yt_fieldIDLabel"), farmlandID))
+ end
+ elseif list == self.cropList then
+ local crop = self.cropInfo[index]
+ if crop then
+ local cropListText = listItem:getAttribute("cropListText")
+ local icon = listItem:getAttribute("icon")
+
+ if cropListText then
+ -- Use the registered fill type title directly. No custom translation entry required.
+ cropListText:setText(crop.title or crop.name or "")
+ end
+
+ if icon and crop.hudOverlayFilename ~= nil and crop.hudOverlayFilename ~= "" then
+ icon:setImageFilename(crop.hudOverlayFilename)
+ end
+ end
+ elseif list == self.yieldPerYearList then
+ local entry = self.yieldYears[index]
+ if entry then
+ local yearText = listItem:getDescendantByName("year")
+ local amountText = listItem:getDescendantByName("yieldAmt")
+ if yearText then
+ yearText:setText(string.format("%s %s", self.i18n:getText("yt_yearLabel"), tostring(entry.year)))
+ end
+ if amountText then
+ amountText:setText(formatWithCommas(entry.amount) .. " l")
+ end
+ end
+ end
+end
+
+
+
+function YieldTrackerPage:getCellTypeForItemInSection(_, sectionIndex, index)
+ return "default"
+end
+
+
+function YieldTrackerPage:getNumberOfItemsInSection(list, sectionIndex)
+ if list == self.farmlandList then
+ return #self.farmlandIDs
+ elseif list == self.cropList then
+ return #self.cropInfo
+ elseif list == self.yieldPerYearList then
+ return #self.yieldYears
+ end
+ return 0
+end
+
+
+
+function YieldTrackerPage:debugMessage(message)
+ if self.debug then
+ print(PRINT_PREFIX .. message)
+ end
+end
+
+
+-- DEBUG
+-- function dump(o, indent, depth)
+-- indent = indent or ""
+-- depth = depth or 1
+
+-- if type(o) ~= "table" then
+-- return tostring(o)
+-- elseif depth <= 0 then
+-- return "{...}"
+-- end
+
+-- local s = "{\n"
+-- for k, v in pairs(o) do
+-- s = s .. indent .. " [" .. tostring(k) .. "] = "
+-- if type(v) == "table" then
+-- s = s .. dump(v, indent .. " ", depth - 1)
+-- else
+-- s = s .. tostring(v)
+-- end
+-- s = s .. "\n"
+-- end
+-- return s .. indent .. "}"
+-- end
diff --git a/FS25_YieldTracker/scripts/trackedCropTypes.lua b/FS25_YieldTracker/scripts/trackedCropTypes.lua
index 86bc399..ada02db 100644
--- a/FS25_YieldTracker/scripts/trackedCropTypes.lua
+++ b/FS25_YieldTracker/scripts/trackedCropTypes.lua
@@ -1,42 +1,44 @@
--- Author: BitBarn Mods
--- Date: 06-11-2025
--- Version: 1.0.0.0
--- trackedCropTypes.lua
-
-TrackedCropTypes = {
- { title = "Barley", fillTypeIndex = 3, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_barley.png"},
- { title = "Canola", fillTypeIndex = 5, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_canola.png"},
- { title = "Chaff", fillTypeIndex = 118, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_chaff.png"},
- { title = "Corn", fillTypeIndex = 9, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_maize.png"},
- { title = "Cotton", fillTypeIndex = 15, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_cotton.png"},
- { title = "Grass", fillTypeIndex = 28, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_grass.png"},
- { title = "Green Beans", fillTypeIndex = 18, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_greenbean.png"},
- { title = "Hay", fillTypeIndex = 30, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_dryGrass_windrow.png"},
- { title = "Long Grain Rice", fillTypeIndex = 16, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_riceLongGrain.png"},
- { title = "Oat", fillTypeIndex = 4, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_oat.png"},
- { title = "Peas", fillTypeIndex = 19, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_pea.png"},
- { title = "Potatoes", fillTypeIndex = 10, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_potato.png"},
- { title = "Rice", fillTypeIndex = 17, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_rice.png"},
- { title = "Sorghum", fillTypeIndex = 6, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sorghum.png"},
- { title = "Soybeans", fillTypeIndex = 8, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_soyBean.png"},
- { title = "Spinach", fillTypeIndex = 20, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_spinach.png"},
- { title = "Straw", fillTypeIndex = 31, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_straw.png"},
- { title = "Sunflowers", fillTypeIndex = 7, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sunflower.png"},
- { title = "Wheat", fillTypeIndex = 2, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_wheat.png"},
-}
-
--- TODO: NEEDS SEPERATE FILL TRACKING METHOD TO ACCURATELY TRACK THESES FILL TYPES (expected in v1.0.1.0)
--- { title = "Carrots", fillTypeIndex = 13, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_carrot.png" },
--- { title = "Red Beet", fillTypeIndex = 12, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_beetRoot.png" },
--- { title = "Parsnips", fillTypeIndex = 14, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_parsnip.png" },
--- { title = "Sugar Beet", fillTypeIndex = 11, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sugarBeet.png" },
--- { title = "Sugarcane", fillTypeIndex = 21, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sugarCane.png" },
-
-
--- BUG: GRAPE/OLIVE HARVESTERS NOT BEING TRACKED
--- { title = "Olives", fillTypeIndex = 38, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_olive.png" },
--- { title = "Grapes", fillTypeIndex = 37, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_grape.png" },
-
-
--- DEPRACTED: NOT A HARVESTABLE CROP, USED AS FERT
+-- Author: BitBarn Mods
+-- Date: 06-11-2025
+-- Version: 1.0.0.1
+-- trackedCropTypes.lua
+
+TrackedCropTypes = {
+ { title = "Barley", fillTypeIndex = 3, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_barley.png"},
+ { title = "Canola", fillTypeIndex = 5, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_canola.png"},
+ { title = "Chaff", fillTypeIndex = 118, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_chaff.png"},
+ { title = "Corn", fillTypeIndex = 9, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_maize.png"},
+ { title = "Cotton", fillTypeIndex = 15, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_cotton.png"},
+ { title = "Grass", fillTypeIndex = 28, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_grass.png"},
+ { title = "Green Beans", fillTypeIndex = 18, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_greenbean.png"},
+ { title = "Hay", fillTypeIndex = 30, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_dryGrass_windrow.png"},
+ { title = "Long Grain Rice", fillTypeIndex = 16, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_riceLongGrain.png"},
+ { title = "Oat", fillTypeIndex = 4, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_oat.png"},
+ { title = "Peas", fillTypeIndex = 19, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_pea.png"},
+ { title = "Potatoes", fillTypeIndex = 10, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_potato.png"},
+ { title = "Rice", fillTypeIndex = 17, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_rice.png"},
+ { title = "Sorghum", fillTypeIndex = 6, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sorghum.png"},
+ { title = "Soybeans", fillTypeIndex = 8, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_soyBean.png"},
+ { title = "Spinach", fillTypeIndex = 20, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_spinach.png"},
+ { title = "Straw", fillTypeIndex = 31, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_straw.png"},
+ { title = "Sunflowers", fillTypeIndex = 7, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sunflower.png"},
+ { title = "Wheat", fillTypeIndex = 2, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_wheat.png"},
+ { title = "Spelt", fillTypeIndex = 183, hudOverlayFilename = "$moddir$/NFMarsch/config/huds/hud_fill_spelt.dds"},
+ { title = "Rye", fillTypeIndex = 184, hudOverlayFilename = "$moddir$/NFMarsch/config/huds/hud_fill_rye.dds"},
+}
+
+-- TODO: NEEDS SEPERATE FILL TRACKING METHOD TO ACCURATELY TRACK THESES FILL TYPES (expected in v1.0.1.0)
+-- { title = "Carrots", fillTypeIndex = 13, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_carrot.png" },
+-- { title = "Red Beet", fillTypeIndex = 12, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_beetRoot.png" },
+-- { title = "Parsnips", fillTypeIndex = 14, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_parsnip.png" },
+-- { title = "Sugar Beet", fillTypeIndex = 11, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sugarBeet.png" },
+-- { title = "Sugarcane", fillTypeIndex = 21, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_sugarCane.png" },
+
+
+-- BUG: GRAPE/OLIVE HARVESTERS NOT BEING TRACKED
+-- { title = "Olives", fillTypeIndex = 38, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_olive.png" },
+-- { title = "Grapes", fillTypeIndex = 37, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_grape.png" },
+
+
+-- DEPRACTED: NOT A HARVESTABLE CROP, USED AS FERT
-- { title = "Oilseed Radish", fillTypeIndex = 121, hudOverlayFilename = "dataS/menu/hud/fillTypes/hud_fill_oilRadish.png" },
\ No newline at end of file
diff --git a/FS25_YieldTracker/scripts/yieldTracker.lua b/FS25_YieldTracker/scripts/yieldTracker.lua
index c81f492..97e2c3e 100644
--- a/FS25_YieldTracker/scripts/yieldTracker.lua
+++ b/FS25_YieldTracker/scripts/yieldTracker.lua
@@ -12,56 +12,17 @@ YieldTracker = {
saveFileName = "yieldTrackerData.xml",
}
YieldTracker.debug = false
+YieldTracker.debugHarvest = false
---- Current active crop fill types that are tracked
-source(g_currentModDirectory .. "scripts/trackedCropTypes.lua") --> TrackedCropTypes
-local trackedCropTypes = TrackedCropTypes
-
+--- Active crop fill types are resolved dynamically from g_fruitTypeManager.
-- Called every frame to update yield tracking for all combines in the current mission.
-- Iterates over all vehicles, checks for combines, determines active fill types, and tracks their fill units.
function YieldTracker:updateByFrame()
- local currentVehicles = {}
-
- for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
- if vehicle.spec_combine then
- -- Track current vehicle ids
- currentVehicles[vehicle.id] = true
-
- local cutterInputFillType, cutterOutputFillType = self:getCutterStatus(vehicle)
- local hasInternalFillUnit, combineFillType, activeFillLevel = self:getCombineStatus(vehicle)
- local fieldFillType = self:getFieldFillTypeFallback(vehicle)
-
- local activeFillType = hasInternalFillUnit
- and self:getActiveFillType(cutterInputFillType, combineFillType, fieldFillType)
- or self:getActiveFillType(cutterOutputFillType, combineFillType, fieldFillType)
-
- local isBaler = false
- self:trackFillUnit(vehicle, activeFillType, activeFillLevel, isBaler)
-
- end
-
- if vehicle.spec_baler then
- currentVehicles[vehicle.id] = true
- local balerFillType = vehicle:getFillUnitFillType(1)
- local balerFillLevel = vehicle:getFillUnitFillLevel(1)
- local isbaler = true
- self:trackFillUnit(vehicle, balerFillType, balerFillLevel, isbaler)
- end
-
- end
-
- -- Remove vehicles from prevFillLevels that are no longer in game/sold
- if self.prevFillLevels then
- for vehicleId in pairs(self.prevFillLevels) do
- if not currentVehicles[vehicleId] then
- self.prevFillLevels[vehicleId] = nil
- self:debugMessage(string.format("Removed stale vehicle %d from tracking", vehicleId))
- end
- end
- end
+ -- Harvest amounts are recorded by Combine.onUpdateTick tank deltas and Dischargeable.onUpdateTick pipe deltas.
+ -- The old per-frame scan is intentionally disabled.
end
@@ -132,6 +93,289 @@ end
+-- EVENT-BASED HARVEST TRACKING
+--=======================================================================================================================================================================
+
+function YieldTracker:installFillUnitHook()
+ self:debugHarvestMessage("installFillUnitHook() called")
+
+ if self.fillUnitHookInstalled then
+ self:debugHarvestMessage("installFillUnitHook() skipped: already installed")
+ return
+ end
+
+ self:debugHarvestMessage("Vehicle exists=" .. tostring(Vehicle ~= nil))
+ self:debugHarvestMessage("Vehicle.addFillUnitFillLevel exists=" .. tostring(Vehicle ~= nil and Vehicle.addFillUnitFillLevel ~= nil))
+ self:debugHarvestMessage("FillUnit exists=" .. tostring(FillUnit ~= nil))
+ self:debugHarvestMessage("FillUnit.addFillUnitFillLevel exists=" .. tostring(FillUnit ~= nil and FillUnit.addFillUnitFillLevel ~= nil))
+
+ local tracker = self
+ local installedAnyHook = false
+
+ if Vehicle ~= nil and Vehicle.addFillUnitFillLevel ~= nil then
+ Vehicle.addFillUnitFillLevel = Utils.overwrittenFunction(Vehicle.addFillUnitFillLevel, function(vehicle, superFunc, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_CALL Vehicle.addFillUnitFillLevel vehicle='%s' vehicleId=%s farmId=%s fillUnitIndex=%s delta=%s fillTypeIndex=%s",
+ tracker:getVehicleDebugName(vehicle),
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(farmId),
+ tostring(fillUnitIndex),
+ tostring(fillLevelDelta),
+ tostring(fillTypeIndex)
+ ))
+
+ local oldFillLevel = nil
+ if vehicle ~= nil and fillUnitIndex ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ oldFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or 0
+ end
+
+ local result = superFunc(vehicle, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+
+ local appliedAmount = fillLevelDelta
+ if type(result) == "number" then
+ appliedAmount = result
+ elseif oldFillLevel ~= nil and vehicle ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ local newFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or oldFillLevel
+ local measuredDelta = newFillLevel - oldFillLevel
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_AFTER Vehicle.addFillUnitFillLevel old=%s new=%s measuredDelta=%s result=%s",
+ tostring(oldFillLevel), tostring(newFillLevel), tostring(measuredDelta), tostring(result)
+ ))
+ if measuredDelta > 0 then
+ appliedAmount = measuredDelta
+ end
+ end
+
+ tracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, appliedAmount, fillTypeIndex)
+ return result
+ end)
+
+ installedAnyHook = true
+ self:debugHarvestMessage("Installed hook: Vehicle.addFillUnitFillLevel")
+ end
+
+ -- Additional trace hook in case FS25 calls the specialization table directly instead of Vehicle.*
+ if FillUnit ~= nil and FillUnit.addFillUnitFillLevel ~= nil and (Vehicle == nil or FillUnit.addFillUnitFillLevel ~= Vehicle.addFillUnitFillLevel) then
+ FillUnit.addFillUnitFillLevel = Utils.overwrittenFunction(FillUnit.addFillUnitFillLevel, function(vehicle, superFunc, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_CALL FillUnit.addFillUnitFillLevel vehicle='%s' vehicleId=%s farmId=%s fillUnitIndex=%s delta=%s fillTypeIndex=%s",
+ tracker:getVehicleDebugName(vehicle),
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(farmId),
+ tostring(fillUnitIndex),
+ tostring(fillLevelDelta),
+ tostring(fillTypeIndex)
+ ))
+
+ local oldFillLevel = nil
+ if vehicle ~= nil and fillUnitIndex ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ oldFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or 0
+ end
+
+ local result = superFunc(vehicle, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+
+ local appliedAmount = fillLevelDelta
+ if type(result) == "number" then
+ appliedAmount = result
+ elseif oldFillLevel ~= nil and vehicle ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ local newFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or oldFillLevel
+ local measuredDelta = newFillLevel - oldFillLevel
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_AFTER FillUnit.addFillUnitFillLevel old=%s new=%s measuredDelta=%s result=%s",
+ tostring(oldFillLevel), tostring(newFillLevel), tostring(measuredDelta), tostring(result)
+ ))
+ if measuredDelta > 0 then
+ appliedAmount = measuredDelta
+ end
+ end
+
+ tracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, appliedAmount, fillTypeIndex)
+ return result
+ end)
+
+ installedAnyHook = true
+ self:debugHarvestMessage("Installed hook: FillUnit.addFillUnitFillLevel")
+ end
+
+ if not installedAnyHook then
+ self:debugHarvestMessage("ERROR: no fill unit hook installed; no supported addFillUnitFillLevel function found")
+ self:debugMessage("Could not install fill unit hook: no addFillUnitFillLevel function found")
+ return
+ end
+
+ self.fillUnitHookInstalled = true
+ self:debugHarvestMessage("installFillUnitHook() finished")
+ self:debugMessage("Installed fill unit harvest hook")
+end
+
+function YieldTracker:getVehicleDebugName(vehicle)
+ if vehicle == nil then
+ return "nil"
+ end
+
+ if vehicle.getName ~= nil then
+ local ok, name = pcall(function() return vehicle:getName() end)
+ if ok and name ~= nil and name ~= "" then
+ return tostring(name)
+ end
+ end
+
+ if vehicle.configFileName ~= nil then
+ return tostring(vehicle.configFileName)
+ end
+
+ if vehicle.typeName ~= nil then
+ return tostring(vehicle.typeName)
+ end
+
+ return tostring(vehicle)
+end
+
+function YieldTracker:debugHarvestMessage(message)
+ if self.debugHarvest then
+ print(PRINT_PREFIX .. "HARVEST_DEBUG - " .. tostring(message))
+ end
+end
+
+function YieldTracker:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, amount, reason)
+ if not self.debugHarvest then
+ return
+ end
+
+ local vehicleName = self:getVehicleDebugName(vehicle)
+ local fillTypeName = "nil"
+ if fillTypeIndex ~= nil and g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil then
+ local desc = g_fillTypeManager.indexToFillType[fillTypeIndex]
+ if desc ~= nil then
+ fillTypeName = tostring(desc.title or desc.name or fillTypeIndex)
+ else
+ fillTypeName = tostring(fillTypeIndex)
+ end
+ end
+
+ self:debugHarvestMessage(string.format(
+ "vehicle='%s' vehicleId=%s combine=%s baler=%s field=%s fillType=%s(%s) amount=%.2f reason=%s",
+ vehicleName,
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(vehicle ~= nil and vehicle.spec_combine ~= nil),
+ tostring(vehicle ~= nil and vehicle.spec_baler ~= nil),
+ tostring(fieldID),
+ fillTypeName,
+ tostring(fillTypeIndex),
+ tonumber(amount or 0) or 0,
+ tostring(reason)
+ ))
+end
+
+function YieldTracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, fillLevelDelta, fillTypeIndex)
+ local rawAmount = tonumber(fillLevelDelta or 0) or 0
+
+ if not self.yieldDataLoaded then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_yieldData_not_loaded")
+ return
+ end
+
+ if vehicle == nil or fillUnitIndex == nil or fillLevelDelta == nil or rawAmount <= 0 then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_invalid_or_non_positive_delta")
+ return
+ end
+
+ local isCombine = vehicle.spec_combine ~= nil
+ local isBaler = vehicle.spec_baler ~= nil
+
+ -- Count only where crop is generated. Ignore trailer/silo/transfer additions.
+ if not isCombine and not isBaler then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_not_combine_or_baler")
+ return
+ end
+
+ local fieldID = self:getHarvestFieldID(vehicle, isBaler)
+
+ if isCombine and not self:getIsCombineHarvesting(vehicle) then
+ self:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, rawAmount, "SKIP_combine_not_harvesting")
+ return
+ end
+
+ if isBaler and not self:getIsBalerHarvesting(vehicle) then
+ self:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, rawAmount, "SKIP_baler_not_harvesting")
+ return
+ end
+
+ local activeFillType = fillTypeIndex
+ if (activeFillType == nil or activeFillType == FillType.UNKNOWN) and vehicle.getFillUnitFillType ~= nil then
+ activeFillType = vehicle:getFillUnitFillType(fillUnitIndex)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_active_fillType")
+ return
+ end
+
+ if not self:isTrackedFillType(activeFillType) then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_fillType_not_registered_fruit")
+ return
+ end
+
+ if fieldID == nil then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_fieldID")
+ return
+ end
+
+ local fillTypeDesc = g_fillTypeManager.indexToFillType[activeFillType]
+ if fillTypeDesc == nil then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_fillTypeDesc")
+ return
+ end
+
+ local currentYear = g_currentMission.environment.currentYear
+ self:recordHarvest(currentYear, fieldID, fillTypeDesc.title, rawAmount, activeFillType)
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "RECORDED")
+end
+
+function YieldTracker:getIsCombineHarvesting(vehicle)
+ if vehicle == nil or vehicle.spec_combine == nil then
+ return false
+ end
+
+ local spec = vehicle.spec_combine
+ if spec.attachedCutters ~= nil then
+ for _, cutter in pairs(spec.attachedCutters) do
+ if cutter ~= nil and cutter.spec_cutter ~= nil and cutter.spec_cutter.isWorking then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
+function YieldTracker:getIsBalerHarvesting(vehicle)
+ if vehicle == nil or vehicle.spec_baler == nil then
+ return false
+ end
+
+ if vehicle.getIsTurnedOn ~= nil and vehicle:getIsTurnedOn() then
+ return true
+ end
+
+ if vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() > 0.5 then
+ return true
+ end
+
+ return true
+end
+
+function YieldTracker:getHarvestFieldID(vehicle, isBaler)
+ if isBaler then
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ return g_farmlandManager:getFarmlandIdAtWorldPosition(x, z)
+ end
+
+ return self:getCutterFieldID(vehicle)
+end
+
-- UTIL METHODS
--=======================================================================================================================================================================
@@ -252,11 +496,29 @@ end
-- Checks if a given fill type index is in the tracked crop types.
function YieldTracker:isTrackedFillType(fillTypeIndex)
- for _, crop in ipairs(trackedCropTypes) do
- if crop.fillTypeIndex == fillTypeIndex then
- return true
+ if fillTypeIndex == nil then
+ return false
+ end
+
+ if g_fruitTypeManager == nil or g_fruitTypeManager.getFruitTypes == nil then
+ return false
+ end
+
+ for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do
+ if fruitType ~= nil then
+ local fillType = fruitType.fillType
+ local fruitFillTypeIndex = fruitType.fillTypeIndex
+
+ if fillType ~= nil then
+ fruitFillTypeIndex = fillType.index or fillType.fillTypeIndex or fruitFillTypeIndex
+ end
+
+ if fruitFillTypeIndex == fillTypeIndex then
+ return true
+ end
end
end
+
return false
end
@@ -297,129 +559,1183 @@ end
--- SAVE/LOAD METHODS
---=======================================================================================================================================================================
--- Call required dependencies upon loadmap from main.lua
-function YieldTracker:setup()
- self:loadYieldData()
- self:hookInGameSaveEvent()
-end
+-- COMBINE FILL LEVEL DELTA DEBUG HOOK
+--=======================================================================================================================================================================
+function YieldTracker:installCombineFillLevelDeltaDebugHook()
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() called")
+ if self.combineFillLevelDeltaDebugHookInstalled then
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() skipped: already installed")
+ return
+ end
--- Hooks into the save event to trigger yield data saving on every game save.
-function YieldTracker:hookInGameSaveEvent()
- FSCareerMissionInfo.saveToXMLFile = Utils.appendedFunction(
- FSCareerMissionInfo.saveToXMLFile,
- function() YieldTracker:saveYieldData() end)
-end
+ self.combineFillLevelDebugCallCount = self.combineFillLevelDebugCallCount or {}
+ self.combineFillLevelDebugMaxIdleLogsPerVehicle = 25
+ self.combinePreviousFillLevels = self.combinePreviousFillLevels or {}
+ self.combinePreviousFillTypes = self.combinePreviousFillTypes or {}
+ self:debugHarvestMessage("DELTA_EXISTS Combine=" .. tostring(Combine ~= nil))
+ self:debugHarvestMessage("DELTA_EXISTS Combine.onUpdateTick=" .. tostring(Combine ~= nil and type(Combine.onUpdateTick) == "function"))
+ self:debugHarvestMessage("DELTA_EXISTS Combine.updateTick=" .. tostring(Combine ~= nil and type(Combine.updateTick) == "function"))
--- Loads yield tracking data and vehicle baseline fill levels from an XML file in the current savegame directory.
--- Rebuilds the yieldData structure by year, field, and crop, and restores previous fill levels per vehicle.
--- If no file exists or loading fails, initializes with empty data and marks data as loaded.
-function YieldTracker:loadYieldData()
- if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
- self:debugMessage("Load skipped: no savegame directory")
- self.yieldDataLoaded = true
+ if Combine == nil or type(Combine.onUpdateTick) ~= "function" then
+ self:debugHarvestMessage("ERROR: Combine.onUpdateTick not found; combine delta debug hook not installed")
return
end
- local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
- if not fileExists(filePath) then
- self:debugMessage("No existing yield data XML found")
- self.yieldDataLoaded = true
- return
+ local tracker = self
+
+ Combine.onUpdateTick = Utils.overwrittenFunction(Combine.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local result = superFunc(vehicle, dt, ...)
+
+ -- Compare against the previous onUpdateTick call, not against a value
+ -- read earlier in the same tick. In FS25 the combine fill level can
+ -- already be updated before Combine.onUpdateTick is entered.
+ local currentLevel, currentFillType, fillUnitIndex = tracker:getCombineInternalFillState(vehicle)
+ local vehicleId = vehicle ~= nil and (vehicle.id or tostring(vehicle)) or nil
+ local key = nil
+
+ if vehicleId ~= nil and fillUnitIndex ~= nil then
+ key = tostring(vehicleId) .. ":" .. tostring(fillUnitIndex)
+ end
+
+ local previousLevel = nil
+ local previousFillType = nil
+
+ if key ~= nil then
+ previousLevel = tracker.combinePreviousFillLevels[key]
+ previousFillType = tracker.combinePreviousFillTypes[key]
+ end
+
+ local delta = nil
+ if previousLevel ~= nil and currentLevel ~= nil then
+ delta = currentLevel - previousLevel
+ end
+
+ tracker:handleCombineFillLevelDeltaDebug(vehicle, fillUnitIndex, previousLevel, currentLevel, delta, previousFillType, currentFillType)
+
+ -- Update baseline only after logging/recording.
+ if key ~= nil and currentLevel ~= nil then
+ tracker.combinePreviousFillLevels[key] = currentLevel
+ tracker.combinePreviousFillTypes[key] = currentFillType
+ end
+
+ return result
+ end)
+
+ self.combineFillLevelDeltaDebugHookInstalled = true
+ self:debugHarvestMessage("DELTA_INSTALLED Combine.onUpdateTick")
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() finished")
+end
+
+function YieldTracker:getCombineInternalFillState(vehicle)
+ if vehicle == nil or vehicle.spec_combine == nil then
+ return nil, nil, nil
end
- local xmlFile = loadXMLFile("YieldDataXML", filePath)
- if not xmlFile then
- self:debugMessage("Failed to load yield data XML")
- return
+ local spec = vehicle.spec_combine
+ local fillUnitIndex = spec.fillUnitIndex
+ if fillUnitIndex == nil then
+ return nil, nil, nil
end
- self.yieldData = {}
+ local fillLevel = nil
+ local fillType = nil
- local i = 0
- while true do
- local yearKey = string.format("yieldData.year(%d)", i)
- if not hasXMLProperty(xmlFile, yearKey) then break end
+ if vehicle.getFillUnitFillLevel ~= nil then
+ fillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex)
+ end
- local year = getXMLInt(xmlFile, yearKey .. "#value")
- self.yieldData[year] = {}
+ if vehicle.getFillUnitFillType ~= nil then
+ fillType = vehicle:getFillUnitFillType(fillUnitIndex)
+ end
- local j = 0
- while true do
- local fieldKey = string.format("%s.field(%d)", yearKey, j)
- if not hasXMLProperty(xmlFile, fieldKey) then break end
+ return fillLevel, fillType, fillUnitIndex
+end
- local fieldId = getXMLInt(xmlFile, fieldKey .. "#id")
- self.yieldData[year][fieldId] = {}
+function YieldTracker:handleCombineFillLevelDeltaDebug(vehicle, fillUnitIndex, oldLevel, newLevel, delta, oldFillType, newFillType)
+ if not self.debugHarvest then
+ return
+ end
- local k = 0
- while true do
- local cropKey = string.format("%s.crop(%d)", fieldKey, k)
- if not hasXMLProperty(xmlFile, cropKey) then break end
+ if vehicle == nil then
+ self:debugHarvestMessage("DELTA_SKIP vehicle=nil")
+ return
+ end
- local cropName = getXMLString(xmlFile, cropKey .. "#name")
- local amount = getXMLFloat(xmlFile, cropKey .. "#amount")
- local fillTypeIndex = getXMLInt(xmlFile, cropKey .. "#fillTypeIndex")
+ local vehicleId = vehicle.id or tostring(vehicle)
+ self.combineFillLevelDebugCallCount[vehicleId] = (self.combineFillLevelDebugCallCount[vehicleId] or 0) + 1
- self.yieldData[year][fieldId][fillTypeIndex] = {
- amount = amount,
- fillTypeTitle = cropName
- }
+ local fieldID = self:getCutterFieldID(vehicle)
+ local cutterInputFillType, cutterOutputFillType = self:getCutterStatus(vehicle)
+ local activeFillType = self:getActiveFillType(newFillType, cutterOutputFillType, cutterInputFillType)
- k = k + 1
- end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
- j = j + 1
- end
+ local fillTypeTitle = self:getFillTypeDebugTitle(activeFillType)
+ local vehicleName = self:getVehicleDebugName(vehicle)
+ local amount = tonumber(delta or 0) or 0
- i = i + 1
+ if newLevel == nil then
+ self:debugHarvestMessage(string.format(
+ "DELTA_SKIP vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%s new=%s delta=%s activeFillType=%s(%s) reason=no_current_fill_state",
+ vehicleName, tostring(vehicleId), tostring(fieldID), tostring(fillUnitIndex), tostring(oldLevel), tostring(newLevel), tostring(delta), fillTypeTitle, tostring(activeFillType)
+ ))
+ return
end
- -- Load prevFillLevels baseline per vehicle
- self.prevFillLevels = {}
- local v = 0
- while true do
- local vehicleKey = string.format("yieldData.prevFillLevels.vehicle(%d)", v)
- if not hasXMLProperty(xmlFile, vehicleKey) then break end
+ if oldLevel == nil then
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA_BASELINE vehicle='%s' vehicleId=%s field=%s fillUnit=%s current=%.2f newFillType=%s activeFillType=%s(%s) reason=baseline_initialized",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(newLevel or 0) or 0,
+ tostring(newFillType),
+ fillTypeTitle,
+ tostring(activeFillType)
+ ))
+ return
+ end
- local vehicleId = getXMLInt(xmlFile, vehicleKey .. "#id")
- local level = getXMLFloat(xmlFile, vehicleKey .. "#level")
+ if amount > 0.001 then
+ local reason = "DELTA_POSITIVE"
+ local recorded = false
+
+ if not self.yieldDataLoaded then
+ reason = "DELTA_POSITIVE_SKIP_yieldData_not_loaded"
+ elseif fieldID == nil then
+ reason = "DELTA_POSITIVE_SKIP_no_fieldID"
+ elseif activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ reason = "DELTA_POSITIVE_SKIP_no_active_fillType"
+ elseif not self:isTrackedFillType(activeFillType) then
+ reason = "DELTA_POSITIVE_SKIP_fillType_not_registered_fruit"
+ else
+ local fillTypeDesc = g_fillTypeManager.indexToFillType[activeFillType]
+ local cropName = fillTypeDesc ~= nil and (fillTypeDesc.title or fillTypeDesc.name or tostring(activeFillType)) or tostring(activeFillType)
+ local currentYear = g_currentMission.environment.currentYear
+ self:recordHarvest(currentYear, fieldID, cropName, amount, activeFillType)
+ reason = "DELTA_POSITIVE_RECORDED"
+ recorded = true
+ end
- if vehicleId and level then
- self.prevFillLevels[vehicleId] = level
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%.2f new=%.2f delta=%.2f oldFillType=%s newFillType=%s cutterInput=%s cutterOutput=%s activeFillType=%s(%s) recorded=%s reason=%s",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(oldLevel or 0) or 0,
+ tonumber(newLevel or 0) or 0,
+ amount,
+ tostring(oldFillType),
+ tostring(newFillType),
+ tostring(cutterInputFillType),
+ tostring(cutterOutputFillType),
+ fillTypeTitle,
+ tostring(activeFillType),
+ tostring(recorded),
+ reason
+ ))
+ else
+ local count = self.combineFillLevelDebugCallCount[vehicleId] or 0
+ if count <= self.combineFillLevelDebugMaxIdleLogsPerVehicle or count % 300 == 0 then
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA_IDLE vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%.2f new=%.2f delta=%.2f newFillType=%s activeFillType=%s(%s) call=%d",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(oldLevel or 0) or 0,
+ tonumber(newLevel or 0) or 0,
+ amount,
+ tostring(newFillType),
+ self:getFillTypeDebugTitle(activeFillType),
+ tostring(activeFillType),
+ count
+ ))
end
+ end
+end
- v = v + 1
+function YieldTracker:getFillTypeDebugTitle(fillTypeIndex)
+ if fillTypeIndex == nil then
+ return "nil"
end
- delete(xmlFile)
+ if g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil then
+ local desc = g_fillTypeManager.indexToFillType[fillTypeIndex]
+ if desc ~= nil then
+ return tostring(desc.title or desc.name or fillTypeIndex)
+ end
+ end
- self.yieldDataLoaded = true
- self:debugMessage("Yield data loaded from XML")
+ return tostring(fillTypeIndex)
end
--- Saves yield tracking data and vehicle baseline fill levels to an XML file in the current savegame directory.
--- Data saved includes harvested amounts grouped by year, field, and crop, as well as last known fill levels per vehicle.
-function YieldTracker:saveYieldData()
- if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
- self:debugMessage("Save skipped: no savegame directory")
+-- REDUCED PIPE TRACKING HOOK
+-- Tracks only the most likely pipe transfer path: Dischargeable.onUpdateTick.
+-- Debug output is restricted to harvest vehicles only.
+-- Pipe-only comparison formula:
+-- pipeHarvestAmount = targetDelta
+-- targetDelta = liters added to the overload/trailer target during this discharge tick
+-- Tank drawdown is NOT subtracted anymore. For the pipe-only approach, every liter that
+-- leaves the combine/harvester pipe is relevant.
+function YieldTracker:installPipeDischargeTrackingHook()
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() called")
+
+ if self.pipeDischargeTrackingHookInstalled then
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() skipped: already installed")
return
end
- local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
- local xmlFile = createXMLFile("YieldDataXML", filePath, "yieldData")
+ self.pipeDischargeCallCount = self.pipeDischargeCallCount or {}
+ self.pipeDischargeMaxIdleLogsPerVehicle = 20
- -- Save yield data per year/field/crop
- local i = 0
- for year, fields in pairs(self.yieldData) do
- local yearKey = string.format("yieldData.year(%d)", i)
- setXMLInt(xmlFile, yearKey .. "#value", year)
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_EXISTS Dischargeable=" .. tostring(Dischargeable ~= nil))
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_EXISTS Dischargeable.onUpdateTick=" .. tostring(Dischargeable ~= nil and type(Dischargeable.onUpdateTick) == "function"))
+
+ if Dischargeable == nil or type(Dischargeable.onUpdateTick) ~= "function" then
+ self:debugHarvestMessage("ERROR: Dischargeable.onUpdateTick not found; pipe tracking hook not installed")
+ return
+ end
+
+ local tracker = self
+
+ Dischargeable.onUpdateTick = Utils.overwrittenFunction(Dischargeable.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local args = {...}
+ local beforeCombineLevel, beforeCombineFillType, combineFillUnitIndex = tracker:getCombineInternalFillState(vehicle)
+ local beforeTargetObject, beforeTargetFillUnitIndex, beforeTargetLevel, beforeTargetFillType = tracker:getPipeTargetState(vehicle, args)
+
+ local result = superFunc(vehicle, dt, ...)
+
+ local afterCombineLevel, afterCombineFillType = tracker:getCombineInternalFillState(vehicle)
+ local afterTargetObject, afterTargetFillUnitIndex, afterTargetLevel, afterTargetFillType = tracker:getPipeTargetState(vehicle, args, beforeTargetObject, beforeTargetFillUnitIndex)
+
+ tracker:handlePipeDischargeTracking(
+ vehicle,
+ dt,
+ beforeTargetObject,
+ beforeTargetFillUnitIndex,
+ beforeTargetLevel,
+ beforeTargetFillType,
+ afterTargetObject,
+ afterTargetFillUnitIndex,
+ afterTargetLevel,
+ afterTargetFillType,
+ combineFillUnitIndex,
+ beforeCombineLevel,
+ beforeCombineFillType,
+ afterCombineLevel,
+ afterCombineFillType
+ )
+
+ return result
+ end)
+
+ self.pipeDischargeTrackingHookInstalled = true
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_INSTALLED Dischargeable.onUpdateTick")
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() finished")
+end
+
+function YieldTracker:getPipeTargetState(vehicle, args, knownTargetObject, knownTargetFillUnitIndex)
+ if knownTargetObject ~= nil and knownTargetFillUnitIndex ~= nil and knownTargetObject.getFillUnitFillLevel ~= nil then
+ local level = knownTargetObject:getFillUnitFillLevel(knownTargetFillUnitIndex)
+ local fillType = nil
+ if knownTargetObject.getFillUnitFillType ~= nil then
+ fillType = knownTargetObject:getFillUnitFillType(knownTargetFillUnitIndex)
+ end
+ return knownTargetObject, knownTargetFillUnitIndex, level, fillType
+ end
+
+ if vehicle == nil or vehicle.getDischargeTargetObject == nil then
+ return nil, nil, nil, nil
+ end
+
+ if args ~= nil then
+ for _, possibleDischargeNode in ipairs(args) do
+ local ok, targetObject, targetFillUnitIndex = pcall(function()
+ return vehicle:getDischargeTargetObject(possibleDischargeNode)
+ end)
+
+ if ok and targetObject ~= nil and targetFillUnitIndex ~= nil then
+ local level = nil
+ local fillType = nil
+ if targetObject.getFillUnitFillLevel ~= nil then
+ level = targetObject:getFillUnitFillLevel(targetFillUnitIndex)
+ end
+ if targetObject.getFillUnitFillType ~= nil then
+ fillType = targetObject:getFillUnitFillType(targetFillUnitIndex)
+ end
+ return targetObject, targetFillUnitIndex, level, fillType
+ end
+ end
+ end
+
+ local dischargeSpec = vehicle.spec_dischargeable or (vehicle.spec_combine ~= nil and vehicle.spec_combine.spec_dischargeable) or nil
+ if dischargeSpec ~= nil and dischargeSpec.dischargeNodes ~= nil then
+ for _, dischargeNode in pairs(dischargeSpec.dischargeNodes) do
+ local ok, targetObject, targetFillUnitIndex = pcall(function()
+ return vehicle:getDischargeTargetObject(dischargeNode)
+ end)
+
+ if ok and targetObject ~= nil and targetFillUnitIndex ~= nil then
+ local level = nil
+ local fillType = nil
+ if targetObject.getFillUnitFillLevel ~= nil then
+ level = targetObject:getFillUnitFillLevel(targetFillUnitIndex)
+ end
+ if targetObject.getFillUnitFillType ~= nil then
+ fillType = targetObject:getFillUnitFillType(targetFillUnitIndex)
+ end
+ return targetObject, targetFillUnitIndex, level, fillType
+ end
+ end
+ end
+
+ return nil, nil, nil, nil
+end
+
+
+function YieldTracker:isPipeDebugHarvestVehicle(vehicle)
+ if vehicle == nil then
+ return false
+ end
+
+ -- Normal combine harvesters and many specialized harvesters use spec_combine.
+ if vehicle.spec_combine ~= nil then
+ return true
+ end
+
+ -- Fallback for modded harvesters that expose a harvester specialization/name instead of spec_combine.
+ if vehicle.spec_harvester ~= nil or vehicle.spec_combineDrivable ~= nil then
+ return true
+ end
+
+ local typeName = nil
+ if vehicle.typeName ~= nil then
+ typeName = tostring(vehicle.typeName):lower()
+ elseif vehicle.configFileName ~= nil then
+ typeName = tostring(vehicle.configFileName):lower()
+ end
+
+ if typeName ~= nil then
+ if typeName:find("combine", 1, true) ~= nil or typeName:find("harvester", 1, true) ~= nil then
+ return true
+ end
+ end
+
+ return false
+end
+
+function YieldTracker:handlePipeDischargeTracking(vehicle, dt, beforeTargetObject, beforeTargetFillUnitIndex, beforeTargetLevel, beforeTargetFillType, afterTargetObject, afterTargetFillUnitIndex, afterTargetLevel, afterTargetFillType, combineFillUnitIndex, beforeCombineLevel, beforeCombineFillType, afterCombineLevel, afterCombineFillType)
+ if vehicle == nil then
+ return
+ end
+
+ if not self:isPipeDebugHarvestVehicle(vehicle) then
+ return
+ end
+
+ if not self.yieldDataLoaded then
+ return
+ end
+
+ local targetDelta = 0
+ if beforeTargetLevel ~= nil and afterTargetLevel ~= nil then
+ targetDelta = (tonumber(afterTargetLevel) or 0) - (tonumber(beforeTargetLevel) or 0)
+ end
+
+ if targetDelta <= 0.001 then
+ return
+ end
+
+ local fieldID = self:getCutterFieldID(vehicle)
+ if fieldID == nil then
+ return
+ end
+
+ local cutterInputFillType, cutterOutputFillType = self:getCutterStatus(vehicle)
+ local activeFillType = self:getActiveFillType(afterCombineFillType, afterTargetFillType, cutterOutputFillType)
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getActiveFillType(cutterOutputFillType, cutterInputFillType, beforeCombineFillType)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
+
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN or not self:isTrackedFillType(activeFillType) then
+ self:debugForageHarvesterPipeTransfer(vehicle, fieldID, afterTargetObject, afterTargetFillUnitIndex, targetDelta, afterTargetFillType, activeFillType, false)
+ return
+ end
+
+ local currentYear = g_currentMission.environment.currentYear
+ self:addPipeHarvestAmount(currentYear, fieldID, activeFillType, targetDelta)
+ self:debugForageHarvesterPipeTransfer(vehicle, fieldID, afterTargetObject, afterTargetFillUnitIndex, targetDelta, afterTargetFillType, activeFillType, true)
+
+ -- After a successful pipe transfer, the combine tank usually changed too.
+ -- Refresh the transient tank contribution so the GUI shows pipe + remaining tank.
+ self:updateCombineLiveTank(vehicle, fieldID, activeFillType)
+end
+
+
+
+-- PIPE-BASED FINAL HARVEST CALCULATION
+--=======================================================================================================================================================================
+function YieldTracker:makeYieldKey(year, fieldID, fillTypeIndex)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return nil
+ end
+ return tostring(year) .. ":" .. tostring(fieldID) .. ":" .. tostring(fillTypeIndex)
+end
+
+function YieldTracker:getFillTypeTitle(fillTypeIndex)
+ local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil and g_fillTypeManager.indexToFillType[fillTypeIndex] or nil
+ if fillTypeDesc ~= nil then
+ return fillTypeDesc.title or fillTypeDesc.name or tostring(fillTypeIndex)
+ end
+ return tostring(fillTypeIndex)
+end
+
+function YieldTracker:initializePipeBaseFromYieldData()
+ self.pipeBaseTotals = {}
+ self.liveTankContributions = {}
+ self.vehicleLiveTankKeys = {}
+
+ local liveByKey = {}
+ if self.savedLiveTankContributions ~= nil then
+ for _, entry in pairs(self.savedLiveTankContributions) do
+ local key = self:makeYieldKey(entry.year, entry.fieldID, entry.fillTypeIndex)
+ if key ~= nil then
+ liveByKey[key] = (liveByKey[key] or 0) + (tonumber(entry.amount) or 0)
+ end
+ end
+ end
+
+ for year, fields in pairs(self.yieldData or {}) do
+ self.pipeBaseTotals[year] = self.pipeBaseTotals[year] or {}
+ for fieldID, crops in pairs(fields) do
+ self.pipeBaseTotals[year][fieldID] = self.pipeBaseTotals[year][fieldID] or {}
+ for fillTypeIndex, cropData in pairs(crops) do
+ local amount = tonumber(cropData.amount or 0) or 0
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ local liveAmount = liveByKey[key] or 0
+ self.pipeBaseTotals[year][fieldID][fillTypeIndex] = math.max(0, amount - liveAmount)
+ end
+ end
+ end
+
+ self.savedLiveTankContributions = nil
+end
+
+function YieldTracker:getPipeBaseAmount(year, fieldID, fillTypeIndex)
+ if self.pipeBaseTotals == nil or self.pipeBaseTotals[year] == nil or self.pipeBaseTotals[year][fieldID] == nil then
+ return 0
+ end
+ return tonumber(self.pipeBaseTotals[year][fieldID][fillTypeIndex] or 0) or 0
+end
+
+function YieldTracker:getLiveTankAmountForKey(key)
+ local total = 0
+ for _, contribution in pairs(self.liveTankContributions or {}) do
+ if contribution.key == key then
+ total = total + (tonumber(contribution.amount) or 0)
+ end
+ end
+ return total
+end
+
+function YieldTracker:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return
+ end
+
+ self.yieldData[year] = self.yieldData[year] or {}
+ self.yieldData[year][fieldID] = self.yieldData[year][fieldID] or {}
+
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ local amount = self:getPipeBaseAmount(year, fieldID, fillTypeIndex) + self:getLiveTankAmountForKey(key)
+
+ self.yieldData[year][fieldID][fillTypeIndex] = self.yieldData[year][fieldID][fillTypeIndex] or {
+ amount = 0,
+ fillTypeTitle = self:getFillTypeTitle(fillTypeIndex)
+ }
+
+ self.yieldData[year][fieldID][fillTypeIndex].amount = amount
+ self.yieldData[year][fieldID][fillTypeIndex].fillTypeTitle = self:getFillTypeTitle(fillTypeIndex)
+end
+
+function YieldTracker:addPipeHarvestAmount(year, fieldID, fillTypeIndex, amount)
+ amount = tonumber(amount or 0) or 0
+ if year == nil or fieldID == nil or fillTypeIndex == nil or amount <= 0.001 then
+ return
+ end
+
+ self.pipeBaseTotals = self.pipeBaseTotals or {}
+ self.pipeBaseTotals[year] = self.pipeBaseTotals[year] or {}
+ self.pipeBaseTotals[year][fieldID] = self.pipeBaseTotals[year][fieldID] or {}
+ self.pipeBaseTotals[year][fieldID][fillTypeIndex] = (self.pipeBaseTotals[year][fieldID][fillTypeIndex] or 0) + amount
+
+ self:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+end
+
+function YieldTracker:updateCombineLiveTank(vehicle, fieldID, fillTypeIndex)
+ if vehicle == nil or not self.yieldDataLoaded then
+ return
+ end
+
+ local year = g_currentMission.environment.currentYear
+ local vehicleId = vehicle.id or tostring(vehicle)
+ local oldContribution = self.liveTankContributions ~= nil and self.liveTankContributions[vehicleId] or nil
+
+ if oldContribution ~= nil then
+ self.liveTankContributions[vehicleId] = nil
+ self:syncYieldDataEntry(oldContribution.year, oldContribution.fieldID, oldContribution.fillTypeIndex)
+ end
+
+ local tankLevel = 0
+ local tankFillType = fillTypeIndex
+ local fillLevel, currentFillType = self:getCombineInternalFillState(vehicle)
+ if fillLevel ~= nil then
+ tankLevel = tonumber(fillLevel or 0) or 0
+ end
+ if currentFillType ~= nil and currentFillType ~= FillType.UNKNOWN then
+ tankFillType = currentFillType
+ end
+
+ if tankLevel <= 0.001 then
+ return
+ end
+
+ if fieldID == nil or fillTypeIndex == nil or fillTypeIndex == FillType.UNKNOWN then
+ return
+ end
+
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ self.liveTankContributions = self.liveTankContributions or {}
+ self.liveTankContributions[vehicleId] = {
+ key = key,
+ year = year,
+ fieldID = fieldID,
+ fillTypeIndex = fillTypeIndex,
+ amount = tankLevel
+ }
+
+ self:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+end
+
+function YieldTracker:installCombineLiveTankTrackingHook()
+ if self.combineLiveTankTrackingHookInstalled then
+ return
+ end
+
+ if Combine == nil or type(Combine.onUpdateTick) ~= "function" then
+ return
+ end
+
+ local tracker = self
+ Combine.onUpdateTick = Utils.overwrittenFunction(Combine.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local result = superFunc(vehicle, dt, ...)
+
+ if tracker:isPipeDebugHarvestVehicle(vehicle) then
+ local fieldID = tracker:getCutterFieldID(vehicle)
+ local currentLevel, currentFillType = tracker:getCombineInternalFillState(vehicle)
+ local cutterInputFillType, cutterOutputFillType = tracker:getCutterStatus(vehicle)
+ local activeFillType = tracker:getActiveFillType(currentFillType, cutterOutputFillType, cutterInputFillType)
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = tracker:getFieldFillTypeFallback(vehicle)
+ end
+
+ if fieldID ~= nil and activeFillType ~= nil and activeFillType ~= FillType.UNKNOWN and tracker:isTrackedFillType(activeFillType) then
+ tracker:updateCombineLiveTank(vehicle, fieldID, activeFillType)
+ elseif currentLevel ~= nil and currentLevel <= 0.001 then
+ tracker:updateCombineLiveTank(vehicle, nil, nil)
+ end
+ end
+
+ return result
+ end)
+
+ self.combineLiveTankTrackingHookInstalled = true
+end
+
+-- PIPE-ONLY DEBUG CALCULATION
+--=======================================================================================================================================================================
+function YieldTracker:addPipeOnlyDebugAmount(year, fieldID, fillTypeIndex, amount)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return
+ end
+
+ amount = tonumber(amount or 0) or 0
+ if amount <= 0.001 then
+ return
+ end
+
+ self.pipeOnlyDebugTotals = self.pipeOnlyDebugTotals or {}
+ self.pipeOnlyDebugTotals[year] = self.pipeOnlyDebugTotals[year] or {}
+ self.pipeOnlyDebugTotals[year][fieldID] = self.pipeOnlyDebugTotals[year][fieldID] or {}
+ self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] = (self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] or 0) + amount
+end
+
+function YieldTracker:getPipeOnlyDebugAmount(year, fieldID, fillTypeIndex)
+ if self.pipeOnlyDebugTotals == nil then
+ return 0
+ end
+
+ if self.pipeOnlyDebugTotals[year] == nil or self.pipeOnlyDebugTotals[year][fieldID] == nil then
+ return 0
+ end
+
+ return self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] or 0
+end
+
+function YieldTracker:getCurrentCombineTankForDebug(vehicle, expectedFillType)
+ local level, fillType, fillUnitIndex = self:getCombineInternalFillState(vehicle)
+
+ if expectedFillType ~= nil and fillType ~= nil and fillType ~= expectedFillType then
+ return 0, fillType, fillUnitIndex
+ end
+
+ return tonumber(level or 0) or 0, fillType, fillUnitIndex
+end
+
+function YieldTracker:formatDebugNumber(value)
+ if value == nil then
+ return "nil"
+ end
+ local numberValue = tonumber(value)
+ if numberValue == nil then
+ return tostring(value)
+ end
+ return string.format("%.2f", numberValue)
+end
+
+
+-- LOOSE MATERIAL PRODUCTIVE TRACKING + FORAGE HARVESTER PIPE DEBUG
+--=======================================================================================================================================================================
+-- Productive tracking for loose material pickup:
+-- - ForageWagon.onUpdateTick
+-- - Baler.onUpdateTick
+--
+-- These write directly into yieldData via addPipeHarvestAmount(), so the GUI sees the values.
+-- Debug remains limited to forage harvester pipe transfers and is printed only when a positive target delta exists.
+
+function YieldTracker:isLooseMaterialProductiveVehicle(vehicle)
+ if vehicle == nil or type(vehicle) ~= "table" then
+ return false
+ end
+
+ if vehicle.spec_baler ~= nil then return true end
+ if vehicle.spec_forageWagon ~= nil then return true end
+
+ local typeName = nil
+ if vehicle.typeName ~= nil then
+ typeName = tostring(vehicle.typeName):lower()
+ elseif vehicle.configFileName ~= nil then
+ typeName = tostring(vehicle.configFileName):lower()
+ end
+
+ if typeName ~= nil then
+ if typeName:find("baler", 1, true) ~= nil then return true end
+ if typeName:find("foragewagon", 1, true) ~= nil then return true end
+ if typeName:find("forage_wagon", 1, true) ~= nil then return true end
+ if typeName:find("mowerforagewagon", 1, true) ~= nil then return true end
+ end
+
+ return false
+end
+
+function YieldTracker:isForageHarvesterDebugVehicle(vehicle)
+ if vehicle == nil or type(vehicle) ~= "table" then
+ return false
+ end
+
+ if vehicle.spec_combine ~= nil then
+ local typeName = tostring(vehicle.typeName or vehicle.configFileName or ""):lower()
+ if typeName:find("forage", 1, true) ~= nil or typeName:find("chopper", 1, true) ~= nil then
+ return true
+ end
+ end
+
+ if vehicle.spec_harvester ~= nil then
+ local typeName = tostring(vehicle.typeName or vehicle.configFileName or ""):lower()
+ if typeName:find("forage", 1, true) ~= nil or typeName:find("chopper", 1, true) ~= nil or typeName:find("bigx", 1, true) ~= nil then
+ return true
+ end
+ end
+
+ local typeName = nil
+ if vehicle.typeName ~= nil then
+ typeName = tostring(vehicle.typeName):lower()
+ elseif vehicle.configFileName ~= nil then
+ typeName = tostring(vehicle.configFileName):lower()
+ end
+
+ if typeName ~= nil then
+ if typeName:find("forageharvester", 1, true) ~= nil then return true end
+ if typeName:find("forage_harvester", 1, true) ~= nil then return true end
+ if typeName:find("woodharvester", 1, true) == nil and typeName:find("harvester", 1, true) ~= nil and typeName:find("forage", 1, true) ~= nil then return true end
+ if typeName:find("bigx", 1, true) ~= nil then return true end
+ if typeName:find("jaguar", 1, true) ~= nil then return true end
+ if typeName:find("fr780", 1, true) ~= nil then return true end
+ if typeName:find("fh", 1, true) ~= nil and typeName:find("combine", 1, true) == nil then return true end
+ end
+
+ return false
+end
+
+function YieldTracker:getLoosePickupFieldID(vehicle)
+ if vehicle == nil or type(vehicle) ~= "table" then
+ return nil
+ end
+
+ if vehicle.rootNode ~= nil and g_farmlandManager ~= nil and g_farmlandManager.getFarmlandIdAtWorldPosition ~= nil then
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ return g_farmlandManager:getFarmlandIdAtWorldPosition(x, z)
+ end
+
+ return nil
+end
+
+function YieldTracker:getLoosePickupSnapshot(vehicle)
+ local snapshot = {
+ totalFillLevel = 0,
+ fillUnitCount = 0,
+ fillUnitLevels = {},
+ fillUnitFillTypes = {},
+ changedFillUnitIndex = nil,
+ changedFillTypeIndex = nil,
+ changedFillLevel = nil
+ }
+
+ if vehicle == nil or type(vehicle) ~= "table" then
+ return snapshot
+ end
+
+ local count = 0
+ if vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits ~= nil then
+ count = #vehicle.spec_fillUnit.fillUnits
+ elseif vehicle.getFillUnits ~= nil then
+ local ok, fillUnits = pcall(function() return vehicle:getFillUnits() end)
+ if ok and type(fillUnits) == "table" then
+ count = #fillUnits
+ end
+ end
+
+ snapshot.fillUnitCount = count
+
+ if vehicle.getFillUnitFillLevel ~= nil then
+ for i = 1, count do
+ local level = vehicle:getFillUnitFillLevel(i) or 0
+ local fillTypeIndex = nil
+
+ if vehicle.getFillUnitFillType ~= nil then
+ fillTypeIndex = vehicle:getFillUnitFillType(i)
+ elseif vehicle.spec_fillUnit ~= nil and vehicle.spec_fillUnit.fillUnits ~= nil and vehicle.spec_fillUnit.fillUnits[i] ~= nil then
+ fillTypeIndex = vehicle.spec_fillUnit.fillUnits[i].fillType
+ end
+
+ snapshot.fillUnitLevels[i] = level
+ snapshot.fillUnitFillTypes[i] = fillTypeIndex
+ snapshot.totalFillLevel = snapshot.totalFillLevel + level
+
+ if snapshot.changedFillUnitIndex == nil or level > (snapshot.changedFillLevel or -1) then
+ snapshot.changedFillUnitIndex = i
+ snapshot.changedFillTypeIndex = fillTypeIndex
+ snapshot.changedFillLevel = level
+ end
+ end
+ end
+
+ return snapshot
+end
+
+function YieldTracker:getLoosePickupPositiveDelta(before, after)
+ if before == nil or after == nil then
+ return 0
+ end
+
+ local delta = (after.totalFillLevel or 0) - (before.totalFillLevel or 0)
+ if delta > 0.01 then
+ return delta
+ end
+
+ return 0
+end
+
+
+function YieldTracker:getFillTypeIndexByNameSafe(fillTypeName)
+ if fillTypeName == nil or g_fillTypeManager == nil then
+ return nil
+ end
+
+ if g_fillTypeManager.getFillTypeIndexByName ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeIndexByName(fillTypeName) end)
+ if ok and result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.nameToIndex ~= nil then
+ local result = g_fillTypeManager.nameToIndex[fillTypeName]
+ if result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.nameToFillType ~= nil and g_fillTypeManager.nameToFillType[fillTypeName] ~= nil then
+ local desc = g_fillTypeManager.nameToFillType[fillTypeName]
+ local result = desc.index or desc.fillTypeIndex
+ if result ~= nil and result ~= FillType.UNKNOWN then
+ return result
+ end
+ end
+
+ if g_fillTypeManager.indexToFillType ~= nil then
+ for index, desc in pairs(g_fillTypeManager.indexToFillType) do
+ if desc ~= nil and desc.name == fillTypeName then
+ return index
+ end
+ end
+ end
+
+ return nil
+end
+
+function YieldTracker:getNormalizedLoosePickupFillType(fillTypeIndex)
+ if fillTypeIndex == nil or fillTypeIndex == FillType.UNKNOWN then
+ return fillTypeIndex
+ end
+
+ local fillTypeName = nil
+ if g_fillTypeManager ~= nil then
+ if g_fillTypeManager.getFillTypeNameByIndex ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeNameByIndex(fillTypeIndex) end)
+ if ok and result ~= nil then
+ fillTypeName = tostring(result)
+ end
+ end
+
+ if fillTypeName == nil and g_fillTypeManager.indexToFillType ~= nil and g_fillTypeManager.indexToFillType[fillTypeIndex] ~= nil then
+ fillTypeName = g_fillTypeManager.indexToFillType[fillTypeIndex].name
+ end
+ end
+
+ if fillTypeName == nil then
+ return fillTypeIndex
+ end
+
+ local normalizedName = fillTypeName
+
+ if fillTypeName == "GRASS_WINDROW" then
+ normalizedName = "GRASS"
+ elseif fillTypeName == "DRYGRASS_WINDROW" then
+ -- Keep hay as DRYGRASS_WINDROW. This is the fill type that is actually collected
+ -- and it can be displayed as localized "Heu" in the GUI.
+ normalizedName = "DRYGRASS_WINDROW"
+ elseif fillTypeName == "STRAW" or fillTypeName == "STRAW_WINDROW" then
+ normalizedName = "STRAW"
+ elseif fillTypeName == "CHAFF_WINDROW" then
+ normalizedName = "CHAFF"
+ elseif string.sub(fillTypeName, -8) == "_WINDROW" then
+ -- Do not blindly strip every windrow suffix: some windrow fill types, e.g. hay,
+ -- are the selectable/storable fill type and should remain unchanged.
+ normalizedName = fillTypeName
+ end
+
+ if normalizedName ~= fillTypeName then
+ local normalizedIndex = self:getFillTypeIndexByNameSafe(normalizedName)
+ if normalizedIndex ~= nil and normalizedIndex ~= FillType.UNKNOWN then
+ return normalizedIndex
+ end
+ end
+
+ return fillTypeIndex
+end
+
+function YieldTracker:trackLooseMaterialPickupUpdate(hookName, vehicle, dt)
+ if not self.yieldDataLoaded then
+ return
+ end
+
+ if not self:isLooseMaterialProductiveVehicle(vehicle) then
+ return
+ end
+
+ self.loosePickupBaselines = self.loosePickupBaselines or {}
+
+ local vehicleId = vehicle.id or vehicle.rootNode or tostring(vehicle)
+ local snapshot = self:getLoosePickupSnapshot(vehicle)
+ local previous = self.loosePickupBaselines[vehicleId]
+
+ self.loosePickupBaselines[vehicleId] = snapshot
+
+ if previous == nil then
+ return
+ end
+
+ local amount = self:getLoosePickupPositiveDelta(previous, snapshot)
+ if amount <= 0.01 then
+ return
+ end
+
+ local fieldID = self:getLoosePickupFieldID(vehicle)
+ if fieldID == nil then
+ return
+ end
+
+ local fillTypeIndex = snapshot.changedFillTypeIndex
+ if (fillTypeIndex == nil or fillTypeIndex == FillType.UNKNOWN) and snapshot.changedFillUnitIndex ~= nil and vehicle.getFillUnitFillType ~= nil then
+ fillTypeIndex = vehicle:getFillUnitFillType(snapshot.changedFillUnitIndex)
+ end
+
+ if fillTypeIndex == nil or fillTypeIndex == FillType.UNKNOWN then
+ return
+ end
+
+ fillTypeIndex = self:getNormalizedLoosePickupFillType(fillTypeIndex)
+
+ local year = g_currentMission.environment.currentYear
+ self:addPipeHarvestAmount(year, fieldID, fillTypeIndex, amount)
+end
+
+function YieldTracker:installLooseMaterialPickupTrackingHook()
+ if self.looseMaterialPickupTrackingHookInstalled then
+ return
+ end
+
+ local tracker = self
+ local installedBaler = false
+ local installedForageWagon = false
+
+ if Baler ~= nil and type(Baler.onUpdateTick) == "function" and not self.loosePickupBalerHookInstalled then
+ Baler.onUpdateTick = Utils.appendedFunction(Baler.onUpdateTick, function(vehicle, dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected, ...)
+ tracker:trackLooseMaterialPickupUpdate("Baler.onUpdateTick", vehicle, dt)
+ end)
+ self.loosePickupBalerHookInstalled = true
+ installedBaler = true
+ end
+
+ if ForageWagon ~= nil and type(ForageWagon.onUpdateTick) == "function" and not self.loosePickupForageWagonHookInstalled then
+ ForageWagon.onUpdateTick = Utils.appendedFunction(ForageWagon.onUpdateTick, function(vehicle, dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected, ...)
+ tracker:trackLooseMaterialPickupUpdate("ForageWagon.onUpdateTick", vehicle, dt)
+ end)
+ self.loosePickupForageWagonHookInstalled = true
+ installedForageWagon = true
+ end
+
+ self.looseMaterialPickupTrackingHookInstalled = true
+
+ if self.debugHarvest then
+ print(string.format("%sLOOSE_PICKUP_TRACKING_INSTALLED baler=%s forageWagon=%s", PRINT_PREFIX, tostring(installedBaler), tostring(installedForageWagon)))
+ end
+end
+
+function YieldTracker:getFillTypeDebugName(fillTypeIndex)
+ if fillTypeIndex == nil then
+ return "nil"
+ end
+
+ if g_fillTypeManager ~= nil then
+ if g_fillTypeManager.getFillTypeNameByIndex ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeNameByIndex(fillTypeIndex) end)
+ if ok and result ~= nil then return tostring(result) end
+ end
+ if g_fillTypeManager.getFillTypeTitleByIndex ~= nil then
+ local ok, result = pcall(function() return g_fillTypeManager:getFillTypeTitleByIndex(fillTypeIndex) end)
+ if ok and result ~= nil then return tostring(result) end
+ end
+ if g_fillTypeManager.indexToFillType ~= nil and g_fillTypeManager.indexToFillType[fillTypeIndex] ~= nil then
+ local desc = g_fillTypeManager.indexToFillType[fillTypeIndex]
+ return tostring(desc.name or desc.title or fillTypeIndex)
+ end
+ end
+
+ return tostring(fillTypeIndex)
+end
+
+function YieldTracker:debugForageHarvesterPipeTransfer(vehicle, fieldID, targetObject, targetFillUnitIndex, targetDelta, targetFillType, activeFillType, recorded)
+ if not self:isForageHarvesterDebugVehicle(vehicle) then
+ return
+ end
+
+ local targetName = "nil"
+ if targetObject ~= nil then
+ targetName = self:getVehicleDebugName(targetObject)
+ end
+
+ print(string.format(
+ "%sFORAGE_HARVESTER_PIPE_DEBUG vehicle='%s' vehicleId=%s typeName=%s field=%s target='%s' targetFillUnit=%s targetDelta=%.2f targetFillType=%s(%s) activeFillType=%s(%s) recordedToGui=%s",
+ PRINT_PREFIX,
+ self:getVehicleDebugName(vehicle),
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(vehicle ~= nil and vehicle.typeName or nil),
+ tostring(fieldID),
+ tostring(targetName),
+ tostring(targetFillUnitIndex),
+ tonumber(targetDelta) or 0,
+ self:getFillTypeDebugName(targetFillType),
+ tostring(targetFillType),
+ self:getFillTypeDebugName(activeFillType),
+ tostring(activeFillType),
+ tostring(recorded)
+ ))
+end
+
+
+-- SAVE/LOAD METHODS
+--=======================================================================================================================================================================
+
+-- Call required dependencies upon loadmap from main.lua
+function YieldTracker:setup()
+ self:loadYieldData()
+ self:initializePipeBaseFromYieldData()
+ self:installCombineLiveTankTrackingHook()
+ self:installPipeDischargeTrackingHook()
+ self:installLooseMaterialPickupTrackingHook()
+ self:hookInGameSaveEvent()
+end
+
+
+-- Hooks into the save event to trigger yield data saving on every game save.
+function YieldTracker:hookInGameSaveEvent()
+ FSCareerMissionInfo.saveToXMLFile = Utils.appendedFunction(
+ FSCareerMissionInfo.saveToXMLFile,
+ function() YieldTracker:saveYieldData() end)
+end
+
+
+-- Loads yield tracking data and vehicle baseline fill levels from an XML file in the current savegame directory.
+-- Rebuilds the yieldData structure by year, field, and crop, and restores previous fill levels per vehicle.
+-- If no file exists or loading fails, initializes with empty data and marks data as loaded.
+function YieldTracker:loadYieldData()
+ if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
+ self:debugMessage("Load skipped: no savegame directory")
+ self.yieldDataLoaded = true
+ return
+ end
+
+ local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
+ if not fileExists(filePath) then
+ self:debugMessage("No existing yield data XML found")
+ self.yieldDataLoaded = true
+ return
+ end
+
+ local xmlFile = loadXMLFile("YieldDataXML", filePath)
+ if not xmlFile then
+ self:debugMessage("Failed to load yield data XML")
+ return
+ end
+
+ self.yieldData = {}
+
+ local i = 0
+ while true do
+ local yearKey = string.format("yieldData.year(%d)", i)
+ if not hasXMLProperty(xmlFile, yearKey) then break end
+
+ local year = getXMLInt(xmlFile, yearKey .. "#value")
+ self.yieldData[year] = {}
+
+ local j = 0
+ while true do
+ local fieldKey = string.format("%s.field(%d)", yearKey, j)
+ if not hasXMLProperty(xmlFile, fieldKey) then break end
+
+ local fieldId = getXMLInt(xmlFile, fieldKey .. "#id")
+ self.yieldData[year][fieldId] = {}
+
+ local k = 0
+ while true do
+ local cropKey = string.format("%s.crop(%d)", fieldKey, k)
+ if not hasXMLProperty(xmlFile, cropKey) then break end
+
+ local cropName = getXMLString(xmlFile, cropKey .. "#name")
+ local amount = getXMLFloat(xmlFile, cropKey .. "#amount")
+ local fillTypeIndex = getXMLInt(xmlFile, cropKey .. "#fillTypeIndex")
+
+ self.yieldData[year][fieldId][fillTypeIndex] = {
+ amount = amount,
+ fillTypeTitle = cropName
+ }
+
+ k = k + 1
+ end
+
+ j = j + 1
+ end
+
+ i = i + 1
+ end
+
+ -- Load prevFillLevels baseline per vehicle
+ self.prevFillLevels = {}
+ local v = 0
+ while true do
+ local vehicleKey = string.format("yieldData.prevFillLevels.vehicle(%d)", v)
+ if not hasXMLProperty(xmlFile, vehicleKey) then break end
+
+ local vehicleId = getXMLInt(xmlFile, vehicleKey .. "#id")
+ local level = getXMLFloat(xmlFile, vehicleKey .. "#level")
+
+ if vehicleId and level then
+ self.prevFillLevels[vehicleId] = level
+ end
+
+ v = v + 1
+ end
+
+
+ -- Load transient live tank contributions that were included in the saved display amount.
+ -- They are subtracted from pipeBaseTotals after loading to avoid double counting when the same tank is unloaded after reload.
+ self.savedLiveTankContributions = {}
+ local lt = 0
+ while true do
+ local liveKey = string.format("yieldData.liveTankContributions.vehicle(%d)", lt)
+ if not hasXMLProperty(xmlFile, liveKey) then break end
+
+ table.insert(self.savedLiveTankContributions, {
+ vehicleId = getXMLString(xmlFile, liveKey .. "#id"),
+ year = getXMLInt(xmlFile, liveKey .. "#year"),
+ fieldID = getXMLInt(xmlFile, liveKey .. "#fieldId"),
+ fillTypeIndex = getXMLInt(xmlFile, liveKey .. "#fillTypeIndex"),
+ amount = getXMLFloat(xmlFile, liveKey .. "#amount") or 0
+ })
+
+ lt = lt + 1
+ end
+
+ delete(xmlFile)
+
+ self.yieldDataLoaded = true
+ self:debugMessage("Yield data loaded from XML")
+end
+
+
+
+-- Saves yield tracking data and vehicle baseline fill levels to an XML file in the current savegame directory.
+-- Data saved includes harvested amounts grouped by year, field, and crop, as well as last known fill levels per vehicle.
+function YieldTracker:saveYieldData()
+ if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
+ self:debugMessage("Save skipped: no savegame directory")
+ return
+ end
+
+ local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
+ local xmlFile = createXMLFile("YieldDataXML", filePath, "yieldData")
+
+ -- Save yield data per year/field/crop
+ local i = 0
+ for year, fields in pairs(self.yieldData) do
+ local yearKey = string.format("yieldData.year(%d)", i)
+ setXMLInt(xmlFile, yearKey .. "#value", year)
local j = 0
for fieldId, crops in pairs(fields) do
@@ -448,10 +1764,26 @@ function YieldTracker:saveYieldData()
v = v + 1
end
+
+
+ -- Save transient live tank contributions separately so they can be subtracted from pipe base totals on reload.
+ local lt = 0
+ for vehicleId, contribution in pairs(self.liveTankContributions or {}) do
+ if contribution.amount ~= nil and contribution.amount > 0.001 then
+ local liveKey = string.format("yieldData.liveTankContributions.vehicle(%d)", lt)
+ setXMLString(xmlFile, liveKey .. "#id", tostring(vehicleId))
+ setXMLInt(xmlFile, liveKey .. "#year", contribution.year)
+ setXMLInt(xmlFile, liveKey .. "#fieldId", contribution.fieldID)
+ setXMLInt(xmlFile, liveKey .. "#fillTypeIndex", contribution.fillTypeIndex)
+ setXMLFloat(xmlFile, liveKey .. "#amount", contribution.amount)
+ lt = lt + 1
+ end
+ end
+
saveXMLFile(xmlFile)
delete(xmlFile)
self:debugMessage("Yield data saved to XML")
end
---=======================================================================================================================================================================
\ No newline at end of file
+--=======================================================================================================================================================================
diff --git a/FS25_YieldTracker/scripts/yieldTracker_backup.lua b/FS25_YieldTracker/scripts/yieldTracker_backup.lua
new file mode 100644
index 0000000..39323e1
--- /dev/null
+++ b/FS25_YieldTracker/scripts/yieldTracker_backup.lua
@@ -0,0 +1,1414 @@
+-- Author: BitBarn Mods
+-- Date: 06-11-2025
+-- Version: 1.0.0.0
+-- yieldTracker.lua
+
+
+PRINT_PREFIX = "[ YieldTracker ] - "
+
+YieldTracker = {
+ modDir = g_currentModDirectory,
+ yieldData = {},
+ saveFileName = "yieldTrackerData.xml",
+}
+YieldTracker.debug = false
+YieldTracker.debugHarvest = false
+
+
+--- Active crop fill types are resolved dynamically from g_fruitTypeManager.
+
+
+-- Called every frame to update yield tracking for all combines in the current mission.
+-- Iterates over all vehicles, checks for combines, determines active fill types, and tracks their fill units.
+function YieldTracker:updateByFrame()
+ -- Harvest amounts are recorded by Combine.onUpdateTick tank deltas and Dischargeable.onUpdateTick pipe deltas.
+ -- The old per-frame scan is intentionally disabled.
+end
+
+
+-- Called every frame to track fill unit changes for a given combine vehicle.
+function YieldTracker:trackFillUnit(vehicle, activeFillType, activeFillLevel, isbaler)
+ if not self.yieldDataLoaded then return end
+ if not vehicle or not activeFillType then return end
+
+ local currentYear = g_currentMission.environment.currentYear
+ local vehicleId = vehicle.id
+ local fieldID
+
+ if not isbaler then
+ fieldID = self:getCutterFieldID(vehicle)
+ elseif isbaler then
+ local baler_pos_x, baler_pos_y, baler_pos_z = getWorldTranslation(vehicle.rootNode)
+ fieldID = g_farmlandManager:getFarmlandIdAtWorldPosition(baler_pos_x, baler_pos_z)
+ end
+
+ if not fieldID then return end
+
+ local fillTypeDesc = g_fillTypeManager.indexToFillType[activeFillType]
+ if not fillTypeDesc then return end
+
+ local cropName = fillTypeDesc.title
+
+ self.prevFillLevels = self.prevFillLevels or {}
+ self.initializedVehicles = self.initializedVehicles or {}
+
+ if activeFillLevel == nil then
+ self:debugMessage(string.format("Vehicle %d has no valid fill level this frame; skipping", vehicle.id))
+ return
+ end
+
+ -- If fill unit is empty or close to empty, reset baseline
+ if activeFillLevel <= 0.1 and (self.prevFillLevels[vehicleId] or 0) > 0 then
+ self.prevFillLevels[vehicleId] = 0
+ self:debugMessage(string.format("Vehicle %d fill level is 0L; resetting baseline", vehicleId))
+ return
+ end
+
+ -- First-time detection for this vehicle: skip initial fill, no yield recorded
+ if self.initializedVehicles[vehicleId] == nil then
+ self.initializedVehicles[vehicleId] = true
+ self.prevFillLevels[vehicleId] = activeFillLevel
+ self:debugMessage(string.format("Vehicle %d seen for first time this session with %.2fL", vehicleId, activeFillLevel))
+ end
+
+ local prevLevel = self.prevFillLevels[vehicleId] or 0
+ local delta = activeFillLevel - prevLevel
+
+ if delta > 0 then
+ if self:isTrackedFillType(activeFillType) then
+ -- Harvest increase detected for a tracked crop
+ self:recordHarvest(currentYear, fieldID, cropName, delta, activeFillType)
+ self:debugMessage(string.format("Harvest detected: +%.2fL of '%s' for vehicle %d on field %d.",
+ delta, cropName, vehicleId, fieldID))
+ else
+ self:debugMessage(string.format("Skipping untracked crop type '%s' (fillTypeIndex: %d)", cropName, activeFillType))
+ end
+ end
+
+ -- Always update baseline
+ self.prevFillLevels[vehicleId] = activeFillLevel
+end
+
+
+
+
+
+-- EVENT-BASED HARVEST TRACKING
+--=======================================================================================================================================================================
+
+function YieldTracker:installFillUnitHook()
+ self:debugHarvestMessage("installFillUnitHook() called")
+
+ if self.fillUnitHookInstalled then
+ self:debugHarvestMessage("installFillUnitHook() skipped: already installed")
+ return
+ end
+
+ self:debugHarvestMessage("Vehicle exists=" .. tostring(Vehicle ~= nil))
+ self:debugHarvestMessage("Vehicle.addFillUnitFillLevel exists=" .. tostring(Vehicle ~= nil and Vehicle.addFillUnitFillLevel ~= nil))
+ self:debugHarvestMessage("FillUnit exists=" .. tostring(FillUnit ~= nil))
+ self:debugHarvestMessage("FillUnit.addFillUnitFillLevel exists=" .. tostring(FillUnit ~= nil and FillUnit.addFillUnitFillLevel ~= nil))
+
+ local tracker = self
+ local installedAnyHook = false
+
+ if Vehicle ~= nil and Vehicle.addFillUnitFillLevel ~= nil then
+ Vehicle.addFillUnitFillLevel = Utils.overwrittenFunction(Vehicle.addFillUnitFillLevel, function(vehicle, superFunc, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_CALL Vehicle.addFillUnitFillLevel vehicle='%s' vehicleId=%s farmId=%s fillUnitIndex=%s delta=%s fillTypeIndex=%s",
+ tracker:getVehicleDebugName(vehicle),
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(farmId),
+ tostring(fillUnitIndex),
+ tostring(fillLevelDelta),
+ tostring(fillTypeIndex)
+ ))
+
+ local oldFillLevel = nil
+ if vehicle ~= nil and fillUnitIndex ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ oldFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or 0
+ end
+
+ local result = superFunc(vehicle, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+
+ local appliedAmount = fillLevelDelta
+ if type(result) == "number" then
+ appliedAmount = result
+ elseif oldFillLevel ~= nil and vehicle ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ local newFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or oldFillLevel
+ local measuredDelta = newFillLevel - oldFillLevel
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_AFTER Vehicle.addFillUnitFillLevel old=%s new=%s measuredDelta=%s result=%s",
+ tostring(oldFillLevel), tostring(newFillLevel), tostring(measuredDelta), tostring(result)
+ ))
+ if measuredDelta > 0 then
+ appliedAmount = measuredDelta
+ end
+ end
+
+ tracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, appliedAmount, fillTypeIndex)
+ return result
+ end)
+
+ installedAnyHook = true
+ self:debugHarvestMessage("Installed hook: Vehicle.addFillUnitFillLevel")
+ end
+
+ -- Additional trace hook in case FS25 calls the specialization table directly instead of Vehicle.*
+ if FillUnit ~= nil and FillUnit.addFillUnitFillLevel ~= nil and (Vehicle == nil or FillUnit.addFillUnitFillLevel ~= Vehicle.addFillUnitFillLevel) then
+ FillUnit.addFillUnitFillLevel = Utils.overwrittenFunction(FillUnit.addFillUnitFillLevel, function(vehicle, superFunc, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_CALL FillUnit.addFillUnitFillLevel vehicle='%s' vehicleId=%s farmId=%s fillUnitIndex=%s delta=%s fillTypeIndex=%s",
+ tracker:getVehicleDebugName(vehicle),
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(farmId),
+ tostring(fillUnitIndex),
+ tostring(fillLevelDelta),
+ tostring(fillTypeIndex)
+ ))
+
+ local oldFillLevel = nil
+ if vehicle ~= nil and fillUnitIndex ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ oldFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or 0
+ end
+
+ local result = superFunc(vehicle, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, ...)
+
+ local appliedAmount = fillLevelDelta
+ if type(result) == "number" then
+ appliedAmount = result
+ elseif oldFillLevel ~= nil and vehicle ~= nil and vehicle.getFillUnitFillLevel ~= nil then
+ local newFillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or oldFillLevel
+ local measuredDelta = newFillLevel - oldFillLevel
+ tracker:debugHarvestMessage(string.format(
+ "HOOK_AFTER FillUnit.addFillUnitFillLevel old=%s new=%s measuredDelta=%s result=%s",
+ tostring(oldFillLevel), tostring(newFillLevel), tostring(measuredDelta), tostring(result)
+ ))
+ if measuredDelta > 0 then
+ appliedAmount = measuredDelta
+ end
+ end
+
+ tracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, appliedAmount, fillTypeIndex)
+ return result
+ end)
+
+ installedAnyHook = true
+ self:debugHarvestMessage("Installed hook: FillUnit.addFillUnitFillLevel")
+ end
+
+ if not installedAnyHook then
+ self:debugHarvestMessage("ERROR: no fill unit hook installed; no supported addFillUnitFillLevel function found")
+ self:debugMessage("Could not install fill unit hook: no addFillUnitFillLevel function found")
+ return
+ end
+
+ self.fillUnitHookInstalled = true
+ self:debugHarvestMessage("installFillUnitHook() finished")
+ self:debugMessage("Installed fill unit harvest hook")
+end
+
+function YieldTracker:getVehicleDebugName(vehicle)
+ if vehicle == nil then
+ return "nil"
+ end
+
+ if vehicle.getName ~= nil then
+ local ok, name = pcall(function() return vehicle:getName() end)
+ if ok and name ~= nil and name ~= "" then
+ return tostring(name)
+ end
+ end
+
+ if vehicle.configFileName ~= nil then
+ return tostring(vehicle.configFileName)
+ end
+
+ if vehicle.typeName ~= nil then
+ return tostring(vehicle.typeName)
+ end
+
+ return tostring(vehicle)
+end
+
+function YieldTracker:debugHarvestMessage(message)
+ if self.debugHarvest then
+ print(PRINT_PREFIX .. "HARVEST_DEBUG - " .. tostring(message))
+ end
+end
+
+function YieldTracker:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, amount, reason)
+ if not self.debugHarvest then
+ return
+ end
+
+ local vehicleName = self:getVehicleDebugName(vehicle)
+ local fillTypeName = "nil"
+ if fillTypeIndex ~= nil and g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil then
+ local desc = g_fillTypeManager.indexToFillType[fillTypeIndex]
+ if desc ~= nil then
+ fillTypeName = tostring(desc.title or desc.name or fillTypeIndex)
+ else
+ fillTypeName = tostring(fillTypeIndex)
+ end
+ end
+
+ self:debugHarvestMessage(string.format(
+ "vehicle='%s' vehicleId=%s combine=%s baler=%s field=%s fillType=%s(%s) amount=%.2f reason=%s",
+ vehicleName,
+ tostring(vehicle ~= nil and vehicle.id or nil),
+ tostring(vehicle ~= nil and vehicle.spec_combine ~= nil),
+ tostring(vehicle ~= nil and vehicle.spec_baler ~= nil),
+ tostring(fieldID),
+ fillTypeName,
+ tostring(fillTypeIndex),
+ tonumber(amount or 0) or 0,
+ tostring(reason)
+ ))
+end
+
+function YieldTracker:handleHarvestFillUnitAdded(vehicle, fillUnitIndex, fillLevelDelta, fillTypeIndex)
+ local rawAmount = tonumber(fillLevelDelta or 0) or 0
+
+ if not self.yieldDataLoaded then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_yieldData_not_loaded")
+ return
+ end
+
+ if vehicle == nil or fillUnitIndex == nil or fillLevelDelta == nil or rawAmount <= 0 then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_invalid_or_non_positive_delta")
+ return
+ end
+
+ local isCombine = vehicle.spec_combine ~= nil
+ local isBaler = vehicle.spec_baler ~= nil
+
+ -- Count only where crop is generated. Ignore trailer/silo/transfer additions.
+ if not isCombine and not isBaler then
+ self:debugHarvestDecision(vehicle, nil, fillTypeIndex, rawAmount, "SKIP_not_combine_or_baler")
+ return
+ end
+
+ local fieldID = self:getHarvestFieldID(vehicle, isBaler)
+
+ if isCombine and not self:getIsCombineHarvesting(vehicle) then
+ self:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, rawAmount, "SKIP_combine_not_harvesting")
+ return
+ end
+
+ if isBaler and not self:getIsBalerHarvesting(vehicle) then
+ self:debugHarvestDecision(vehicle, fieldID, fillTypeIndex, rawAmount, "SKIP_baler_not_harvesting")
+ return
+ end
+
+ local activeFillType = fillTypeIndex
+ if (activeFillType == nil or activeFillType == FillType.UNKNOWN) and vehicle.getFillUnitFillType ~= nil then
+ activeFillType = vehicle:getFillUnitFillType(fillUnitIndex)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_active_fillType")
+ return
+ end
+
+ if not self:isTrackedFillType(activeFillType) then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_fillType_not_registered_fruit")
+ return
+ end
+
+ if fieldID == nil then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_fieldID")
+ return
+ end
+
+ local fillTypeDesc = g_fillTypeManager.indexToFillType[activeFillType]
+ if fillTypeDesc == nil then
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "SKIP_no_fillTypeDesc")
+ return
+ end
+
+ local currentYear = g_currentMission.environment.currentYear
+ self:recordHarvest(currentYear, fieldID, fillTypeDesc.title, rawAmount, activeFillType)
+ self:debugHarvestDecision(vehicle, fieldID, activeFillType, rawAmount, "RECORDED")
+end
+
+function YieldTracker:getIsCombineHarvesting(vehicle)
+ if vehicle == nil or vehicle.spec_combine == nil then
+ return false
+ end
+
+ local spec = vehicle.spec_combine
+ if spec.attachedCutters ~= nil then
+ for _, cutter in pairs(spec.attachedCutters) do
+ if cutter ~= nil and cutter.spec_cutter ~= nil and cutter.spec_cutter.isWorking then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
+function YieldTracker:getIsBalerHarvesting(vehicle)
+ if vehicle == nil or vehicle.spec_baler == nil then
+ return false
+ end
+
+ if vehicle.getIsTurnedOn ~= nil and vehicle:getIsTurnedOn() then
+ return true
+ end
+
+ if vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() > 0.5 then
+ return true
+ end
+
+ return true
+end
+
+function YieldTracker:getHarvestFieldID(vehicle, isBaler)
+ if isBaler then
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ return g_farmlandManager:getFarmlandIdAtWorldPosition(x, z)
+ end
+
+ return self:getCutterFieldID(vehicle)
+end
+
+-- UTIL METHODS
+--=======================================================================================================================================================================
+
+-- Fetches the currently attached cutter's fill types.
+function YieldTracker:getCutterStatus(vehicle)
+ local spec = vehicle.spec_combine
+ if not spec then
+ return FillType.UNKNOWN, nil
+ end
+
+ if spec.attachedCutters then
+ for _, cutter in pairs(spec.attachedCutters) do
+ if cutter and cutter.spec_cutter and cutter.spec_cutter.isWorking then
+ local cutterInputFillType = cutter.spec_cutter.currentInputFillType or FillType.UNKNOWN
+ local cutterOutputFillType = cutter.spec_cutter.currentOutputFillType or FillType.UNKNOWN
+ return cutterInputFillType, cutterOutputFillType
+ end
+ end
+ end
+
+ return FillType.UNKNOWN, nil
+end
+
+
+---Fetches the current combine fill unit status.
+function YieldTracker:getCombineStatus(vehicle)
+ if not vehicle or not vehicle.spec_combine then
+ return false, FillType.UNKNOWN, 0
+ end
+
+ local spec = vehicle.spec_combine
+ local fillUnitIndex = spec.fillUnitIndex
+ local fillUnitCapacity = vehicle:getFillUnitCapacity(fillUnitIndex)
+
+ -- Combine has an Internal fill unit - case
+ if type(fillUnitCapacity) == "number" and fillUnitCapacity > 0 and fillUnitCapacity ~= math.huge then
+ -- local isFilling = spec.isFilling -- DEPRACTED
+ local fillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex) or 0
+ local combineFillType = vehicle:getFillUnitFillType(fillUnitIndex)
+ local hasInternalFillUnit = true
+ return hasInternalFillUnit, combineFillType, fillLevel
+ end
+
+ -- Discharge to external target (fillUnitCapacity <= 0 or is inf)
+ local dischargeSpec = vehicle.spec_combine.spec_dischargeable
+ if dischargeSpec then
+ for i, dischargeNode in ipairs(dischargeSpec.dischargeNodes) do
+ local targetObject, targetFillUnitIndex = vehicle:getDischargeTargetObject(dischargeNode)
+
+ if targetObject and targetFillUnitIndex then
+ local fillLevel = targetObject:getFillUnitFillLevel(targetFillUnitIndex)
+ local targetFillType = targetObject:getFillUnitFillType(targetFillUnitIndex)
+ local hasInternalFillUnit = false
+ return hasInternalFillUnit, targetFillType, fillLevel
+ end
+ end
+ end
+
+
+ return nil, nil, nil
+end
+
+
+-- Fallback method to determine the field's fill type when the fill unit is empty but the cutter is active.
+function YieldTracker:getFieldFillTypeFallback(vehicle)
+ local cutterFieldID = self:getCutterFieldID(vehicle)
+ if not cutterFieldID then
+ return nil
+ end
+
+ local field = g_fieldManager.fields[cutterFieldID]
+ if not field then
+ return nil
+ end
+
+ local fruitTypeIndex = field.fieldState.fruitTypeIndex
+ if fruitTypeIndex == nil or fruitTypeIndex == 0 then
+ return nil
+ end
+
+ local fruitType = g_fruitTypeManager:getFruitTypeByIndex(fruitTypeIndex)
+ if fruitType then
+ return fruitType.fillType
+ end
+
+ return FillType.UNKNOWN
+end
+
+
+-- Determines fill type based on primary, secondary triggers, or fallback (cutter input/output, combine fill unit, and field fill type).
+function YieldTracker:getActiveFillType(primary, secondary, fallback)
+ if primary and primary ~= FillType.UNKNOWN then
+ return primary
+ elseif secondary and secondary ~= FillType.UNKNOWN then
+ return secondary
+ elseif fallback and fallback ~= FillType.Unknow then
+ return fallback -- Field fillType
+ end
+ return FillType.UNKNOWN
+end
+
+
+-- Uses the cutter's position to determine the current field being harvested.
+function YieldTracker:getCutterFieldID(vehicle)
+ local spec_combine = vehicle.spec_combine
+ if not spec_combine then return nil end
+
+ for _, cutter in pairs(spec_combine.attachedCutters) do
+ if cutter and cutter.spec_cutter then
+ local x, y, z = getWorldTranslation(cutter.rootNode)
+ local fieldID = g_farmlandManager:getFarmlandIdAtWorldPosition(x, z)
+ return fieldID
+ end
+ end
+ return nil
+end
+
+
+-- Checks if a given fill type index is in the tracked crop types.
+function YieldTracker:isTrackedFillType(fillTypeIndex)
+ if fillTypeIndex == nil then
+ return false
+ end
+
+ if g_fruitTypeManager == nil or g_fruitTypeManager.getFruitTypes == nil then
+ return false
+ end
+
+ for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do
+ if fruitType ~= nil then
+ local fillType = fruitType.fillType
+ local fruitFillTypeIndex = fruitType.fillTypeIndex
+
+ if fillType ~= nil then
+ fruitFillTypeIndex = fillType.index or fillType.fillTypeIndex or fruitFillTypeIndex
+ end
+
+ if fruitFillTypeIndex == fillTypeIndex then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
+
+---Updates the yieldData table with the latest harvested amount. Also referenced during game save.
+function YieldTracker:recordHarvest(year, fieldId, cropName, amount, fillTypeIndex)
+ self.yieldData[year] = self.yieldData[year] or {}
+ self.yieldData[year][fieldId] = self.yieldData[year][fieldId] or {}
+
+ local fillTypeEntry = self.yieldData[year][fieldId][fillTypeIndex]
+ if not fillTypeEntry then
+ self.yieldData[year][fieldId][fillTypeIndex] = {
+ amount = 0,
+ fillTypeTitle = cropName
+ }
+ fillTypeEntry = self.yieldData[year][fieldId][fillTypeIndex]
+ end
+
+ fillTypeEntry.amount = fillTypeEntry.amount + amount
+
+ self:debugMessage(string.format(
+ "Recorded %.2fL of %s (fillTypeIndex=%d) on field %d (year %d)",
+ amount, cropName, fillTypeIndex, fieldId, year
+ ))
+
+end
+
+
+-- Prints a debug message if debugging is enabled.
+function YieldTracker:debugMessage(message)
+ if self.debug then
+ print(PRINT_PREFIX .. message)
+ end
+end
+
+--=======================================================================================================================================================================
+
+
+
+
+
+-- COMBINE FILL LEVEL DELTA DEBUG HOOK
+--=======================================================================================================================================================================
+function YieldTracker:installCombineFillLevelDeltaDebugHook()
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() called")
+
+ if self.combineFillLevelDeltaDebugHookInstalled then
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() skipped: already installed")
+ return
+ end
+
+ self.combineFillLevelDebugCallCount = self.combineFillLevelDebugCallCount or {}
+ self.combineFillLevelDebugMaxIdleLogsPerVehicle = 25
+ self.combinePreviousFillLevels = self.combinePreviousFillLevels or {}
+ self.combinePreviousFillTypes = self.combinePreviousFillTypes or {}
+
+ self:debugHarvestMessage("DELTA_EXISTS Combine=" .. tostring(Combine ~= nil))
+ self:debugHarvestMessage("DELTA_EXISTS Combine.onUpdateTick=" .. tostring(Combine ~= nil and type(Combine.onUpdateTick) == "function"))
+ self:debugHarvestMessage("DELTA_EXISTS Combine.updateTick=" .. tostring(Combine ~= nil and type(Combine.updateTick) == "function"))
+
+ if Combine == nil or type(Combine.onUpdateTick) ~= "function" then
+ self:debugHarvestMessage("ERROR: Combine.onUpdateTick not found; combine delta debug hook not installed")
+ return
+ end
+
+ local tracker = self
+
+ Combine.onUpdateTick = Utils.overwrittenFunction(Combine.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local result = superFunc(vehicle, dt, ...)
+
+ -- Compare against the previous onUpdateTick call, not against a value
+ -- read earlier in the same tick. In FS25 the combine fill level can
+ -- already be updated before Combine.onUpdateTick is entered.
+ local currentLevel, currentFillType, fillUnitIndex = tracker:getCombineInternalFillState(vehicle)
+ local vehicleId = vehicle ~= nil and (vehicle.id or tostring(vehicle)) or nil
+ local key = nil
+
+ if vehicleId ~= nil and fillUnitIndex ~= nil then
+ key = tostring(vehicleId) .. ":" .. tostring(fillUnitIndex)
+ end
+
+ local previousLevel = nil
+ local previousFillType = nil
+
+ if key ~= nil then
+ previousLevel = tracker.combinePreviousFillLevels[key]
+ previousFillType = tracker.combinePreviousFillTypes[key]
+ end
+
+ local delta = nil
+ if previousLevel ~= nil and currentLevel ~= nil then
+ delta = currentLevel - previousLevel
+ end
+
+ tracker:handleCombineFillLevelDeltaDebug(vehicle, fillUnitIndex, previousLevel, currentLevel, delta, previousFillType, currentFillType)
+
+ -- Update baseline only after logging/recording.
+ if key ~= nil and currentLevel ~= nil then
+ tracker.combinePreviousFillLevels[key] = currentLevel
+ tracker.combinePreviousFillTypes[key] = currentFillType
+ end
+
+ return result
+ end)
+
+ self.combineFillLevelDeltaDebugHookInstalled = true
+ self:debugHarvestMessage("DELTA_INSTALLED Combine.onUpdateTick")
+ self:debugHarvestMessage("installCombineFillLevelDeltaDebugHook() finished")
+end
+
+function YieldTracker:getCombineInternalFillState(vehicle)
+ if vehicle == nil or vehicle.spec_combine == nil then
+ return nil, nil, nil
+ end
+
+ local spec = vehicle.spec_combine
+ local fillUnitIndex = spec.fillUnitIndex
+ if fillUnitIndex == nil then
+ return nil, nil, nil
+ end
+
+ local fillLevel = nil
+ local fillType = nil
+
+ if vehicle.getFillUnitFillLevel ~= nil then
+ fillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex)
+ end
+
+ if vehicle.getFillUnitFillType ~= nil then
+ fillType = vehicle:getFillUnitFillType(fillUnitIndex)
+ end
+
+ return fillLevel, fillType, fillUnitIndex
+end
+
+function YieldTracker:handleCombineFillLevelDeltaDebug(vehicle, fillUnitIndex, oldLevel, newLevel, delta, oldFillType, newFillType)
+ if not self.debugHarvest then
+ return
+ end
+
+ if vehicle == nil then
+ self:debugHarvestMessage("DELTA_SKIP vehicle=nil")
+ return
+ end
+
+ local vehicleId = vehicle.id or tostring(vehicle)
+ self.combineFillLevelDebugCallCount[vehicleId] = (self.combineFillLevelDebugCallCount[vehicleId] or 0) + 1
+
+ local fieldID = self:getCutterFieldID(vehicle)
+ local cutterInputFillType, cutterOutputFillType = self:getCutterStatus(vehicle)
+ local activeFillType = self:getActiveFillType(newFillType, cutterOutputFillType, cutterInputFillType)
+
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
+
+ local fillTypeTitle = self:getFillTypeDebugTitle(activeFillType)
+ local vehicleName = self:getVehicleDebugName(vehicle)
+ local amount = tonumber(delta or 0) or 0
+
+ if newLevel == nil then
+ self:debugHarvestMessage(string.format(
+ "DELTA_SKIP vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%s new=%s delta=%s activeFillType=%s(%s) reason=no_current_fill_state",
+ vehicleName, tostring(vehicleId), tostring(fieldID), tostring(fillUnitIndex), tostring(oldLevel), tostring(newLevel), tostring(delta), fillTypeTitle, tostring(activeFillType)
+ ))
+ return
+ end
+
+ if oldLevel == nil then
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA_BASELINE vehicle='%s' vehicleId=%s field=%s fillUnit=%s current=%.2f newFillType=%s activeFillType=%s(%s) reason=baseline_initialized",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(newLevel or 0) or 0,
+ tostring(newFillType),
+ fillTypeTitle,
+ tostring(activeFillType)
+ ))
+ return
+ end
+
+ if amount > 0.001 then
+ local reason = "DELTA_POSITIVE"
+ local recorded = false
+
+ if not self.yieldDataLoaded then
+ reason = "DELTA_POSITIVE_SKIP_yieldData_not_loaded"
+ elseif fieldID == nil then
+ reason = "DELTA_POSITIVE_SKIP_no_fieldID"
+ elseif activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ reason = "DELTA_POSITIVE_SKIP_no_active_fillType"
+ elseif not self:isTrackedFillType(activeFillType) then
+ reason = "DELTA_POSITIVE_SKIP_fillType_not_registered_fruit"
+ else
+ local fillTypeDesc = g_fillTypeManager.indexToFillType[activeFillType]
+ local cropName = fillTypeDesc ~= nil and (fillTypeDesc.title or fillTypeDesc.name or tostring(activeFillType)) or tostring(activeFillType)
+ local currentYear = g_currentMission.environment.currentYear
+ self:recordHarvest(currentYear, fieldID, cropName, amount, activeFillType)
+ reason = "DELTA_POSITIVE_RECORDED"
+ recorded = true
+ end
+
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%.2f new=%.2f delta=%.2f oldFillType=%s newFillType=%s cutterInput=%s cutterOutput=%s activeFillType=%s(%s) recorded=%s reason=%s",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(oldLevel or 0) or 0,
+ tonumber(newLevel or 0) or 0,
+ amount,
+ tostring(oldFillType),
+ tostring(newFillType),
+ tostring(cutterInputFillType),
+ tostring(cutterOutputFillType),
+ fillTypeTitle,
+ tostring(activeFillType),
+ tostring(recorded),
+ reason
+ ))
+ else
+ local count = self.combineFillLevelDebugCallCount[vehicleId] or 0
+ if count <= self.combineFillLevelDebugMaxIdleLogsPerVehicle or count % 300 == 0 then
+ self:debugHarvestMessage(string.format(
+ "COMBINE_DELTA_IDLE vehicle='%s' vehicleId=%s field=%s fillUnit=%s old=%.2f new=%.2f delta=%.2f newFillType=%s activeFillType=%s(%s) call=%d",
+ vehicleName,
+ tostring(vehicleId),
+ tostring(fieldID),
+ tostring(fillUnitIndex),
+ tonumber(oldLevel or 0) or 0,
+ tonumber(newLevel or 0) or 0,
+ amount,
+ tostring(newFillType),
+ self:getFillTypeDebugTitle(activeFillType),
+ tostring(activeFillType),
+ count
+ ))
+ end
+ end
+end
+
+function YieldTracker:getFillTypeDebugTitle(fillTypeIndex)
+ if fillTypeIndex == nil then
+ return "nil"
+ end
+
+ if g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil then
+ local desc = g_fillTypeManager.indexToFillType[fillTypeIndex]
+ if desc ~= nil then
+ return tostring(desc.title or desc.name or fillTypeIndex)
+ end
+ end
+
+ return tostring(fillTypeIndex)
+end
+
+
+
+-- REDUCED PIPE TRACKING HOOK
+-- Tracks only the most likely pipe transfer path: Dischargeable.onUpdateTick.
+-- Debug output is restricted to harvest vehicles only.
+-- Pipe-only comparison formula:
+-- pipeHarvestAmount = targetDelta
+-- targetDelta = liters added to the overload/trailer target during this discharge tick
+-- Tank drawdown is NOT subtracted anymore. For the pipe-only approach, every liter that
+-- leaves the combine/harvester pipe is relevant.
+function YieldTracker:installPipeDischargeTrackingHook()
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() called")
+
+ if self.pipeDischargeTrackingHookInstalled then
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() skipped: already installed")
+ return
+ end
+
+ self.pipeDischargeCallCount = self.pipeDischargeCallCount or {}
+ self.pipeDischargeMaxIdleLogsPerVehicle = 20
+
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_EXISTS Dischargeable=" .. tostring(Dischargeable ~= nil))
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_EXISTS Dischargeable.onUpdateTick=" .. tostring(Dischargeable ~= nil and type(Dischargeable.onUpdateTick) == "function"))
+
+ if Dischargeable == nil or type(Dischargeable.onUpdateTick) ~= "function" then
+ self:debugHarvestMessage("ERROR: Dischargeable.onUpdateTick not found; pipe tracking hook not installed")
+ return
+ end
+
+ local tracker = self
+
+ Dischargeable.onUpdateTick = Utils.overwrittenFunction(Dischargeable.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local args = {...}
+ local beforeCombineLevel, beforeCombineFillType, combineFillUnitIndex = tracker:getCombineInternalFillState(vehicle)
+ local beforeTargetObject, beforeTargetFillUnitIndex, beforeTargetLevel, beforeTargetFillType = tracker:getPipeTargetState(vehicle, args)
+
+ local result = superFunc(vehicle, dt, ...)
+
+ local afterCombineLevel, afterCombineFillType = tracker:getCombineInternalFillState(vehicle)
+ local afterTargetObject, afterTargetFillUnitIndex, afterTargetLevel, afterTargetFillType = tracker:getPipeTargetState(vehicle, args, beforeTargetObject, beforeTargetFillUnitIndex)
+
+ tracker:handlePipeDischargeTracking(
+ vehicle,
+ dt,
+ beforeTargetObject,
+ beforeTargetFillUnitIndex,
+ beforeTargetLevel,
+ beforeTargetFillType,
+ afterTargetObject,
+ afterTargetFillUnitIndex,
+ afterTargetLevel,
+ afterTargetFillType,
+ combineFillUnitIndex,
+ beforeCombineLevel,
+ beforeCombineFillType,
+ afterCombineLevel,
+ afterCombineFillType
+ )
+
+ return result
+ end)
+
+ self.pipeDischargeTrackingHookInstalled = true
+ self:debugHarvestMessage("PIPE_ONLY_DEBUG_V2_INSTALLED Dischargeable.onUpdateTick")
+ self:debugHarvestMessage("installPipeDischargeTrackingHook() finished")
+end
+
+function YieldTracker:getPipeTargetState(vehicle, args, knownTargetObject, knownTargetFillUnitIndex)
+ if knownTargetObject ~= nil and knownTargetFillUnitIndex ~= nil and knownTargetObject.getFillUnitFillLevel ~= nil then
+ local level = knownTargetObject:getFillUnitFillLevel(knownTargetFillUnitIndex)
+ local fillType = nil
+ if knownTargetObject.getFillUnitFillType ~= nil then
+ fillType = knownTargetObject:getFillUnitFillType(knownTargetFillUnitIndex)
+ end
+ return knownTargetObject, knownTargetFillUnitIndex, level, fillType
+ end
+
+ if vehicle == nil or vehicle.getDischargeTargetObject == nil then
+ return nil, nil, nil, nil
+ end
+
+ if args ~= nil then
+ for _, possibleDischargeNode in ipairs(args) do
+ local ok, targetObject, targetFillUnitIndex = pcall(function()
+ return vehicle:getDischargeTargetObject(possibleDischargeNode)
+ end)
+
+ if ok and targetObject ~= nil and targetFillUnitIndex ~= nil then
+ local level = nil
+ local fillType = nil
+ if targetObject.getFillUnitFillLevel ~= nil then
+ level = targetObject:getFillUnitFillLevel(targetFillUnitIndex)
+ end
+ if targetObject.getFillUnitFillType ~= nil then
+ fillType = targetObject:getFillUnitFillType(targetFillUnitIndex)
+ end
+ return targetObject, targetFillUnitIndex, level, fillType
+ end
+ end
+ end
+
+ local dischargeSpec = vehicle.spec_dischargeable or (vehicle.spec_combine ~= nil and vehicle.spec_combine.spec_dischargeable) or nil
+ if dischargeSpec ~= nil and dischargeSpec.dischargeNodes ~= nil then
+ for _, dischargeNode in pairs(dischargeSpec.dischargeNodes) do
+ local ok, targetObject, targetFillUnitIndex = pcall(function()
+ return vehicle:getDischargeTargetObject(dischargeNode)
+ end)
+
+ if ok and targetObject ~= nil and targetFillUnitIndex ~= nil then
+ local level = nil
+ local fillType = nil
+ if targetObject.getFillUnitFillLevel ~= nil then
+ level = targetObject:getFillUnitFillLevel(targetFillUnitIndex)
+ end
+ if targetObject.getFillUnitFillType ~= nil then
+ fillType = targetObject:getFillUnitFillType(targetFillUnitIndex)
+ end
+ return targetObject, targetFillUnitIndex, level, fillType
+ end
+ end
+ end
+
+ return nil, nil, nil, nil
+end
+
+
+function YieldTracker:isPipeDebugHarvestVehicle(vehicle)
+ if vehicle == nil then
+ return false
+ end
+
+ -- Normal combine harvesters and many specialized harvesters use spec_combine.
+ if vehicle.spec_combine ~= nil then
+ return true
+ end
+
+ -- Fallback for modded harvesters that expose a harvester specialization/name instead of spec_combine.
+ if vehicle.spec_harvester ~= nil or vehicle.spec_combineDrivable ~= nil then
+ return true
+ end
+
+ local typeName = nil
+ if vehicle.typeName ~= nil then
+ typeName = tostring(vehicle.typeName):lower()
+ elseif vehicle.configFileName ~= nil then
+ typeName = tostring(vehicle.configFileName):lower()
+ end
+
+ if typeName ~= nil then
+ if typeName:find("combine", 1, true) ~= nil or typeName:find("harvester", 1, true) ~= nil then
+ return true
+ end
+ end
+
+ return false
+end
+
+function YieldTracker:handlePipeDischargeTracking(vehicle, dt, beforeTargetObject, beforeTargetFillUnitIndex, beforeTargetLevel, beforeTargetFillType, afterTargetObject, afterTargetFillUnitIndex, afterTargetLevel, afterTargetFillType, combineFillUnitIndex, beforeCombineLevel, beforeCombineFillType, afterCombineLevel, afterCombineFillType)
+ if vehicle == nil then
+ return
+ end
+
+ if not self:isPipeDebugHarvestVehicle(vehicle) then
+ return
+ end
+
+ if not self.yieldDataLoaded then
+ return
+ end
+
+ local targetDelta = 0
+ if beforeTargetLevel ~= nil and afterTargetLevel ~= nil then
+ targetDelta = (tonumber(afterTargetLevel) or 0) - (tonumber(beforeTargetLevel) or 0)
+ end
+
+ if targetDelta <= 0.001 then
+ return
+ end
+
+ local fieldID = self:getCutterFieldID(vehicle)
+ if fieldID == nil then
+ return
+ end
+
+ local cutterInputFillType, cutterOutputFillType = self:getCutterStatus(vehicle)
+ local activeFillType = self:getActiveFillType(afterCombineFillType, afterTargetFillType, cutterOutputFillType)
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getActiveFillType(cutterOutputFillType, cutterInputFillType, beforeCombineFillType)
+ end
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = self:getFieldFillTypeFallback(vehicle)
+ end
+
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN or not self:isTrackedFillType(activeFillType) then
+ return
+ end
+
+ local currentYear = g_currentMission.environment.currentYear
+ self:addPipeHarvestAmount(currentYear, fieldID, activeFillType, targetDelta)
+
+ -- After a successful pipe transfer, the combine tank usually changed too.
+ -- Refresh the transient tank contribution so the GUI shows pipe + remaining tank.
+ self:updateCombineLiveTank(vehicle, fieldID, activeFillType)
+end
+
+
+
+-- PIPE-BASED FINAL HARVEST CALCULATION
+--=======================================================================================================================================================================
+function YieldTracker:makeYieldKey(year, fieldID, fillTypeIndex)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return nil
+ end
+ return tostring(year) .. ":" .. tostring(fieldID) .. ":" .. tostring(fillTypeIndex)
+end
+
+function YieldTracker:getFillTypeTitle(fillTypeIndex)
+ local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.indexToFillType ~= nil and g_fillTypeManager.indexToFillType[fillTypeIndex] or nil
+ if fillTypeDesc ~= nil then
+ return fillTypeDesc.title or fillTypeDesc.name or tostring(fillTypeIndex)
+ end
+ return tostring(fillTypeIndex)
+end
+
+function YieldTracker:initializePipeBaseFromYieldData()
+ self.pipeBaseTotals = {}
+ self.liveTankContributions = {}
+ self.vehicleLiveTankKeys = {}
+
+ local liveByKey = {}
+ if self.savedLiveTankContributions ~= nil then
+ for _, entry in pairs(self.savedLiveTankContributions) do
+ local key = self:makeYieldKey(entry.year, entry.fieldID, entry.fillTypeIndex)
+ if key ~= nil then
+ liveByKey[key] = (liveByKey[key] or 0) + (tonumber(entry.amount) or 0)
+ end
+ end
+ end
+
+ for year, fields in pairs(self.yieldData or {}) do
+ self.pipeBaseTotals[year] = self.pipeBaseTotals[year] or {}
+ for fieldID, crops in pairs(fields) do
+ self.pipeBaseTotals[year][fieldID] = self.pipeBaseTotals[year][fieldID] or {}
+ for fillTypeIndex, cropData in pairs(crops) do
+ local amount = tonumber(cropData.amount or 0) or 0
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ local liveAmount = liveByKey[key] or 0
+ self.pipeBaseTotals[year][fieldID][fillTypeIndex] = math.max(0, amount - liveAmount)
+ end
+ end
+ end
+
+ self.savedLiveTankContributions = nil
+end
+
+function YieldTracker:getPipeBaseAmount(year, fieldID, fillTypeIndex)
+ if self.pipeBaseTotals == nil or self.pipeBaseTotals[year] == nil or self.pipeBaseTotals[year][fieldID] == nil then
+ return 0
+ end
+ return tonumber(self.pipeBaseTotals[year][fieldID][fillTypeIndex] or 0) or 0
+end
+
+function YieldTracker:getLiveTankAmountForKey(key)
+ local total = 0
+ for _, contribution in pairs(self.liveTankContributions or {}) do
+ if contribution.key == key then
+ total = total + (tonumber(contribution.amount) or 0)
+ end
+ end
+ return total
+end
+
+function YieldTracker:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return
+ end
+
+ self.yieldData[year] = self.yieldData[year] or {}
+ self.yieldData[year][fieldID] = self.yieldData[year][fieldID] or {}
+
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ local amount = self:getPipeBaseAmount(year, fieldID, fillTypeIndex) + self:getLiveTankAmountForKey(key)
+
+ self.yieldData[year][fieldID][fillTypeIndex] = self.yieldData[year][fieldID][fillTypeIndex] or {
+ amount = 0,
+ fillTypeTitle = self:getFillTypeTitle(fillTypeIndex)
+ }
+
+ self.yieldData[year][fieldID][fillTypeIndex].amount = amount
+ self.yieldData[year][fieldID][fillTypeIndex].fillTypeTitle = self:getFillTypeTitle(fillTypeIndex)
+end
+
+function YieldTracker:addPipeHarvestAmount(year, fieldID, fillTypeIndex, amount)
+ amount = tonumber(amount or 0) or 0
+ if year == nil or fieldID == nil or fillTypeIndex == nil or amount <= 0.001 then
+ return
+ end
+
+ self.pipeBaseTotals = self.pipeBaseTotals or {}
+ self.pipeBaseTotals[year] = self.pipeBaseTotals[year] or {}
+ self.pipeBaseTotals[year][fieldID] = self.pipeBaseTotals[year][fieldID] or {}
+ self.pipeBaseTotals[year][fieldID][fillTypeIndex] = (self.pipeBaseTotals[year][fieldID][fillTypeIndex] or 0) + amount
+
+ self:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+end
+
+function YieldTracker:updateCombineLiveTank(vehicle, fieldID, fillTypeIndex)
+ if vehicle == nil or not self.yieldDataLoaded then
+ return
+ end
+
+ local year = g_currentMission.environment.currentYear
+ local vehicleId = vehicle.id or tostring(vehicle)
+ local oldContribution = self.liveTankContributions ~= nil and self.liveTankContributions[vehicleId] or nil
+
+ if oldContribution ~= nil then
+ self.liveTankContributions[vehicleId] = nil
+ self:syncYieldDataEntry(oldContribution.year, oldContribution.fieldID, oldContribution.fillTypeIndex)
+ end
+
+ local tankLevel = 0
+ local tankFillType = fillTypeIndex
+ local fillLevel, currentFillType = self:getCombineInternalFillState(vehicle)
+ if fillLevel ~= nil then
+ tankLevel = tonumber(fillLevel or 0) or 0
+ end
+ if currentFillType ~= nil and currentFillType ~= FillType.UNKNOWN then
+ tankFillType = currentFillType
+ end
+
+ if tankLevel <= 0.001 then
+ return
+ end
+
+ if fieldID == nil or fillTypeIndex == nil or fillTypeIndex == FillType.UNKNOWN then
+ return
+ end
+
+ local key = self:makeYieldKey(year, fieldID, fillTypeIndex)
+ self.liveTankContributions = self.liveTankContributions or {}
+ self.liveTankContributions[vehicleId] = {
+ key = key,
+ year = year,
+ fieldID = fieldID,
+ fillTypeIndex = fillTypeIndex,
+ amount = tankLevel
+ }
+
+ self:syncYieldDataEntry(year, fieldID, fillTypeIndex)
+end
+
+function YieldTracker:installCombineLiveTankTrackingHook()
+ if self.combineLiveTankTrackingHookInstalled then
+ return
+ end
+
+ if Combine == nil or type(Combine.onUpdateTick) ~= "function" then
+ return
+ end
+
+ local tracker = self
+ Combine.onUpdateTick = Utils.overwrittenFunction(Combine.onUpdateTick, function(vehicle, superFunc, dt, ...)
+ local result = superFunc(vehicle, dt, ...)
+
+ if tracker:isPipeDebugHarvestVehicle(vehicle) then
+ local fieldID = tracker:getCutterFieldID(vehicle)
+ local currentLevel, currentFillType = tracker:getCombineInternalFillState(vehicle)
+ local cutterInputFillType, cutterOutputFillType = tracker:getCutterStatus(vehicle)
+ local activeFillType = tracker:getActiveFillType(currentFillType, cutterOutputFillType, cutterInputFillType)
+ if activeFillType == nil or activeFillType == FillType.UNKNOWN then
+ activeFillType = tracker:getFieldFillTypeFallback(vehicle)
+ end
+
+ if fieldID ~= nil and activeFillType ~= nil and activeFillType ~= FillType.UNKNOWN and tracker:isTrackedFillType(activeFillType) then
+ tracker:updateCombineLiveTank(vehicle, fieldID, activeFillType)
+ elseif currentLevel ~= nil and currentLevel <= 0.001 then
+ tracker:updateCombineLiveTank(vehicle, nil, nil)
+ end
+ end
+
+ return result
+ end)
+
+ self.combineLiveTankTrackingHookInstalled = true
+end
+
+-- PIPE-ONLY DEBUG CALCULATION
+--=======================================================================================================================================================================
+function YieldTracker:addPipeOnlyDebugAmount(year, fieldID, fillTypeIndex, amount)
+ if year == nil or fieldID == nil or fillTypeIndex == nil then
+ return
+ end
+
+ amount = tonumber(amount or 0) or 0
+ if amount <= 0.001 then
+ return
+ end
+
+ self.pipeOnlyDebugTotals = self.pipeOnlyDebugTotals or {}
+ self.pipeOnlyDebugTotals[year] = self.pipeOnlyDebugTotals[year] or {}
+ self.pipeOnlyDebugTotals[year][fieldID] = self.pipeOnlyDebugTotals[year][fieldID] or {}
+ self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] = (self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] or 0) + amount
+end
+
+function YieldTracker:getPipeOnlyDebugAmount(year, fieldID, fillTypeIndex)
+ if self.pipeOnlyDebugTotals == nil then
+ return 0
+ end
+
+ if self.pipeOnlyDebugTotals[year] == nil or self.pipeOnlyDebugTotals[year][fieldID] == nil then
+ return 0
+ end
+
+ return self.pipeOnlyDebugTotals[year][fieldID][fillTypeIndex] or 0
+end
+
+function YieldTracker:getCurrentCombineTankForDebug(vehicle, expectedFillType)
+ local level, fillType, fillUnitIndex = self:getCombineInternalFillState(vehicle)
+
+ if expectedFillType ~= nil and fillType ~= nil and fillType ~= expectedFillType then
+ return 0, fillType, fillUnitIndex
+ end
+
+ return tonumber(level or 0) or 0, fillType, fillUnitIndex
+end
+
+function YieldTracker:formatDebugNumber(value)
+ if value == nil then
+ return "nil"
+ end
+ local numberValue = tonumber(value)
+ if numberValue == nil then
+ return tostring(value)
+ end
+ return string.format("%.2f", numberValue)
+end
+
+
+-- SAVE/LOAD METHODS
+--=======================================================================================================================================================================
+
+-- Call required dependencies upon loadmap from main.lua
+function YieldTracker:setup()
+ self:loadYieldData()
+ self:initializePipeBaseFromYieldData()
+ self:installCombineLiveTankTrackingHook()
+ self:installPipeDischargeTrackingHook()
+ self:hookInGameSaveEvent()
+end
+
+
+-- Hooks into the save event to trigger yield data saving on every game save.
+function YieldTracker:hookInGameSaveEvent()
+ FSCareerMissionInfo.saveToXMLFile = Utils.appendedFunction(
+ FSCareerMissionInfo.saveToXMLFile,
+ function() YieldTracker:saveYieldData() end)
+end
+
+
+-- Loads yield tracking data and vehicle baseline fill levels from an XML file in the current savegame directory.
+-- Rebuilds the yieldData structure by year, field, and crop, and restores previous fill levels per vehicle.
+-- If no file exists or loading fails, initializes with empty data and marks data as loaded.
+function YieldTracker:loadYieldData()
+ if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
+ self:debugMessage("Load skipped: no savegame directory")
+ self.yieldDataLoaded = true
+ return
+ end
+
+ local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
+ if not fileExists(filePath) then
+ self:debugMessage("No existing yield data XML found")
+ self.yieldDataLoaded = true
+ return
+ end
+
+ local xmlFile = loadXMLFile("YieldDataXML", filePath)
+ if not xmlFile then
+ self:debugMessage("Failed to load yield data XML")
+ return
+ end
+
+ self.yieldData = {}
+
+ local i = 0
+ while true do
+ local yearKey = string.format("yieldData.year(%d)", i)
+ if not hasXMLProperty(xmlFile, yearKey) then break end
+
+ local year = getXMLInt(xmlFile, yearKey .. "#value")
+ self.yieldData[year] = {}
+
+ local j = 0
+ while true do
+ local fieldKey = string.format("%s.field(%d)", yearKey, j)
+ if not hasXMLProperty(xmlFile, fieldKey) then break end
+
+ local fieldId = getXMLInt(xmlFile, fieldKey .. "#id")
+ self.yieldData[year][fieldId] = {}
+
+ local k = 0
+ while true do
+ local cropKey = string.format("%s.crop(%d)", fieldKey, k)
+ if not hasXMLProperty(xmlFile, cropKey) then break end
+
+ local cropName = getXMLString(xmlFile, cropKey .. "#name")
+ local amount = getXMLFloat(xmlFile, cropKey .. "#amount")
+ local fillTypeIndex = getXMLInt(xmlFile, cropKey .. "#fillTypeIndex")
+
+ self.yieldData[year][fieldId][fillTypeIndex] = {
+ amount = amount,
+ fillTypeTitle = cropName
+ }
+
+ k = k + 1
+ end
+
+ j = j + 1
+ end
+
+ i = i + 1
+ end
+
+ -- Load prevFillLevels baseline per vehicle
+ self.prevFillLevels = {}
+ local v = 0
+ while true do
+ local vehicleKey = string.format("yieldData.prevFillLevels.vehicle(%d)", v)
+ if not hasXMLProperty(xmlFile, vehicleKey) then break end
+
+ local vehicleId = getXMLInt(xmlFile, vehicleKey .. "#id")
+ local level = getXMLFloat(xmlFile, vehicleKey .. "#level")
+
+ if vehicleId and level then
+ self.prevFillLevels[vehicleId] = level
+ end
+
+ v = v + 1
+ end
+
+
+ -- Load transient live tank contributions that were included in the saved display amount.
+ -- They are subtracted from pipeBaseTotals after loading to avoid double counting when the same tank is unloaded after reload.
+ self.savedLiveTankContributions = {}
+ local lt = 0
+ while true do
+ local liveKey = string.format("yieldData.liveTankContributions.vehicle(%d)", lt)
+ if not hasXMLProperty(xmlFile, liveKey) then break end
+
+ table.insert(self.savedLiveTankContributions, {
+ vehicleId = getXMLString(xmlFile, liveKey .. "#id"),
+ year = getXMLInt(xmlFile, liveKey .. "#year"),
+ fieldID = getXMLInt(xmlFile, liveKey .. "#fieldId"),
+ fillTypeIndex = getXMLInt(xmlFile, liveKey .. "#fillTypeIndex"),
+ amount = getXMLFloat(xmlFile, liveKey .. "#amount") or 0
+ })
+
+ lt = lt + 1
+ end
+
+ delete(xmlFile)
+
+ self.yieldDataLoaded = true
+ self:debugMessage("Yield data loaded from XML")
+end
+
+
+
+-- Saves yield tracking data and vehicle baseline fill levels to an XML file in the current savegame directory.
+-- Data saved includes harvested amounts grouped by year, field, and crop, as well as last known fill levels per vehicle.
+function YieldTracker:saveYieldData()
+ if not g_currentMission or not g_currentMission.missionInfo.savegameDirectory then
+ self:debugMessage("Save skipped: no savegame directory")
+ return
+ end
+
+ local filePath = g_currentMission.missionInfo.savegameDirectory .. "/" .. self.saveFileName
+ local xmlFile = createXMLFile("YieldDataXML", filePath, "yieldData")
+
+ -- Save yield data per year/field/crop
+ local i = 0
+ for year, fields in pairs(self.yieldData) do
+ local yearKey = string.format("yieldData.year(%d)", i)
+ setXMLInt(xmlFile, yearKey .. "#value", year)
+
+ local j = 0
+ for fieldId, crops in pairs(fields) do
+ local fieldKey = string.format("%s.field(%d)", yearKey, j)
+ setXMLInt(xmlFile, fieldKey .. "#id", fieldId)
+
+ local k = 0
+ for fillTypeIndex, cropData in pairs(crops) do
+ local cropKey = string.format("%s.crop(%d)", fieldKey, k)
+ setXMLInt(xmlFile, cropKey .. "#fillTypeIndex", fillTypeIndex)
+ setXMLString(xmlFile, cropKey .. "#name", cropData.fillTypeTitle)
+ setXMLFloat(xmlFile, cropKey .. "#amount", cropData.amount)
+ k = k + 1
+ end
+ j = j + 1
+ end
+ i = i + 1
+ end
+
+ -- Save prevFillLevels baseline per vehicle
+ local v = 0
+ for vehicleId, level in pairs(self.prevFillLevels or {}) do
+ local vehicleKey = string.format("yieldData.prevFillLevels.vehicle(%d)", v)
+ setXMLInt(xmlFile, vehicleKey .. "#id", vehicleId)
+ setXMLFloat(xmlFile, vehicleKey .. "#level", level)
+ v = v + 1
+ end
+
+
+
+ -- Save transient live tank contributions separately so they can be subtracted from pipe base totals on reload.
+ local lt = 0
+ for vehicleId, contribution in pairs(self.liveTankContributions or {}) do
+ if contribution.amount ~= nil and contribution.amount > 0.001 then
+ local liveKey = string.format("yieldData.liveTankContributions.vehicle(%d)", lt)
+ setXMLString(xmlFile, liveKey .. "#id", tostring(vehicleId))
+ setXMLInt(xmlFile, liveKey .. "#year", contribution.year)
+ setXMLInt(xmlFile, liveKey .. "#fieldId", contribution.fieldID)
+ setXMLInt(xmlFile, liveKey .. "#fillTypeIndex", contribution.fillTypeIndex)
+ setXMLFloat(xmlFile, liveKey .. "#amount", contribution.amount)
+ lt = lt + 1
+ end
+ end
+
+ saveXMLFile(xmlFile)
+ delete(xmlFile)
+
+ self:debugMessage("Yield data saved to XML")
+end
+
+--=======================================================================================================================================================================
diff --git a/FS25_YieldTracker/translations/translation_br.xml b/FS25_YieldTracker/translations/translation_br.xml
index c4ca0e3..d706971 100644
--- a/FS25_YieldTracker/translations/translation_br.xml
+++ b/FS25_YieldTracker/translations/translation_br.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/translations/translation_cs.xml b/FS25_YieldTracker/translations/translation_cs.xml
index 3987760..021c05a 100644
--- a/FS25_YieldTracker/translations/translation_cs.xml
+++ b/FS25_YieldTracker/translations/translation_cs.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/translations/translation_de.xml b/FS25_YieldTracker/translations/translation_de.xml
index fec1c52..076868c 100644
--- a/FS25_YieldTracker/translations/translation_de.xml
+++ b/FS25_YieldTracker/translations/translation_de.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/translations/translation_en.xml b/FS25_YieldTracker/translations/translation_en.xml
index 1dbe65c..46bec2e 100644
--- a/FS25_YieldTracker/translations/translation_en.xml
+++ b/FS25_YieldTracker/translations/translation_en.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FS25_YieldTracker/translations/translation_fr.xml b/FS25_YieldTracker/translations/translation_fr.xml
index bd49fc8..1602b89 100644
--- a/FS25_YieldTracker/translations/translation_fr.xml
+++ b/FS25_YieldTracker/translations/translation_fr.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/translations/translation_pl.xml b/FS25_YieldTracker/translations/translation_pl.xml
index 569caab..5c6686a 100644
--- a/FS25_YieldTracker/translations/translation_pl.xml
+++ b/FS25_YieldTracker/translations/translation_pl.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_YieldTracker/translations/translation_pt.xml b/FS25_YieldTracker/translations/translation_pt.xml
index bd762d4..32d1e8b 100644
--- a/FS25_YieldTracker/translations/translation_pt.xml
+++ b/FS25_YieldTracker/translations/translation_pt.xml
@@ -1,40 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+