analyserMiniWindow = nil analyserButton = nil if not configPopupWindow then configPopupWindow = {} end openedWindows = {} cancelNextRelease = nil -- Party member tracking partyMemberCheckEvent = nil local lastPartyMembers = {} local analyserWindows = { huntingButton = 'styles/hunting', lootButton = 'styles/loot', supplyButton = 'styles/supply', impactButton = 'styles/impact', damageButton = 'styles/input', xpButton = 'styles/xp', dropButton = 'styles/droptracker', partyButton = 'styles/partyhunt', bossButton = 'styles/boss' } -- Utility function to get combat name from effect ID function getCombatName(effectId) if not effectId then return "Unknown" end -- Use the clientCombat table from player.lua if clientCombat and clientCombat[effectId] then return clientCombat[effectId].id or "Unknown" end -- Fallback names if clientCombat is not available local combatNames = { [0] = "Physical", [1] = "Fire", [2] = "Earth", [3] = "Energy", [4] = "Ice", [5] = "Holy", [6] = "Death", [7] = "Healing", [8] = "Drown", [9] = "Lifedrain", [10] = "Manadrain" } return combatNames[effectId] or "Unknown" end -- objects function init() analyserButton = modules.game_mainpanel.addToggleButton('analyzerButton', tr('Open analytics selector window'), '/images/options/analyzers', toggle) analyserButton:setOn(false) analyserMiniWindow = g_ui.loadUI('analyser') analyserMiniWindow:disableResize() analyserMiniWindow:close() analyserMiniWindow:setup() -- Hide buttons we don't want in the main analyser selector window local toggleFilterButton = analyserMiniWindow:recursiveGetChildById('toggleFilterButton') if toggleFilterButton then toggleFilterButton:setVisible(false) end local newWindowButton = analyserMiniWindow:recursiveGetChildById('newWindowButton') if newWindowButton then newWindowButton:setVisible(false) end -- Position contextMenuButton where toggleFilterButton was (to the left of minimize button) local contextMenuButton = analyserMiniWindow:recursiveGetChildById('contextMenuButton') local minimizeButton = analyserMiniWindow:recursiveGetChildById('minimizeButton') if contextMenuButton and minimizeButton then contextMenuButton:setVisible(true) contextMenuButton:breakAnchors() contextMenuButton:addAnchor(AnchorTop, minimizeButton:getId(), AnchorTop) contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft) contextMenuButton:setMarginRight(7) -- Same margin as toggleFilterButton had contextMenuButton:setMarginTop(0) end -- Position lockButton to the left of contextMenuButton local lockButton = analyserMiniWindow:recursiveGetChildById('lockButton') if lockButton and contextMenuButton then lockButton:setVisible(true) lockButton:breakAnchors() lockButton:addAnchor(AnchorTop, contextMenuButton:getId(), AnchorTop) lockButton:addAnchor(AnchorRight, contextMenuButton:getId(), AnchorLeft) lockButton:setMarginRight(2) -- Same margin as in miniwindow style lockButton:setMarginTop(0) end configPopupWindow["lootButton"] = g_ui.displayUI('styles/lootTarget') configPopupWindow["lootButton"]:hide() configPopupWindow["impactButton"] = g_ui.displayUI('styles/dpshpsTarget') configPopupWindow["impactButton"]:hide() configPopupWindow["xpButton"] = g_ui.displayUI('styles/xpTarget') configPopupWindow["xpButton"]:hide() configPopupWindow["dropButton"] = g_ui.displayUI('styles/dropTarget') configPopupWindow["dropButton"]:hide() huntingButton = analyserMiniWindow:recursiveGetChildById("huntingButton") lootButton = analyserMiniWindow:recursiveGetChildById("lootButton") supplyButton = analyserMiniWindow:recursiveGetChildById("supplyButton") impactButton = analyserMiniWindow:recursiveGetChildById("impactButton") damageButton = analyserMiniWindow:recursiveGetChildById("damageButton") xpButton = analyserMiniWindow:recursiveGetChildById("xpButton") dropButton = analyserMiniWindow:recursiveGetChildById("dropButton") partyButton = analyserMiniWindow:recursiveGetChildById("partyButton") bossButton = analyserMiniWindow:recursiveGetChildById("bossButton") for id, style in pairs(analyserWindows) do openedWindows[id] = g_ui.loadUI(style) if openedWindows[id] then openedWindows[id]:setup() openedWindows[id].closeButton.onClick = function() toggleAnalysers(id) end openedWindows[id]:close() local scrollbar = openedWindows[id]:getChildById('miniwindowScrollBar') scrollbar:mergeStyle({ ['$!on'] = { }}) end end HuntingAnalyser:create() HuntingAnalyser:updateWindow() LootAnalyser:create() LootAnalyser:updateWindow() SupplyAnalyser:create() SupplyAnalyser:updateWindow() ImpactAnalyser:create() ImpactAnalyser:updateWindow() InputAnalyser:create() InputAnalyser:updateWindow() XPAnalyser:create() XPAnalyser:updateWindow() DropTrackerAnalyser:create() DropTrackerAnalyser:updateWindow() PartyHuntAnalyser:create() PartyHuntAnalyser:updateWindow() BossCooldown:create() BossCooldown:updateWindow() connect(g_game, { onGameStart = onlineAnalyser, onGameEnd = offlineAnalyser, onSupplyTracker = onSupplyTracker, onLootStats = onLootStats, onImpactTracker = onImpactTracker, onKillTracker = onKillTracker, onPartyAnalyzer = onPartyAnalyzer, onBossCooldown = onBossCooldown, onUpdateExperience = onUpdateExperience, }) connect(LocalPlayer, { onExperienceChange = onExperienceChange, onLevelChange = onLevelChange, onPartyMembersChange = onPartyMembersChange }) connect(Creature, { onShieldChange = onShieldChange, }) -- Set up party member tracking as backup (less frequent since shield changes trigger it) if partyMemberCheckEvent then partyMemberCheckEvent:cancel() end partyMemberCheckEvent = cycleEvent(checkPartyMembersChange, 5000) -- Every 5 seconds as backup end function terminate() if analyserButton then analyserButton:destroy() analyserButton = nil end if analyserMiniWindow then analyserMiniWindow:destroy() analyserMiniWindow = nil end for _, w in pairs(openedWindows) do w:destroy() end openedWindows = {} for _, w in pairs(configPopupWindow) do w:destroy() end configPopupWindow = {} disconnect(g_game, { onGameStart = onlineAnalyser, onGameEnd = offlineAnalyser, onSupplyTracker = onSupplyTracker, onLootStats = onLootStats, onImpactTracker = onImpactTracker, onKillTracker = onKillTracker, onPartyAnalyzer = onPartyAnalyzer, onBossCooldown = onBossCooldown, onUpdateExperience = onUpdateExperience, }) disconnect(LocalPlayer, { onExperienceChange = onExperienceChange, onLevelChange = onLevelChange, onPartyMembersChange = onPartyMembersChange }) disconnect(Creature, { onShieldChange = onShieldChange, }) -- Clean up party member tracking if partyMemberCheckEvent then partyMemberCheckEvent:cancel() partyMemberCheckEvent = nil end end function startNewSession(login) -- Hunting HuntingAnalyser:reset() if login then HuntingAnalyser:loadConfigJson() end HuntingAnalyser:updateWindow(true) -- Loot LootAnalyser:reset() LootAnalyser:updateWindow(true, true) -- Supply SupplyAnalyser:reset() SupplyAnalyser:updateWindow(true, true) ImpactAnalyser:reset() if login then ImpactAnalyser:loadConfigJson() end ImpactAnalyser:updateWindow(true) InputAnalyser:reset() if login then InputAnalyser:loadConfigJson() end InputAnalyser:updateWindow(true) XPAnalyser:reset() if login then XPAnalyser:loadConfigJson() end XPAnalyser:updateWindow(true) DropTrackerAnalyser:reset(login) if login then DropTrackerAnalyser:loadConfigJson() end DropTrackerAnalyser:updateWindow(true) PartyHuntAnalyser:reset() PartyHuntAnalyser:updateWindow(true, true) PartyHuntAnalyser:startEvent() ControllerAnalyser:startEvent() end function onlineAnalyser() local benchmark = g_clock.millis() startNewSession(true) loadGainAndWastConfigJson() end function offlineAnalyser() -- Only save if we have a valid player and can still write to filesystem local player = g_game.getLocalPlayer() if player then -- Ensure the characterdata directory exists before saving anything local characterDir = "/characterdata/" .. player:getId() pcall(function() g_resources.makeDir("/characterdata") end) pcall(function() g_resources.makeDir(characterDir) end) -- Use pcall to safely attempt saves, catching any filesystem errors pcall(function() HuntingAnalyser:saveConfigJson() end) pcall(function() ImpactAnalyser:saveConfigJson() end) pcall(function() InputAnalyser:saveConfigJson() end) pcall(function() XPAnalyser:saveConfigJson() end) pcall(function() DropTrackerAnalyser:saveConfigJson() end) pcall(function() saveGainAndWastConfigJson() end) end BossCooldown.cooldown = {} end function toggle() if analyserMiniWindow:isVisible() then analyserMiniWindow:close() analyserButton:setOn(false) analyserMiniWindow.isOpen = false else if not analyserMiniWindow:getParent() then local panel = modules.game_interface.findContentPanelAvailable(analyserMiniWindow, analyserMiniWindow:getMinimumHeight()) if not panel then return end panel:addChild(analyserMiniWindow) end analyserMiniWindow:open() analyserMiniWindow.isOpen = true analyserButton:setOn(true) end end function hide() analyserMiniWindow:close() analyserMiniWindow.isOpen = false analyserButton:setOn(false) end function onOpen() analyserMiniWindow:setHeight(237) analyserMiniWindow.isOpen = true end function show() analyserMiniWindow:open() analyserMiniWindow.isOpen = true analyserButton:setOn(true) end function toggleAnalysers(buttonId) local buttonWidget = analyserMiniWindow:recursiveGetChildById(buttonId) local widget = openedWindows[buttonId] if not widget then return end if widget:isVisible() then widget:close() widget.isOpen = false buttonWidget:setOn(false) if buttonId == 'bossButton' then toggleBossCDFocus(false) end else widget.isOpen = true widget:open() if buttonId == 'impactButton' then ImpactAnalyser:checkAnchos() elseif buttonId == 'damageButton' then InputAnalyser:checkAnchos() elseif buttonId == 'xpButton' then XPAnalyser:checkAnchos() XPAnalyser:forceUpdateUI() -- Update UI with any accumulated XP data elseif buttonId == 'bossButton' then toggleBossCDFocus(false) widget:focus() elseif buttonId == 'xpAnalyser' then XPAnalyser:checkAnchos() XPAnalyser:forceUpdateUI() -- Update UI with any accumulated XP data end -- Properly assign widget to a panel if it doesn't have a parent if not widget:getParent() then local panel = modules.game_interface.findContentPanelAvailable(widget, widget:getMinimumHeight()) if not panel then return end panel:addChild(widget) end widget:getParent():moveChildToIndex(widget, #widget:getParent():getChildren()) buttonWidget:setOn(true) end end function onExperienceChange(localPlayer, value) -- This function is called when the player's total experience changes -- We should track the experience progression here -- Calculate XP gain BEFORE setting up start exp local previousExp = XPAnalyser.lastExp -- Setup start experience if this is the first time HuntingAnalyser:setupStartExp(value) XPAnalyser:setupStartExp(value) -- Calculate XP gain from experience change using the previous value -- Only calculate gain if we have a previous value and current value is higher if previousExp and previousExp > 0 and value > previousExp then local gain = value - previousExp HuntingAnalyser:addRawXPGain(gain) HuntingAnalyser:addXpGain(gain) XPAnalyser:addRawXPGain(gain) XPAnalyser:addXpGain(gain) end -- Update the last experience for next comparison XPAnalyser.lastExp = value HuntingAnalyser.lastExp = value end function onUpdateExperience(rawExp, exp) end function onLootStats(item, name) HuntingAnalyser:addLootedItems(item, name) LootAnalyser:addLootedItems(item, name) end function onSupplyTracker(itemId) HuntingAnalyser:addSuppliesItems(itemId) SupplyAnalyser:addSuppliesItems(itemId) end function onImpactTracker(analyzerType, amount, effect, target) if analyzerType == ANALYZER_HEAL then HuntingAnalyser:addHealing(amount) ImpactAnalyser:addHealing(amount) elseif analyzerType == ANALYZER_DAMAGE_DEALT then HuntingAnalyser:addDealDamage(amount) ImpactAnalyser:addDealDamage(amount, effect) elseif analyzerType == ANALYZER_DAMAGE_RECEIVED then InputAnalyser:addInputDamage(amount, effect, target) end end function onKillTracker(monsterName, monsterOutfit, dropItems) HuntingAnalyser:addMonsterKilled(monsterName) DropTrackerAnalyser:checkMonsterKilled(monsterName, monsterOutfit, dropItems) end -- Loot and Wast file function loadGainAndWastConfigJson() local config = { gainGaugeTarget = 0, gainGaugeVisible = true, gainGraphVisible = true, wasteGaugeTarget = 0, wasteGaugeVisible = true, wasteGraphVisible = true, } if not g_game.isOnline() then return end local player = g_game.getLocalPlayer() if not player then return end local file = "/characterdata/" .. player:getId() .. "/gainandwaste.json" if g_resources.fileExists(file) then local status, result = pcall(function() return json.decode(g_resources.readFileContents(file)) end) if not status then return g_logger.error("Error while reading characterdata file. Details: " .. result) end config = result end LootAnalyser:setLootPerHourGauge(config.gainGaugeVisible) LootAnalyser:setLootPerHourGraph(config.gainGraphVisible) LootAnalyser:setTarget(config.gainGaugeTarget) SupplyAnalyser:setSupplyPerHourGauge(config.wasteGaugeVisible) SupplyAnalyser:setSupplyPerHourGraph(config.wasteGraphVisible) SupplyAnalyser:setTarget(config.wasteGaugeTarget) end function saveGainAndWastConfigJson() if not g_game.isOnline() then return end local player = g_game.getLocalPlayer() if not player then return end -- Ensure the characterdata directory exists local characterDir = "/characterdata/" .. player:getId() pcall(function() g_resources.makeDir("/characterdata") end) pcall(function() g_resources.makeDir(characterDir) end) local config = { gainGaugeTarget = LootAnalyser:getTarget(), gainGaugeVisible = LootAnalyser:gaugeIsVisible(), gainGraphVisible = LootAnalyser:graphIsVisible(), wasteGaugeTarget = SupplyAnalyser:getTarget(), wasteGaugeVisible = SupplyAnalyser:gaugeIsVisible(), wasteGraphVisible = SupplyAnalyser:graphIsVisible(), } local file = "/characterdata/" .. player:getId() .. "/gainandwaste.json" local status, result = pcall(function() return json.encode(config, 2) end) if not status then return g_logger.error("Error while saving profile Analyzer data. Data won't be saved. Details: " .. result) end if result:len() > 100 * 1024 * 1024 then return g_logger.error("Something went wrong, file is above 100MB, won't be saved") end -- Safely attempt to write the file, ignoring errors during logout local writeStatus, writeError = pcall(function() return g_resources.writeFileContents(file, result) end) if not writeStatus then -- Log the error but don't spam the console during normal logout g_logger.debug("Could not save GainAndWaste config during logout: " .. tostring(writeError)) end end function checkNumber(self, text) local number = tonumber(text) if (not number or number < 0) and #text > 1 then self:setText('0', false) end end function onLevelChange(localPlayer, value, percent) -- See game_skills/skills.lua onLevelChange: the callback passes the level percent raw -- (centipercent with GameLevelPercentU16, protocol >= 1520). Normalize it with -- getLevelPercent() or the analyser bar renders full. if not localPlayer then return end XPAnalyser:setupLevel(value, localPlayer:getLevelPercent()) end function managerDropTracker(itemId, checked) DropTrackerAnalyser:managerDropItem(itemId, checked) end function isInDropTracker(itemId) return DropTrackerAnalyser:isInDropTracker(itemId) end function onPartyAnalyzer(startTime, leaderID, lootType, membersData, membersName) PartyHuntAnalyser:onPartyAnalyzer(startTime, leaderID, lootType, membersData, membersName) end function onPartyMembersChange(self, members) PartyHuntAnalyser.onPartyMembersChange(self, members) end -- Simplified party checking - no longer needed as PartyState handles everything function checkPartyMembersChange() -- This function is now redundant as PartyState manages all party tracking -- Keeping it for compatibility but it does nothing end function onBossCooldown(cooldown) BossCooldown:setupCooldown(cooldown) end function onCloseMiniWindow(self) self.isOpen = false end function onPlayerLoad() end function onPlayerUnload() end function moveAnalyser(panel, height, minimzed) analyserMiniWindow:setParent(panel) analyserMiniWindow:open() if minimzed then analyserMiniWindow:setHeight(height) analyserMiniWindow:minimize() else -- Hardcoded height if height < 237 then height = 237 end analyserMiniWindow:maximize() analyserMiniWindow:setHeight(height) end return analyserMiniWindow end function moveChildAnalyser(type, panel, height, minimzed) local window = { ['bossCooldowns'] = 'bossButton', ['damageInputAnalyser'] = 'damageButton', ['lootTracker'] = 'dropButton', ['huntingSessionAnalyser'] = 'huntingButton', ['impactAnalyser'] = 'impactButton', ['lootAnalyser'] = 'lootButton', ['partyHuntAnalyser'] = 'partyButton', ['wasteAnalyser'] = 'supplyButton', ['xpAnalyser'] = 'xpButton' } local widget = openedWindows[window[type]] if widget then widget:setParent(panel) widget:open() if minimzed then widget:setHeight(height) widget:minimize() else widget:maximize() widget:setHeight(height) end if type == 'xpAnalyser' then XPAnalyser:checkAnchos() end -- check local buttonWidget = analyserMiniWindow:recursiveGetChildById(window[type]) if buttonWidget then buttonWidget:setOn(true) end end return widget end