-- Ignore some luacheck warnings about global vars, just use a ton of them in WoW Lua -- luacheck: no global -- luacheck: no self local _, Simulationcraft = ... Simulationcraft = LibStub("AceAddon-3.0"):NewAddon(Simulationcraft, "Simulationcraft", "AceConsole-3.0", "AceEvent-3.0") LibRealmInfo = LibStub("LibRealmInfo") -- Set up DataBroker for minimap button SimcLDB = LibStub("LibDataBroker-1.1"):NewDataObject("SimulationCraft", { type = "data source", text = "SimulationCraft", label = "SimulationCraft", icon = "Interface\\AddOns\\SimulationCraft\\logo", OnClick = function() if SimcFrame and SimcFrame:IsShown() then SimcFrame:Hide() else Simulationcraft:PrintSimcProfile(false, false, false) end end, OnTooltipShow = function(tt) tt:AddLine("SimulationCraft") tt:AddLine(" ") tt:AddLine("Click to show SimC input") tt:AddLine("To toggle minimap button, type '/simc minimap'") end }) LibDBIcon = LibStub("LibDBIcon-1.0") local SimcFrame = nil local OptionsDB = nil local SpellCache = {} local SpellCacheLoaded = false local OFFSET_ITEM_ID = 1 local OFFSET_ENCHANT_ID = 2 local OFFSET_GEM_ID_1 = 3 -- local OFFSET_GEM_ID_2 = 4 -- local OFFSET_GEM_ID_3 = 5 local OFFSET_GEM_ID_4 = 6 local OFFSET_GEM_BASE = OFFSET_GEM_ID_1 local OFFSET_SUFFIX_ID = 7 -- local OFFSET_FLAGS = 11 -- local OFFSET_CONTEXT = 12 local OFFSET_BONUS_ID = 13 local OFFSET_GEM_BONUS_FROM_MODS = 2 local ITEM_MOD_TYPE_DROP_LEVEL = 9 -- 28 shows frequently but is currently unknown local ITEM_MOD_TYPE_CRAFT_STATS_1 = 29 local ITEM_MOD_TYPE_CRAFT_STATS_2 = 30 local SUPPORTED_LOADOUT_SERIALIZATION_VERSION = 2 local WeeklyRewards = _G.C_WeeklyRewards -- New talents for Dragonflight local ClassTalents = _G.C_ClassTalents local Traits = _G.C_Traits -- GetAddOnMetadata was global until 10.1. It's now in C_AddOns. This line will use C_AddOns if available and work in either WoW build local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata -- Some global item functions have been moved into C_Item in 11.0 local GetDetailedItemLevelInfo = C_Item and C_Item.GetDetailedItemLevelInfo or GetDetailedItemLevelInfo local GetItemInfoInstant = C_Item and C_Item.GetItemInfoInstant or GetItemInfoInstant local GetItemCount = C_Item and C_Item.GetItemCount or GetItemCount -- Talent string export local bitWidthHeaderVersion = 8 local bitWidthSpecID = 16 local bitWidthRanksPurchased = 6 -- load stuff from extras.lua -- local upgradeTable = Simulationcraft.upgradeTable local slotNames = Simulationcraft.slotNames local simcSlotNames = Simulationcraft.simcSlotNames local specNames = Simulationcraft.SpecNames local profNames = Simulationcraft.ProfNames local regionString = Simulationcraft.RegionString local zandalariLoaBuffs = Simulationcraft.zandalariLoaBuffs -- Most of the guts of this addon were based on a variety of other ones, including -- Statslog, AskMrRobot, and BonusScanner. And a bunch of hacking around with AceGUI. -- Many thanks to the authors of those addons, and to reia for fixing my awful amateur -- coding mistakes regarding objects and namespaces. function Simulationcraft:OnInitialize() -- init databroker OptionsDB = LibStub("AceDB-3.0"):New("SimulationCraftDB", { profile = { minimap = { hide = false, }, closeOnCopy = true, frame = { point = "CENTER", relativeFrame = nil, relativePoint = "CENTER", ofsx = 0, ofsy = 0, width = 750, height = 400, }, }, }); LibDBIcon:Register("SimulationCraft", SimcLDB, OptionsDB.profile.minimap) Simulationcraft:UpdateMinimapButton() Simulationcraft:RegisterChatCommand('simc', 'HandleChatCommand') AddonCompartmentFrame:RegisterAddon({ text = "SimulationCraft", icon = "Interface\\AddOns\\SimulationCraft\\logo", notCheckable = true, func = function() Simulationcraft:PrintSimcProfile(false, false, false) end, }) end function Simulationcraft:OnEnable() end function Simulationcraft:OnDisable() end function Simulationcraft:UpdateMinimapButton() if (OptionsDB.profile.minimap.hide) then LibDBIcon:Hide("SimulationCraft") else LibDBIcon:Show("SimulationCraft") end end local function getLinks(input) local separatedLinks = {} for link in input:gmatch("|c.-|h|r") do separatedLinks[#separatedLinks + 1] = link end return separatedLinks end function Simulationcraft:HandleChatCommand(input) local args = {strsplit(' ', input)} local debugOutput = false local noBags = false local showMerchant = false local links = getLinks(input) for _, arg in ipairs(args) do if arg == 'debug' then debugOutput = true elseif arg == 'nobag' or arg == 'nobags' or arg == 'nb' then noBags = true elseif arg == 'merchant' then showMerchant = true elseif arg == 'minimap' then OptionsDB.profile.minimap.hide = not OptionsDB.profile.minimap.hide DEFAULT_CHAT_FRAME:AddMessage( "SimulationCraft: Minimap button is now " .. (OptionsDB.profile.minimap.hide and "hidden" or "shown") ) Simulationcraft:UpdateMinimapButton() return end end self:PrintSimcProfile(debugOutput, noBags, showMerchant, links) end local function GetItemSplit(itemLink) local itemString = string.match(itemLink, "item:([%-?%d:]+)") local itemSplit = {} -- Split data into a table for _, v in ipairs({strsplit(":", itemString)}) do if v == "" then itemSplit[#itemSplit + 1] = 0 else itemSplit[#itemSplit + 1] = tonumber(v) end end return itemSplit end local function Trim(str) return string.match(str, '^%s*(.-)%s*$') end local function GetItemName(itemLink) local name = string.match(itemLink, '|h%[(.*)%]|') local removeIcons = gsub(name, '|%a.+|%a', '') local trimmed = Trim(removeIcons) -- check for empty string or only spaces if string.match(trimmed, '^%s*$') then return nil end return trimmed end -- char size for utf8 strings local function ChrSize(char) if not char then return 0 elseif char > 240 then return 4 elseif char > 225 then return 3 elseif char > 192 then return 2 else return 1 end end -- SimC tokenize function local function Tokenize(str) str = str or "" -- convert to lowercase and remove spaces str = string.lower(str) str = string.gsub(str, ' ', '_') -- keep stuff we want, dumpster everything else local s = "" for i=1,str:len() do local b = str:byte(i) -- keep digits 0-9 if b >= 48 and b <= 57 then s = s .. str:sub(i,i) -- keep lowercase letters elseif b >= 97 and b <= 122 then s = s .. str:sub(i,i) -- keep %, +, ., _ elseif b == 37 or b == 43 or b == 46 or b == 95 then s = s .. str:sub(i,i) -- save all multibyte chars elseif ChrSize(b) > 1 then local offset = ChrSize(b) - 1 s = s .. str:sub(i, i + offset) i = i + offset -- luacheck: no unused end end -- strip trailing spaces if string.sub(s, s:len())=='_' then s = string.sub(s, 0, s:len()-1) end return s end -- method to add spaces to UnitRace names for proper tokenization local function FormatRace(str) str = str or "" local matches = {} for match, _ in string.gmatch(str, '([%u][%l]*)') do matches[#matches+1] = match end return string.join(' ', unpack(matches)) end -- method for constructing the talent string local function CreateSimcTalentString() local talentInfo = {} local maxTiers = 7 local maxColumns = 3 for tier = 1, maxTiers do for column = 1, maxColumns do local _, _, _, selected, _ = GetTalentInfo(tier, column, GetActiveSpecGroup()) if selected then talentInfo[tier] = column end end end local str = 'talents=' for i = 1, maxTiers do if talentInfo[i] then str = str .. talentInfo[i] else str = str .. '0' end end return str end -- class_talents= builder for dragonflight -- Older function, leave around for reference -- local function GetTalentString(configId) -- local entryStrings = {} -- -- local active = false -- if configId == ClassTalents.GetActiveConfigID() then -- active = true -- end -- -- local configInfo = Traits.GetConfigInfo(configId) -- for _, treeId in pairs(configInfo.treeIDs) do -- local nodes = Traits.GetTreeNodes(treeId) -- for _, nodeId in pairs(nodes) do -- local node = Traits.GetNodeInfo(configId, nodeId) -- if node.ranksPurchased > 0 then -- entryStrings[#entryStrings + 1] = node.activeEntry.entryID .. ":" .. node.activeEntry.rank -- end -- end -- end -- -- local str = "class_talents=" .. table.concat(entryStrings, '/') -- if not active then -- -- comment out the class_talents and then prepend a comment with the loadout name -- str = '# ' .. str -- str = '# Saved Loadout: ' .. configInfo.name .. '\n' .. str -- end -- -- return str -- end -- based on ClassTalentImportExportMixin:WriteLoadoutHeader local function WriteLoadoutHeader(exportStream, serializationVersion, specID, treeHash) exportStream:AddValue(bitWidthHeaderVersion, serializationVersion) exportStream:AddValue(bitWidthSpecID, specID) for _, hashVal in ipairs(treeHash) do exportStream:AddValue(8, hashVal) end end -- based on ClassTalentImportExportMixin:GetActiveEntryIndex(treeNode) local function GetActiveEntryIndex(treeNode) for i, entryID in ipairs(treeNode.entryIDs) do if(treeNode.activeEntry and entryID == treeNode.activeEntry.entryID) then return i; end end return 0; end -- based on ClassTalentImportExportMixin:WriteLoadoutContent local function WriteLoadoutContent(exportStream, configID, treeID) local treeNodes = C_Traits.GetTreeNodes(treeID) for _, treeNodeID in ipairs(treeNodes) do local treeNode = C_Traits.GetNodeInfo(configID, treeNodeID); local isNodeGranted = treeNode.activeRank - treeNode.ranksPurchased > 0; local isNodePurchased = treeNode.ranksPurchased > 0; local isNodeSelected = isNodeGranted or isNodePurchased; local isPartiallyRanked = treeNode.ranksPurchased ~= treeNode.maxRanks; local isChoiceNode = treeNode.type == Enum.TraitNodeType.Selection or treeNode.type == Enum.TraitNodeType.SubTreeSelection; exportStream:AddValue(1, isNodeSelected and 1 or 0); if(isNodeSelected) then exportStream:AddValue(1, isNodePurchased and 1 or 0); if isNodePurchased then exportStream:AddValue(1, isPartiallyRanked and 1 or 0); if(isPartiallyRanked) then exportStream:AddValue(bitWidthRanksPurchased, treeNode.ranksPurchased); end exportStream:AddValue(1, isChoiceNode and 1 or 0); if(isChoiceNode) then local entryIndex = GetActiveEntryIndex(treeNode); if(entryIndex <= 0 or entryIndex > 4) then local configInfo = Traits.GetConfigInfo(configID) local errorMsg = "Talent loadout '" .. configInfo.name .. "' is corrupt/incomplete. Find that talent" .. " loadout in your talents UI and delete or update it. It may be on a different spec." print(errorMsg); error(errorMsg); end -- store entry index as zero-index exportStream:AddValue(2, entryIndex - 1); end end end end end -- based on ClassTalentImportExportMixin:GetLoadoutExportString local function GetExportString(configID) local active = false if configID == ClassTalents.GetActiveConfigID() then active = true end local exportStream = ExportUtil.MakeExportDataStream(); local configInfo = Traits.GetConfigInfo(configID); local currentSpecID = PlayerUtil.GetCurrentSpecID(); local treeID = configInfo.treeIDs[1]; local treeHash = C_Traits.GetTreeHash(treeID); local serializationVersion = C_Traits.GetLoadoutSerializationVersion(); WriteLoadoutHeader(exportStream, serializationVersion, currentSpecID, treeHash ) WriteLoadoutContent(exportStream, configID, treeID) local str = "talents=" .. exportStream:GetExportString() if not active then -- comment out the talents and then prepend a comment with the loadout name str = '# ' .. str -- Make sure any pipe characters get unescaped, otherwise breaks checksums str = '# Saved Loadout: ' .. configInfo.name:gsub("||", "|") .. '\n' .. str end return str end -- function that translates between the game's role values and ours local function TranslateRole(spec_id, str) local spec_role = Simulationcraft.RoleTable[spec_id] if spec_role ~= nil then return spec_role end if str == 'TANK' then return 'tank' elseif str == 'DAMAGER' then return 'attack' elseif str == 'HEALER' then return 'attack' else return '' end end -- =================== Item Information ========================= local function GetItemStringFromItemLink(slotNum, itemLink, debugOutput) local itemSplit = GetItemSplit(itemLink) local simcItemOptions = {} local gems = {} local gemBonuses = {} local debugLines = {} -- Item id local itemId = itemSplit[OFFSET_ITEM_ID] simcItemOptions[#simcItemOptions + 1] = ',id=' .. itemId -- Enchant if itemSplit[OFFSET_ENCHANT_ID] > 0 then simcItemOptions[#simcItemOptions + 1] = 'enchant_id=' .. itemSplit[OFFSET_ENCHANT_ID] end -- Gems for gemOffset = OFFSET_GEM_ID_1, OFFSET_GEM_ID_4 do local gemIndex = (gemOffset - OFFSET_GEM_BASE) + 1 gems[gemIndex] = 0 gemBonuses[gemIndex] = 0 if itemSplit[gemOffset] > 0 then local gemId = itemSplit[gemOffset] if gemId > 0 then gems[gemIndex] = gemId end end end -- Remove any trailing zeros from the gems array while #gems > 0 and gems[#gems] == 0 do table.remove(gems, #gems) end if #gems > 0 then simcItemOptions[#simcItemOptions + 1] = 'gem_id=' .. table.concat(gems, '/') end -- New style item suffix, old suffix style not supported if itemSplit[OFFSET_SUFFIX_ID] ~= 0 then simcItemOptions[#simcItemOptions + 1] = 'suffix=' .. itemSplit[OFFSET_SUFFIX_ID] end local bonuses = {} for index=1, itemSplit[OFFSET_BONUS_ID] do bonuses[#bonuses + 1] = itemSplit[OFFSET_BONUS_ID + index] end if #bonuses > 0 then simcItemOptions[#simcItemOptions + 1] = 'bonus_id=' .. table.concat(bonuses, '/') end -- Shadowlands looks like it changed the item string -- There's now a variable list of additional data after bonus IDs, looks like some kind of type/value pairs local linkOffset = OFFSET_BONUS_ID + #bonuses + 1 local craftedStats = {} local numPairs = itemSplit[linkOffset] for index=1, numPairs do local pairOffset = 1 + linkOffset + (2 * (index - 1)) local pairType = itemSplit[pairOffset] local pairValue = itemSplit[pairOffset + 1] if pairType == ITEM_MOD_TYPE_DROP_LEVEL then simcItemOptions[#simcItemOptions + 1] = 'drop_level=' .. pairValue elseif pairType == ITEM_MOD_TYPE_CRAFT_STATS_1 or pairType == ITEM_MOD_TYPE_CRAFT_STATS_2 then craftedStats[#craftedStats + 1] = pairValue end end if #craftedStats > 0 then simcItemOptions[#simcItemOptions + 1] = 'crafted_stats=' .. table.concat(craftedStats, '/') end -- gem bonuses local gemBonusOffset = linkOffset + (2 * numPairs) + OFFSET_GEM_BONUS_FROM_MODS local numGemBonuses = itemSplit[gemBonusOffset] local gemBonuses = {} for index=1, numGemBonuses do local offset = gemBonusOffset + index gemBonuses[index] = itemSplit[offset] end if #gemBonuses > 0 then simcItemOptions[#simcItemOptions + 1] = 'gem_bonus_id=' .. table.concat(gemBonuses, '/') end local craftingQuality = C_TradeSkillUI.GetItemCraftedQualityByItemInfo(itemLink); if craftingQuality then simcItemOptions[#simcItemOptions + 1] = 'crafting_quality=' .. craftingQuality end -- 11.1.7 Belt if itemId == 242664 or itemId == 245964 or itemId == 245965 or itemId == 245966 then local titanDiscId, tooltipStrings = Simulationcraft:GetTitanDiscBeltSpell() if titanDiscId then simcItemOptions[#simcItemOptions + 1] = 'titan_disc_id=' .. titanDiscId end debugLines[#debugLines + 1] = 'Spell Descriptions:' for i = 1, #tooltipStrings do debugLines[#debugLines + 1] = tooltipStrings[i] end end local itemStr = '' itemStr = itemStr .. (simcSlotNames[slotNum] or 'unknown') .. "=" .. table.concat(simcItemOptions, ',') if debugOutput then debugLines[#debugLines + 1] = gsub(itemLink, "\124", "\124\124") for line = 1, #debugLines do itemStr = itemStr .. '\n# ' .. debugLines[line] end itemStr = itemStr .. '\n' end return itemStr end function Simulationcraft:GetItemStrings(debugOutput) local items = {} for slotNum=1, #slotNames do local slotId = GetInventorySlotInfo(slotNames[slotNum]) local itemLink = GetInventoryItemLink('player', slotId) -- if we don't have an item link, we don't care if itemLink then -- In theory, this should always be loaded/cached local name = GetItemName(itemLink) -- get correct level for scaling gear local level, _, _ = GetDetailedItemLevelInfo(itemLink) local itemComment if name and level then itemComment = name .. ' (' .. level .. ')' end items[slotNum] = { string = GetItemStringFromItemLink(slotNum, itemLink, debugOutput), name = itemComment } end end return items end -- Iterate through all container slots looking for gear that can be equipped. -- Item name and item level may not be available if other addons are causing lookups to be throttled but -- item links and IDs should always be available function Simulationcraft:GetBagItemStrings(debugOutput) local bagItems = {} -- https://warcraft.wiki.gg/wiki/InventorySlotID#Bags -- https://warcraft.wiki.gg/wiki/BagID -- 11.2 unifies character, reagent, and void banks and reorganizes the container IDs -- Before 11.2, the addon basically needed to iteration from index -5 to 11 or so -- After 11.2, it's a much more sane 0 to 17 -- The bound of the iteration can mostly be gleaned from various constants (that have shifted around between patches) -- with the addition of the built-in backpack slot -- This should work for a while if they add any more containers and if the container sizes change as it's all pretty -- dynamic now local invConstants = Constants.InventoryConstants local startSlot = nil local totalSlots = nil if invConstants.NumCharacterBankSlots then -- 11.2 and after startSlot = 0 -- add 1 for the backpack local numBagSlots = 1 + invConstants.NumBagSlots + invConstants.NumReagentBagSlots local numBankSlots = invConstants.NumCharacterBankSlots local numWarbandBankSlots = invConstants.NumAccountBankSlots endSlot = numBagSlots + numBankSlots + numWarbandBankSlots else -- 11.1.7 and before startSlot = BACKPACK_CONTAINER - ITEM_INVENTORY_BANK_BAG_OFFSET endSlot = NUM_TOTAL_EQUIPPED_BAG_SLOTS + NUM_BANKBAGSLOTS end for bag=startSlot, endSlot do for slot=1, C_Container.GetContainerNumSlots(bag) do local itemId = C_Container.GetContainerItemID(bag, slot) -- something is in the bag slot if itemId then local _, _, _, itemEquipLoc = GetItemInfoInstant(itemId) local slotNum = Simulationcraft.invTypeToSlotNum[itemEquipLoc] -- item can be equipped if slotNum then local info = C_Container.GetContainerItemInfo(bag, slot) local itemLink = C_Container.GetContainerItemLink(bag, slot) bagItems[#bagItems + 1] = { string = GetItemStringFromItemLink(slotNum, itemLink, debugOutput), slotNum = slotNum } local itemName = GetItemName(itemLink) local level, _, _ = GetDetailedItemLevelInfo(itemLink) if itemName and level then bagItems[#bagItems].name = itemName .. ' (' .. level .. ')' end end end end end -- order results by paper doll slot, not bag slot table.sort(bagItems, function (a, b) return a.slotNum < b.slotNum end) return bagItems end -- Scan buffs to determine which loa racial this player has, if any function Simulationcraft:GetZandalariLoa() local zandalariLoa = nil for index = 1, 32 do local auraData = C_UnitAuras.GetBuffDataByIndex("player", index) local spellId = auraData.spellId if spellId == nil then break end if zandalariLoaBuffs[spellId] then zandalariLoa = zandalariLoaBuffs[spellId] break end end return zandalariLoa end function Simulationcraft:GetSlotHighWatermarks() if C_ItemUpgrade and C_ItemUpgrade.GetHighWatermarkForSlot then local slots = {} -- These are not normal equipment slots, they are Enum.ItemRedundancySlot for slot = 0, 16 do local characterHighWatermark, accountHighWatermark = C_ItemUpgrade.GetHighWatermarkForSlot(slot) if characterHighWatermark or accountHighWatermark then slots[#slots + 1] = table.concat({ slot, characterHighWatermark, accountHighWatermark }, ':') end end return table.concat(slots, '/') end end function Simulationcraft:GetCatalystCurrencies() local catalystCurrencies = {} for currencyId, currencyName in pairs(Simulationcraft.catalystCurrencies) do local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(currencyId) if currencyInfo then catalystCurrencies[#catalystCurrencies + 1] = table.concat({ currencyId, currencyInfo.quantity }, ':') end end return table.concat(catalystCurrencies, '/') end function Simulationcraft:GetUpgradeCurrencies() local upgradeCurrencies = {} -- Collect actual currencies for currencyId, currencyName in pairs(Simulationcraft.upgradeCurrencies) do local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(currencyId) if currencyInfo and currencyInfo.quantity > 0 then upgradeCurrencies[#upgradeCurrencies + 1] = table.concat({ "c", currencyId, currencyInfo.quantity }, ':') end end -- Collect items that get used as currencies for itemId, itemName in pairs(Simulationcraft.upgradeItems) do local count = GetItemCount(itemId, true, true, true) if count > 0 then upgradeCurrencies[#upgradeCurrencies + 1] = table.concat({ "i", itemId, count }, ':') end end return table.concat(upgradeCurrencies, '/') end function Simulationcraft:GetItemUpgradeAchievements() local achieves = {} for i=1, #Simulationcraft.upgradeAchievements do local achId = Simulationcraft.upgradeAchievements[i] _, name, points, complete = GetAchievementInfo(achId) if complete then achieves[#achieves + 1] = achId end end return table.concat(achieves, '/') end local function LoadSpellsAsync(callback) local spellIds = Simulationcraft.preloadSpellIds -- Build up the SpellCache asynchronously local numLoaded = 0 function onLoad() numLoaded = numLoaded + 1 if numLoaded == #spellIds then SpellCacheLoaded = true if callback then callback(SpellCache) end end end for index=1, #spellIds do local spellId = spellIds[index] local spell = Spell:CreateFromSpellID(spellId) if not spell:IsSpellEmpty() then spell:ContinueOnSpellLoad(function() -- It's possible that in some cases, ContinueOnSpellLoad may not fire. If that happens, -- will need to look into ContinueWithCancelOnSpellLoad which can apparently be used with -- timeouts SpellCache[spellId] = spell onLoad() end) else onLoad() end end end -- This requires the SpellCache with the right spell IDs to be loaded function Simulationcraft:GetTitanDiscBeltSpell() local activeSpell = nil local debugTooltipStrings = {} local beltDescription = Trim(SpellCache[Simulationcraft.discBeltSpell]:GetSpellDescription()) debugTooltipStrings[#debugTooltipStrings + 1] = beltDescription if not beltDescription then error('Unable to get spell description for DISC Belt spell') end for k, v in pairs(Simulationcraft.discBeltEffectSpells) do local effectDesc = Trim(SpellCache[k]:GetSpellDescription()) debugTooltipStrings[#debugTooltipStrings + 1] = effectDesc -- disable pattern matching with the last argument if beltDescription:find(effectDesc, 1, true) then activeSpell = v end end return activeSpell, debugTooltipStrings end function Simulationcraft:GetMainFrame(text) -- Frame code largely adapted from https://www.wowinterface.com/forums/showpost.php?p=323901&postcount=2 if not SimcFrame then -- Main Frame local frameConfig = OptionsDB.profile.frame local f = CreateFrame("Frame", "SimcFrame", UIParent, "DialogBoxFrame") f:ClearAllPoints() -- load position from local DB f:SetPoint( frameConfig.point, frameConfig.relativeFrame, frameConfig.relativePoint, frameConfig.ofsx, frameConfig.ofsy ) f:SetSize(frameConfig.width, frameConfig.height) f:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\PVPFrame\\UI-Character-PVP-Highlight", edgeSize = 16, insets = { left = 8, right = 8, top = 8, bottom = 8 }, }) f:SetMovable(true) f:SetClampedToScreen(true) f:SetScript("OnMouseDown", function(self, button) -- luacheck: ignore if button == "LeftButton" then self:StartMoving() end end) f:SetScript("OnMouseUp", function(self, _) -- luacheck: ignore self:StopMovingOrSizing() -- save position between sessions local point, relativeFrame, relativeTo, ofsx, ofsy = self:GetPoint() frameConfig.point = point frameConfig.relativeFrame = relativeFrame frameConfig.relativePoint = relativeTo frameConfig.ofsx = ofsx frameConfig.ofsy = ofsy end) -- scroll frame local sf = CreateFrame("ScrollFrame", "SimcScrollFrame", f, "UIPanelScrollFrameTemplate") sf:SetPoint("LEFT", 16, 0) sf:SetPoint("RIGHT", -32, 0) sf:SetPoint("TOP", 0, -32) sf:SetPoint("BOTTOM", SimcFrameButton, "TOP", 0, 0) -- edit box local ctrlDown = false local eb = CreateFrame("EditBox", "SimcEditBox", SimcScrollFrame) eb:SetSize(sf:GetSize()) eb:SetMultiLine(true) eb:SetAutoFocus(true) eb:SetFontObject("ChatFontNormal") eb:SetScript("OnEscapePressed", function() f:Hide() end) eb:SetScript("OnKeyDown", function(self, key) if key == "LCTRL" or key == "RCTRL" or key == "LMETA" or key == "RMETA" then ctrlDown = true end end) eb:SetScript("OnKeyUp", function(self, key) if key == "LCTRL" or key == "RCTRL" or key == "LMETA" or key == "RMETA" then -- Add a small grace period. In testing, the way I press Ctrl-C would sometimes have Ctrl keyup bfore C C_Timer.After(0.2, function() ctrlDown = false end) end if ctrlDown then -- handle copy or cut if key == "C" or key == "X" then if OptionsDB.profile.closeOnCopy then -- Just in case there's some weird way that WoW could close the window before the OS copies C_Timer.After(0.1, function() f:Hide() end) end end end end) sf:SetScrollChild(eb) -- resizing f:SetResizable(true) if f.SetMinResize then -- older function from shadowlands and before -- Can remove when Dragonflight is in full swing f:SetMinResize(150, 100) else -- new func for dragonflight f:SetResizeBounds(150, 100, nil, nil) end local rb = CreateFrame("Button", "SimcResizeButton", f) rb:SetPoint("BOTTOMRIGHT", -6, 7) rb:SetSize(16, 16) rb:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up") rb:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight") rb:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down") rb:SetScript("OnMouseDown", function(self, button) -- luacheck: ignore if button == "LeftButton" then f:StartSizing("BOTTOMRIGHT") self:GetHighlightTexture():Hide() -- more noticeable end end) rb:SetScript("OnMouseUp", function(self, _) -- luacheck: ignore f:StopMovingOrSizing() self:GetHighlightTexture():Show() eb:SetWidth(sf:GetWidth()) -- save size between sessions frameConfig.width = f:GetWidth() frameConfig.height = f:GetHeight() end) -- Automatic close checkbox local checkbox = CreateFrame("CheckButton", "AutomaticClose", f, "ChatConfigCheckButtonTemplate") checkbox:SetPoint("BOTTOMLEFT", 12, 18) checkbox.Text:SetText("Close after copy") checkbox:SetChecked(true) checkbox:HookScript("OnClick", function(self) OptionsDB.profile.closeOnCopy = self:GetChecked() end) SimcFrame = f end SimcEditBox:SetText(text) SimcEditBox:HighlightText() return SimcFrame end -- Adapted from https://github.com/philanc/plc/blob/master/plc/checksum.lua local function adler32(s) -- return adler32 checksum (uint32) -- adler32 is a checksum defined by Mark Adler for zlib -- (based on the Fletcher checksum used in ITU X.224) -- implementation based on RFC 1950 (zlib format spec), 1996 local prime = 65521 --largest prime smaller than 2^16 local s1, s2 = 1, 0 -- limit s size to ensure that modulo prime can be done only at end -- 2^40 is too large for WoW Lua so limit to 2^30 if #s > (bit.lshift(1, 30)) then error("adler32: string too large") end for i = 1,#s do local b = string.byte(s, i) s1 = s1 + b s2 = s2 + s1 -- no need to test or compute mod prime every turn. end s1 = s1 % prime s2 = s2 % prime return (bit.lshift(s2, 16)) + s1 end --adler32() function Simulationcraft:GetSimcProfile(debugOutput, noBags, showMerchant, links) -- addon metadata local versionComment = '# SimC Addon ' .. GetAddOnMetadata('Simulationcraft', 'Version') local wowVersion, wowBuild, _, wowToc = GetBuildInfo() local wowVersionComment = '# WoW ' .. wowVersion .. '.' .. wowBuild .. ', TOC ' .. wowToc local simcVersionWarning = '# Requires SimulationCraft 1000-01 or newer' -- Basic player info local _, realmName, _, _, _, _, region, _, _, realmLatinName, _ = LibRealmInfo:GetRealmInfoByUnit('player') local playerName = UnitName('player') local _, playerClass = UnitClass('player') local playerLevel = UnitLevel('player') -- Try Latin name for Russian servers first, then realm name from LibRealmInfo, then Realm Name from the game -- Latin name for Russian servers as most APIs use the latin name, not the cyrillic name local playerRealm = realmLatinName or realmName or GetRealmName() -- Try region from LibRealmInfo first, then use default API -- Default API can be wrong for region-switching players local playerRegion = region or GetCurrentRegionName() or regionString[GetCurrentRegion()] -- Race info local _, playerRace = UnitRace('player') -- fix some races to match SimC format if playerRace == 'Scourge' then --lulz playerRace = 'Undead' else playerRace = FormatRace(playerRace) end local isZandalariTroll = false if Tokenize(playerRace) == 'zandalari_troll' then isZandalariTroll = true end -- Spec info local role, globalSpecID, playerRole local specId = GetSpecialization() if specId then globalSpecID,_,_,_,_,role = GetSpecializationInfo(specId) end local playerSpec = specNames[ globalSpecID ] or 'unknown' -- Loot spec local lootSpecId = GetLootSpecialization() if lootSpecId == 0 then lootSpecId = globalSpecID end local playerLootSpec = specNames[ lootSpecId ] -- Professions local pid1, pid2 = GetProfessions() local firstProf, firstProfRank, secondProf, secondProfRank, profOneId, profTwoId if pid1 then _,_,firstProfRank,_,_,_,profOneId = GetProfessionInfo(pid1) end if pid2 then _,_,secondProfRank,_,_,_,profTwoId = GetProfessionInfo(pid2) end firstProf = profNames[ profOneId ] secondProf = profNames[ profTwoId ] local playerProfessions = '' -- luacheck: ignore if pid1 or pid2 then playerProfessions = 'professions=' if pid1 then playerProfessions = playerProfessions..Tokenize(firstProf)..'='..tostring(firstProfRank)..'/' end if pid2 then playerProfessions = playerProfessions..Tokenize(secondProf)..'='..tostring(secondProfRank) end else playerProfessions = '' end -- create a header comment with basic player info and a date local headerComment = ( "# " .. playerName .. ' - ' .. playerSpec .. ' - ' .. date('%Y-%m-%d %H:%M') .. ' - ' .. playerRegion .. '/' .. playerRealm ) -- Construct SimC-compatible strings from the basic information local player = Tokenize(playerClass) .. '="' .. playerName .. '"' playerLevel = 'level=' .. playerLevel playerRace = 'race=' .. Tokenize(playerRace) playerRole = 'role=' .. TranslateRole(globalSpecID, role) local playerSpecStr = 'spec=' .. Tokenize(playerSpec) playerRealm = 'server=' .. Tokenize(playerRealm) playerRegion = 'region=' .. Tokenize(playerRegion) local playerLootSpecStr = 'loot_spec=' .. Tokenize(playerLootSpec) -- Build the output string for the player (not including gear) local simcPrintError = nil local simulationcraftProfile = '' simulationcraftProfile = simulationcraftProfile .. headerComment .. '\n' simulationcraftProfile = simulationcraftProfile .. versionComment .. '\n' simulationcraftProfile = simulationcraftProfile .. wowVersionComment .. '\n' simulationcraftProfile = simulationcraftProfile .. simcVersionWarning .. '\n' simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. player .. '\n' simulationcraftProfile = simulationcraftProfile .. playerLevel .. '\n' simulationcraftProfile = simulationcraftProfile .. playerRace .. '\n' if isZandalariTroll then local zandalari_loa = Simulationcraft:GetZandalariLoa() if zandalari_loa then simulationcraftProfile = simulationcraftProfile .. "zandalari_loa=" .. zandalari_loa .. '\n' end end simulationcraftProfile = simulationcraftProfile .. playerRegion .. '\n' simulationcraftProfile = simulationcraftProfile .. playerRealm .. '\n' simulationcraftProfile = simulationcraftProfile .. playerRole .. '\n' simulationcraftProfile = simulationcraftProfile .. playerProfessions .. '\n' simulationcraftProfile = simulationcraftProfile .. playerSpecStr .. '\n' simulationcraftProfile = simulationcraftProfile .. '# ' .. playerLootSpecStr .. '\n' simulationcraftProfile = simulationcraftProfile .. '\n' if playerSpec == 'unknown' then -- luacheck: ignore -- do nothing -- Player does not have a spec / is in starting player area elseif ClassTalents then -- DRAGONFLIGHT -- new dragonflight talents if Traits.GetLoadoutSerializationVersion() ~= SUPPORTED_LOADOUT_SERIALIZATION_VERSION then simcPrintError = 'This version of the SimC addon does not work with this version of WoW.\n' simcPrintError = simcPrintError .. 'There is a mismatch in the version of talent string exports.\n' simcPrintError = simcPrintError .. '\n' if Traits.GetLoadoutSerializationVersion() > SUPPORTED_LOADOUT_SERIALIZATION_VERSION then simcPrintError = simcPrintError .. 'WoW is using a newer version - you probably need to update your addon.\n' else simcPrintError = simcPrintError .. 'WoW is using an older version - you may be running an alpha/beta addon that is not currently ready for retail.\n' end simcPrintError = simcPrintError .. '\n' simcPrintError = simcPrintError .. 'WoW talent string export version = ' .. Traits.GetLoadoutSerializationVersion() .. '\n' simcPrintError = simcPrintError .. 'Addon talent string export version = ' .. SUPPORTED_LOADOUT_SERIALIZATION_VERSION .. '\n' end local currentConfigId = ClassTalents.GetActiveConfigID() simulationcraftProfile = simulationcraftProfile .. GetExportString(currentConfigId) .. '\n' simulationcraftProfile = simulationcraftProfile .. '\n' local specConfigs = ClassTalents.GetConfigIDsBySpecID(globalSpecID) for _, configId in pairs(specConfigs) do simulationcraftProfile = simulationcraftProfile .. GetExportString(configId) .. '\n' end else -- old talents local playerTalents = CreateSimcTalentString() simulationcraftProfile = simulationcraftProfile .. playerTalents .. '\n' end simulationcraftProfile = simulationcraftProfile .. '\n' -- Method that gets gear information local items = Simulationcraft:GetItemStrings(debugOutput) -- output gear for slotNum=1, #slotNames do local item = items[slotNum] if item then if item.name then simulationcraftProfile = simulationcraftProfile .. '# ' .. item.name .. '\n' end simulationcraftProfile = simulationcraftProfile .. items[slotNum].string .. '\n' end end -- output gear from bags if noBags == false then local bagItems = Simulationcraft:GetBagItemStrings(debugOutput) if #bagItems > 0 then simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. '### Gear from Bags\n' for i=1, #bagItems do simulationcraftProfile = simulationcraftProfile .. '#\n' if bagItems[i].name and bagItems[i].name ~= '' then simulationcraftProfile = simulationcraftProfile .. '# ' .. bagItems[i].name .. '\n' end simulationcraftProfile = simulationcraftProfile .. '# ' .. bagItems[i].string .. '\n' end end end -- output weekly reward gear if WeeklyRewards then if WeeklyRewards:HasAvailableRewards() then simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. '### Weekly Reward Choices\n' local activities = WeeklyRewards.GetActivities() for _, activityInfo in ipairs(activities) do for _, rewardInfo in ipairs(activityInfo.rewards) do local _, _, _, itemEquipLoc = GetItemInfoInstant(rewardInfo.id) local itemLink = WeeklyRewards.GetItemHyperlink(rewardInfo.itemDBID) local itemName = GetItemName(itemLink); local slotNum = Simulationcraft.invTypeToSlotNum[itemEquipLoc] if slotNum then local itemStr = GetItemStringFromItemLink(slotNum, itemLink, debugOutput) local level, _, _ = GetDetailedItemLevelInfo(itemLink) simulationcraftProfile = simulationcraftProfile .. '#\n' if itemName and level then itemNameComment = itemName .. ' ' .. '(' .. level .. ')' simulationcraftProfile = simulationcraftProfile .. '# ' .. itemNameComment .. '\n' end simulationcraftProfile = simulationcraftProfile .. '# ' .. itemStr .. "\n" end end end simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '### End of Weekly Reward Choices\n' end end -- Dump out equippable items from a vendor, this is mostly for debugging / data collection local numMerchantItems = GetMerchantNumItems() if showMerchant and numMerchantItems > 0 then simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. '\n### Merchant items\n' for i=1,numMerchantItems do local link = GetMerchantItemLink(i) local name,_,_,_,_,_,_,_,invType = GetItemInfo(link) if name and invType ~= "" then local slotNum = Simulationcraft.invTypeToSlotNum[invType] -- Doesn't work, seems to always return base item level -- local level, _, _ = GetDetailedItemLevelInfo(itemLink) local itemStr = GetItemStringFromItemLink(slotNum, link, false) simulationcraftProfile = simulationcraftProfile .. '#\n' if name then simulationcraftProfile = simulationcraftProfile .. '# ' .. name .. '\n' end simulationcraftProfile = simulationcraftProfile .. '# ' .. itemStr .. "\n" end end end -- output item links that were included in the /simc chat line if links and #links > 0 then simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. '\n### Linked gear\n' for _, v in pairs(links) do local name,_,_,_,_,_,_,_,invType = GetItemInfo(v) if name and invType ~= "" then local slotNum = Simulationcraft.invTypeToSlotNum[invType] local itemStr = GetItemStringFromItemLink(slotNum, v, debugOutput) simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '# ' .. name .. '\n' simulationcraftProfile = simulationcraftProfile .. '# ' .. itemStr .. "\n" else -- Someone linked something that was not gear. simcPrintError = "Error: " .. v .. " is not gear." break end end end simulationcraftProfile = simulationcraftProfile .. '\n' simulationcraftProfile = simulationcraftProfile .. '### Additional Character Info\n' local catalystCurrenciesStr = Simulationcraft:GetCatalystCurrencies() simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '# catalyst_currencies=' .. catalystCurrenciesStr .. '\n' local upgradeCurrenciesStr = Simulationcraft:GetUpgradeCurrencies() simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '# upgrade_currencies=' .. upgradeCurrenciesStr .. '\n' local highWatermarksStr = Simulationcraft:GetSlotHighWatermarks() if highWatermarksStr then simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '# slot_high_watermarks=' .. highWatermarksStr .. '\n' end local upgradeAchievementsStr = Simulationcraft:GetItemUpgradeAchievements() simulationcraftProfile = simulationcraftProfile .. '#\n' simulationcraftProfile = simulationcraftProfile .. '# upgrade_achievements=' .. upgradeAchievementsStr .. '\n' -- sanity checks - if there's anything that makes the output completely invalid, punt! if specId==nil then simcPrintError = "Error: You need to pick a spec!" end simulationcraftProfile = simulationcraftProfile .. '\n' -- Simple checksum to provide a lightweight verification that the input hasn't been edited/modified local checksum = adler32(simulationcraftProfile) simulationcraftProfile = simulationcraftProfile .. '# Checksum: ' .. string.format('%x', checksum) return simulationcraftProfile, simcPrintError end -- This is the workhorse function that constructs the profile function Simulationcraft:PrintSimcProfile(debugOutput, noBags, showMerchant, links) LoadSpellsAsync(function() simulationcraftProfile, simcPrintError = Simulationcraft:GetSimcProfile(debugOutput, noBags, showMerchant, links) local f = Simulationcraft:GetMainFrame(simcPrintError or simulationcraftProfile) f:Show() end) end