chore: flatten Altoholic-Addon/ wrapper + add standard .gitignore/.gitattributes
Each DataStore_* / Altoholic_* addon now lives at the repo root, matching the Exiles fork-layout convention (one folder per addon, no wrapper dir).
This commit is contained in:
@@ -0,0 +1,871 @@
|
||||
--[[ *** DataStore_Crafts ***
|
||||
Written by : Thaoky, EU-Marécages de Zangar
|
||||
June 23rd, 2009
|
||||
--]]
|
||||
if not DataStore then return end
|
||||
|
||||
local addonName = "DataStore_Crafts"
|
||||
|
||||
_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0", "AceTimer-3.0")
|
||||
|
||||
local addon = _G[addonName]
|
||||
|
||||
local THIS_ACCOUNT = "Default"
|
||||
local commPrefix = "DS_Craft"
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale("DataStore_Crafts")
|
||||
local PT = LibStub("LibPeriodicTable-3.1")
|
||||
|
||||
local MSG_SEND_LOGIN = 1 -- Sends a login message, to request crafts to other players
|
||||
local MSG_LOGIN_REPLY = 2 -- ..reply
|
||||
local MSG_SEND_PROFESSION = 3 -- Sends a profession link, or profession id if no full link
|
||||
|
||||
local AddonDB_Defaults = {
|
||||
global = {
|
||||
Options = {
|
||||
BroadcastProfs = 1, -- Broadcast professions at login or not
|
||||
},
|
||||
Guilds = {
|
||||
['*'] = { -- ["Account.Realm.Name"]
|
||||
Members = {
|
||||
['*'] = { -- ["MemberName"]
|
||||
lastUpdate = nil,
|
||||
Version = nil,
|
||||
Professions = {}, -- 3 profession links : [1] & [2] for the 2 primary professions, [3] for cooking ([4] for archaeology ? wait & see)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
Characters = {
|
||||
['*'] = { -- ["Account.Realm.Name"]
|
||||
lastUpdate = nil,
|
||||
Professions = {
|
||||
['*'] = {
|
||||
FullLink = nil, -- Tradeskill link
|
||||
NumCrafts = 0, -- total number of crafts for this tradeskill
|
||||
Crafts = { ['*'] = nil },
|
||||
Cooldowns = { ['*'] = nil }, -- list of active cooldowns
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local SPELL_ID_ALCHEMY = 2259
|
||||
local SPELL_ID_BLACKSMITHING = 3100
|
||||
local SPELL_ID_ENCHANTING = 7411
|
||||
local SPELL_ID_ENGINEERING = 4036
|
||||
local SPELL_ID_INSCRIPTION = 45357
|
||||
local SPELL_ID_JEWELCRAFTING = 25229
|
||||
local SPELL_ID_LEATHERWORKING = 2108
|
||||
local SPELL_ID_TAILORING = 3908
|
||||
local SPELL_ID_SKINNING = 8613
|
||||
local SPELL_ID_MINING = 2575
|
||||
local SPELL_ID_HERBALISM = 13614
|
||||
local SPELL_ID_SMELTING = 2656
|
||||
local SPELL_ID_COOKING = 2550
|
||||
local SPELL_ID_FIRSTAID = 3273
|
||||
local SPELL_ID_FISHING = 7733
|
||||
|
||||
-- CoA custom professions on the Voljin/PTR realm. IDs sourced from
|
||||
-- coa-professionmenu (ProfessionMenu.lua:200-206) and verified against
|
||||
-- db.exil.es: spell 13977860 = "Woodcutting" (single base spell),
|
||||
-- spell 1005008 = Apprentice "Woodworking" (4 ranks 1005008..1005011,
|
||||
-- matching the vanilla Apprentice/Journeyman/Expert/Artisan pattern).
|
||||
-- Apprentice rank is used to mirror the vanilla constants above.
|
||||
local SPELL_ID_WOODCUTTING = 13977860
|
||||
local SPELL_ID_WOODWORKING = 1005008
|
||||
|
||||
local ProfessionSpellID = {
|
||||
-- GetSpellInfo with this value will return localized spell name
|
||||
["Alchemy"] = SPELL_ID_ALCHEMY,
|
||||
["Blacksmithing"] = SPELL_ID_BLACKSMITHING,
|
||||
["Enchanting"] = SPELL_ID_ENCHANTING,
|
||||
["Engineering"] = SPELL_ID_ENGINEERING,
|
||||
["Inscription"] = SPELL_ID_INSCRIPTION,
|
||||
["Jewelcrafting"] = SPELL_ID_JEWELCRAFTING,
|
||||
["Leatherworking"] = SPELL_ID_LEATHERWORKING,
|
||||
["Tailoring"] = SPELL_ID_TAILORING,
|
||||
["Skinning"] = SPELL_ID_SKINNING,
|
||||
["Mining"] = SPELL_ID_MINING,
|
||||
["Herbalism"] = SPELL_ID_HERBALISM,
|
||||
["Smelting"] = SPELL_ID_SMELTING,
|
||||
|
||||
["Cooking"] = SPELL_ID_COOKING,
|
||||
["First Aid"] = SPELL_ID_FIRSTAID,
|
||||
["Fishing"] = SPELL_ID_FISHING,
|
||||
|
||||
["Woodcutting"] = SPELL_ID_WOODCUTTING,
|
||||
["Woodworking"] = SPELL_ID_WOODWORKING,
|
||||
}
|
||||
|
||||
-- *** Utility functions ***
|
||||
local function GetOption(option)
|
||||
return addon.db.global.Options[option]
|
||||
end
|
||||
|
||||
local function GetProfessionID(profession)
|
||||
-- profession = localized profession name "Cooking" or "Cuisine", "Alchemy"...
|
||||
-- note: we're not using a reverse lookup table because of the localization issue.
|
||||
|
||||
if ProfessionSpellID[profession] then
|
||||
return ProfessionSpellID[profession]
|
||||
end
|
||||
|
||||
for _, id in pairs( ProfessionSpellID ) do
|
||||
if profession == GetSpellInfo(id) then -- profession found ?
|
||||
ProfessionSpellID[profession] = id -- cache the result to speed up future searches
|
||||
return id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function GetThisGuild()
|
||||
local guild = GetGuildInfo("player")
|
||||
if guild then
|
||||
local key = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), guild)
|
||||
return addon.db.global.Guilds[key]
|
||||
end
|
||||
end
|
||||
|
||||
local function GetVersion()
|
||||
local _, version = GetBuildInfo()
|
||||
return tonumber(version)
|
||||
end
|
||||
|
||||
local function SaveVersion(sender, version)
|
||||
local thisGuild = GetThisGuild()
|
||||
if thisGuild and sender and version then
|
||||
thisGuild.Members[sender].Version = version
|
||||
end
|
||||
end
|
||||
|
||||
local function GuildBroadcast(messageType, ...)
|
||||
local serializedData = addon:Serialize(messageType, ...)
|
||||
addon:SendCommMessage(commPrefix, serializedData, "GUILD")
|
||||
end
|
||||
|
||||
local function GuildWhisper(player, messageType, ...)
|
||||
if DataStore:IsGuildMemberOnline(player) then
|
||||
local serializedData = addon:Serialize(messageType, ...)
|
||||
addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player)
|
||||
end
|
||||
end
|
||||
|
||||
local professionQueue -- queue containing the professions to send to guildmates
|
||||
local professionTimer -- timer used to control the pace at which professions are place on comm channels.
|
||||
|
||||
local function SendProfession()
|
||||
if #professionQueue == 0 then -- nothing left in the queue ? cancel the timer & exit
|
||||
addon:CancelTimer(professionTimer)
|
||||
professionTimer = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- send the last profession found in the queue, then remove it
|
||||
local profession = professionQueue[#professionQueue] -- last element
|
||||
local alt = profession[1]
|
||||
local data = profession[2]
|
||||
local index = profession[3]
|
||||
local recipient = profession[4]
|
||||
-- DEFAULT_CHAT_FRAME:AddMessage(format("sending %s, %s to %s (size : %d)", alt, index, recipient or "guild", #professionQueue ))
|
||||
|
||||
if profession[4] then -- recipient found ?
|
||||
GuildWhisper(recipient, MSG_SEND_PROFESSION, alt, data, index)
|
||||
else
|
||||
GuildBroadcast(MSG_SEND_PROFESSION, alt, data, index)
|
||||
end
|
||||
|
||||
table.remove(professionQueue)
|
||||
end
|
||||
|
||||
local function QueueCharacterProfessions(character, recipient)
|
||||
local index = 1
|
||||
local _, _, alt = strsplit(".", character)
|
||||
|
||||
for profName, profession in pairs(addon.db.global.Characters[character].Professions) do
|
||||
local spellID = GetProfessionID(profName) or 0
|
||||
local data = profession.FullLink or spellID
|
||||
|
||||
if profession.isPrimary then
|
||||
table.insert(professionQueue, { alt, data, index, recipient })
|
||||
index = index + 1
|
||||
elseif profession.isSecondary then
|
||||
if profName == GetSpellInfo(SPELL_ID_COOKING) then
|
||||
table.insert(professionQueue, { alt, data, 3, recipient }) -- index = 3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SendAllProfessions(alts, recipient)
|
||||
if GetOption("BroadcastProfs") == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
professionQueue = professionQueue or {}
|
||||
|
||||
-- sends the professions of the current character + his alts
|
||||
local character = DataStore:GetCharacter() -- this character
|
||||
QueueCharacterProfessions(character, recipient)
|
||||
|
||||
if strlen(alts) > 0 then
|
||||
for _, name in pairs( { strsplit("|", alts) }) do -- then all his alts
|
||||
character = DataStore:GetCharacter(name)
|
||||
if character then
|
||||
QueueCharacterProfessions(character, recipient)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- reuse current timer if already available (may be the case if 2 players connect simultaneously)
|
||||
professionTimer = professionTimer or addon:ScheduleRepeatingTimer(SendProfession, 0.5) -- send 1 profession every half-second, max 15 seconds for 30 professions on a realm
|
||||
end
|
||||
|
||||
local function SaveProfession(sender, alt, data, index)
|
||||
local thisGuild = GetThisGuild()
|
||||
if thisGuild and sender then
|
||||
local version = thisGuild.Members[sender].Version
|
||||
local member = thisGuild.Members[alt]
|
||||
|
||||
member.Version = version
|
||||
member.Professions[index] = data
|
||||
member.lastUpdate = time()
|
||||
end
|
||||
addon:SendMessage("DATASTORE_GUILD_PROFESSION_RECEIVED", sender, alt, data, index)
|
||||
end
|
||||
|
||||
local function ClearExpiredProfessions()
|
||||
-- this function will clear all the guild profession links that were saved with a build number anterior to the current one (they're invalid after a patch anyway)
|
||||
|
||||
local thisGuild = GetThisGuild()
|
||||
if not thisGuild then return end
|
||||
|
||||
local version = GetVersion()
|
||||
|
||||
for name, member in pairs(thisGuild.Members) do
|
||||
if member.Version ~= version then
|
||||
thisGuild.Members[name] = nil -- clear this member's entry if version is outdated
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function LocalizeProfessionSpellIDs()
|
||||
-- this function adds localized entries in the ProfessionSpellID table
|
||||
|
||||
local localizedSpells = {} -- avoid infinite loop by storing in a temp table first
|
||||
local localizedName
|
||||
for englishName, spellID in pairs(ProfessionSpellID) do
|
||||
localizedName = GetSpellInfo(spellID)
|
||||
localizedSpells[localizedName] = spellID
|
||||
end
|
||||
|
||||
for name, id in pairs(localizedSpells) do
|
||||
ProfessionSpellID[name] = id
|
||||
end
|
||||
end
|
||||
|
||||
-- *** Scanning functions ***
|
||||
|
||||
local selectedTradeSkillIndex
|
||||
local subClasses, subClassID
|
||||
local invSlots, invSlotID
|
||||
local haveMats
|
||||
|
||||
local function GetSubClassID()
|
||||
-- The purpose of this function is to get the subClassID in a UI independant way
|
||||
-- ie: without relying on UIDropDownMenu_GetSelectedID(TradeSkillSubClassDropDown), which uses a hardcoded frame name.
|
||||
|
||||
if GetTradeSkillSubClassFilter(0) then -- if "All Subclasses" is selected, GetTradeSkillSubClassFilter() will return 1 for all indexes, including 0
|
||||
return 1 -- thus return 1 as selected id (as would be returned by UIDropDownMenu_GetSelectedID(TradeSkillSubClassDropDown))
|
||||
end
|
||||
|
||||
local filter
|
||||
for i = 1, #subClasses do
|
||||
filter = GetTradeSkillSubClassFilter(i)
|
||||
if filter then
|
||||
return i+1 -- ex: 3rd element of the subClasses array, but 4th in the dropdown due to "All Subclasses", so return i+1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function GetInvSlotID()
|
||||
-- The purpose of this function is to get the invSlotID in a UI independant way (same as GetSubClassID)
|
||||
-- ie: without relying on UIDropDownMenu_GetSelectedID(TradeSkillInvSlotDropDown), which uses a hardcoded frame name.
|
||||
|
||||
if GetTradeSkillInvSlotFilter(0) then -- if "All Slots" is selected, GetTradeSkillInvSlotFilter() will return 1 for all indexes, including 0
|
||||
return 1 -- thus return 1 as selected id (as would be returned by UIDropDownMenu_GetSelectedID(TradeSkillInvSlotDropDown))
|
||||
end
|
||||
|
||||
local filter
|
||||
for i = 1, #invSlots do
|
||||
filter = GetTradeSkillInvSlotFilter(i)
|
||||
if filter then
|
||||
return i+1 -- ex: 3rd element of the invSlots array, but 4th in the dropdown due to "All Slots", so return i+1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SaveActiveFilters()
|
||||
selectedTradeSkillIndex = GetTradeSkillSelectionIndex()
|
||||
|
||||
subClasses = { GetTradeSkillSubClasses() }
|
||||
invSlots = { GetTradeSkillInvSlots() }
|
||||
subClassID = GetSubClassID()
|
||||
invSlotID = GetInvSlotID()
|
||||
|
||||
-- Subclasses
|
||||
SetTradeSkillSubClassFilter(0, 1, 1) -- this checks "All subclasses"
|
||||
if TradeSkillSubClassDropDown then
|
||||
UIDropDownMenu_SetSelectedID(TradeSkillSubClassDropDown, 1)
|
||||
end
|
||||
|
||||
-- Inventory slots
|
||||
SetTradeSkillInvSlotFilter(0, 1, 1) -- this checks "All slots"
|
||||
if TradeSkillInvSlotDropDown then
|
||||
UIDropDownMenu_SetSelectedID(TradeSkillInvSlotDropDown, 1)
|
||||
end
|
||||
|
||||
-- Have Materials
|
||||
if TradeSkillFrameAvailableFilterCheckButton then
|
||||
haveMats = TradeSkillFrameAvailableFilterCheckButton:GetChecked() -- nil or true
|
||||
TradeSkillFrameAvailableFilterCheckButton:SetChecked(false)
|
||||
end
|
||||
TradeSkillOnlyShowMakeable(false)
|
||||
end
|
||||
|
||||
local function RestoreActiveFilters()
|
||||
-- Subclasses
|
||||
SetTradeSkillSubClassFilter(subClassID-1, 1, 1) -- this checks the previously checked value
|
||||
|
||||
local frame = TradeSkillSubClassDropDown
|
||||
if frame then -- other addons might nil this frame (delayed load, etc..), so secure DDM calls
|
||||
local text = (subClassID == 1) and ALL_SUBCLASSES or subClasses[subClassID-1]
|
||||
UIDropDownMenu_SetSelectedID(frame, subClassID)
|
||||
UIDropDownMenu_SetText(frame, text);
|
||||
end
|
||||
|
||||
subClassID = nil
|
||||
wipe(subClasses)
|
||||
subClasses = nil
|
||||
|
||||
-- Inventory slots
|
||||
invSlotID = invSlotID or 1
|
||||
SetTradeSkillInvSlotFilter(invSlotID-1, 1, 1) -- this checks the previously checked value
|
||||
|
||||
frame = TradeSkillInvSlotDropDown
|
||||
if frame then
|
||||
local text = (invSlotID == 1) and ALL_INVENTORY_SLOTS or invSlots[invSlotID-1]
|
||||
UIDropDownMenu_SetSelectedID(frame, invSlotID)
|
||||
UIDropDownMenu_SetText(frame, text);
|
||||
end
|
||||
|
||||
invSlotID = nil
|
||||
wipe(invSlots)
|
||||
invSlots = nil
|
||||
|
||||
-- Have Materials
|
||||
if TradeSkillFrameAvailableFilterCheckButton then
|
||||
TradeSkillFrameAvailableFilterCheckButton:SetChecked(haveMats or false)
|
||||
end
|
||||
TradeSkillOnlyShowMakeable(haveMats or false)
|
||||
haveMats = nil
|
||||
|
||||
SelectTradeSkill(selectedTradeSkillIndex)
|
||||
selectedTradeSkillIndex = nil
|
||||
end
|
||||
|
||||
local headersState = {}
|
||||
|
||||
local function SaveHeaders()
|
||||
local headerCount = 0 -- use a counter to avoid being bound to header names, which might not be unique.
|
||||
|
||||
for i = GetNumTradeSkills(), 1, -1 do -- 1st pass, expand all categories
|
||||
local _, skillType, _, isExpanded = GetTradeSkillInfo(i)
|
||||
if (skillType == "header") then
|
||||
headerCount = headerCount + 1
|
||||
if not isExpanded then
|
||||
ExpandTradeSkillSubClass(i)
|
||||
headersState[headerCount] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function RestoreHeaders()
|
||||
local headerCount = 0
|
||||
for i = GetNumTradeSkills(), 1, -1 do
|
||||
local _, skillType = GetTradeSkillInfo(i)
|
||||
if (skillType == "header") then
|
||||
headerCount = headerCount + 1
|
||||
if headersState[headerCount] then
|
||||
CollapseTradeSkillSubClass(i)
|
||||
end
|
||||
end
|
||||
end
|
||||
wipe(headersState)
|
||||
end
|
||||
|
||||
local function ScanCooldowns()
|
||||
local tradeskillName = GetTradeSkillLine()
|
||||
local char = addon.ThisCharacter
|
||||
local profession = char.Professions[tradeskillName]
|
||||
|
||||
wipe(profession.Cooldowns)
|
||||
for i = 1, GetNumTradeSkills() do
|
||||
local skillName, skillType = GetTradeSkillInfo(i)
|
||||
|
||||
if skillType ~= "header" then
|
||||
local cooldown = GetTradeSkillCooldown(i)
|
||||
if cooldown then
|
||||
table.insert(profession.Cooldowns, skillName .. "|" .. cooldown .. "|" .. time())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ScanProfessionLinks()
|
||||
local char = addon.ThisCharacter
|
||||
|
||||
if not char then return end
|
||||
|
||||
local old_professions = {}
|
||||
-- start listing previously known professions
|
||||
for skillName, v in pairs(char.Professions) do
|
||||
old_professions[skillName] = 1
|
||||
end
|
||||
|
||||
for i = GetNumSkillLines(), 1, -1 do -- 1st pass, expand all categories
|
||||
local _, isHeader = GetSkillLineInfo(i)
|
||||
if isHeader then
|
||||
ExpandSkillHeader(i)
|
||||
end
|
||||
end
|
||||
|
||||
local category
|
||||
for i = 1, GetNumSkillLines() do
|
||||
local skillName, isHeader, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(i)
|
||||
|
||||
if isHeader then
|
||||
category = skillName
|
||||
else
|
||||
if category and skillName then
|
||||
local field
|
||||
|
||||
if category == L["Professions"] then
|
||||
field = "isPrimary"
|
||||
end
|
||||
|
||||
if category == L["Secondary Skills"] then
|
||||
field = "isSecondary"
|
||||
end
|
||||
|
||||
if field then
|
||||
char.Professions[skillName][field] = true
|
||||
|
||||
-- remove a confimed Profession from the "old" list
|
||||
old_professions[skillName] = nil
|
||||
|
||||
-- should be nil anyway for fishing, mining, etc..
|
||||
local newLink = select(2, GetSpellLink(skillName))
|
||||
if newLink then -- sometimes a nil value may be returned, so keep the old one if nil
|
||||
char.Professions[skillName].FullLink = newLink
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- clear old professions which are no longer known
|
||||
for oldProfession,_ in pairs(old_professions) do
|
||||
wipe( char.Professions[oldProfession] ) -- clean the table first
|
||||
char.Professions[oldProfession] = nil -- remove the entry
|
||||
end
|
||||
end
|
||||
|
||||
local SkillTypeToColor = {
|
||||
["header"] = 0,
|
||||
["optimal"] = 1, -- orange
|
||||
["medium"] = 2, -- yellow
|
||||
["easy"] = 3, -- green
|
||||
["trivial"] = 4, -- grey
|
||||
}
|
||||
|
||||
local function ScanRecipes()
|
||||
local tradeskillName = GetTradeSkillLine()
|
||||
if not tradeskillName or tradeskillName == "UNKNOWN" then return end -- may happen after a patch, or under extreme lag, so do not save anything to the db !
|
||||
|
||||
local char = addon.ThisCharacter
|
||||
local profession = char.Professions[tradeskillName]
|
||||
-- local crafts = profession.Crafts
|
||||
-- wipe(crafts)
|
||||
local crafts = {}
|
||||
|
||||
|
||||
local NumCrafts = 0
|
||||
local NumHeaders = 0
|
||||
local color, craftInfo, link
|
||||
|
||||
for i = 1, GetNumTradeSkills() do
|
||||
local skillName, skillType = GetTradeSkillInfo(i)
|
||||
color = SkillTypeToColor[skillType]
|
||||
|
||||
if skillType == "header" then
|
||||
craftInfo = skillName or ""
|
||||
NumHeaders = NumHeaders + 1
|
||||
else
|
||||
link = GetTradeSkillRecipeLink(i)
|
||||
craftInfo = tonumber(link:match("enchant:(%d+)")) -- this actually extracts the spellID
|
||||
NumCrafts = NumCrafts + 1
|
||||
end
|
||||
crafts[i] = color .. "|" .. craftInfo
|
||||
end
|
||||
|
||||
if NumHeaders > 0 then -- only overwrite stored info if he have seen some headers (indicator that the whole profession is in game cache and the interface is shown in its entirety)
|
||||
profession.FullLink = select(2, GetSpellLink(tradeskillName))
|
||||
profession.NumCrafts = NumCrafts
|
||||
wipe(profession.Crafts) -- wipe old table (for memory saving)
|
||||
profession.Crafts = crafts -- assign the valid new table
|
||||
else
|
||||
wipe(crafts) -- wipe unused table
|
||||
print(format("|cffFFFF20%s|r:",addonName), "Profession not yet fully loaded.")
|
||||
end
|
||||
end
|
||||
|
||||
local function ScanTradeSkills()
|
||||
SaveActiveFilters()
|
||||
SaveHeaders()
|
||||
ScanRecipes()
|
||||
ScanCooldowns()
|
||||
RestoreHeaders()
|
||||
RestoreActiveFilters()
|
||||
|
||||
addon.ThisCharacter.lastUpdate = time()
|
||||
end
|
||||
|
||||
-- *** Event Handlers ***
|
||||
local function OnPlayerAlive()
|
||||
-- print("DataStore_Crafts.lua") -- DEBUG 2025 07 21
|
||||
if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard
|
||||
|
||||
ScanProfessionLinks()
|
||||
end
|
||||
|
||||
local function OnTradeSkillClose()
|
||||
addon:UnregisterEvent("TRADE_SKILL_CLOSE")
|
||||
addon:UnregisterEvent("TRADE_SKILL_UPDATE")
|
||||
addon.isOpen = nil
|
||||
end
|
||||
|
||||
local function OnTradeSkillShow()
|
||||
if IsTradeSkillLinked() then return end
|
||||
|
||||
addon.isOpen = true
|
||||
addon:RegisterEvent("TRADE_SKILL_CLOSE", OnTradeSkillClose)
|
||||
ScanTradeSkills()
|
||||
end
|
||||
|
||||
local function OnTradeSkillUpdate()
|
||||
-- The hook in DoTradeSkill will register this event so that we only update skills once.
|
||||
-- unregister it before calling the update, or the event will be called recursively (due to expand/collapse)
|
||||
addon:UnregisterEvent("TRADE_SKILL_UPDATE")
|
||||
ScanCooldowns() -- only cooldowns need to be refreshed
|
||||
end
|
||||
|
||||
-- this turns
|
||||
-- "Your skill in %s has increased to %d."
|
||||
-- into
|
||||
-- "Your skill in (.+) has increased to (%d+)."
|
||||
local arg1pattern, arg2pattern
|
||||
if GetLocale() == "deDE" then
|
||||
-- ERR_SKILL_UP_SI = "Eure Fertigkeit '%1$s' hat sich auf %2$d erhöht.";
|
||||
arg1pattern = "'%%1%$s'"
|
||||
arg2pattern = "%%2%$d"
|
||||
else
|
||||
arg1pattern = "%%s"
|
||||
arg2pattern = "%%d"
|
||||
end
|
||||
|
||||
local skillUpMsg = gsub(ERR_SKILL_UP_SI, arg1pattern, "(.+)")
|
||||
skillUpMsg = gsub(skillUpMsg, arg2pattern, "(%%d+)")
|
||||
|
||||
local function OnChatMsgSkill(self, msg)
|
||||
if msg and addon.isOpen then -- point gained while ts window is open ? rescan
|
||||
local skill = msg:match(skillUpMsg)
|
||||
if skill and skill == GetTradeSkillLine() then -- if we gained a skill point in the currently opened profession pane, rescan
|
||||
ScanTradeSkills()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- ** Mixins **
|
||||
local function _GetProfession(character, name)
|
||||
return character.Professions[name]
|
||||
end
|
||||
|
||||
local function _GetProfessions(character)
|
||||
return character.Professions
|
||||
end
|
||||
|
||||
local function _GetProfessionInfo(profession)
|
||||
-- accepts either a pointer (type == table)to the profession table, as returned by addon:GetProfession()
|
||||
-- or a link (type == string)
|
||||
local link
|
||||
if type(profession) == "table" then
|
||||
link = profession.FullLink
|
||||
elseif type(profession) == "string" then
|
||||
link = profession
|
||||
end
|
||||
|
||||
if link then
|
||||
local spellID, currentLevel, maxLevel = link:match("trade:(%d+):(%d+):(%d+):")
|
||||
return tonumber(currentLevel), tonumber(maxLevel), tonumber(spellID)
|
||||
end
|
||||
end
|
||||
|
||||
local function _GetNumCraftLines(profession)
|
||||
return #profession.Crafts
|
||||
end
|
||||
|
||||
local function _GetCraftLineInfo(profession, index)
|
||||
local craft = profession.Crafts[index]
|
||||
local color, info = strsplit("|", craft)
|
||||
|
||||
color = tonumber(color)
|
||||
if color ~= 0 then -- not a header ?
|
||||
info = tonumber(info) -- it's a spellID, return a number
|
||||
end
|
||||
return (color == 0), color, info -- 1st return value = isHeader
|
||||
end
|
||||
|
||||
local function _GetCraftCooldownInfo(profession, index)
|
||||
local cooldown = profession.Cooldowns[index]
|
||||
local name, reset, lastCheck = strsplit("|", cooldown)
|
||||
|
||||
reset = tonumber(reset)
|
||||
lastCheck = tonumber(lastCheck)
|
||||
local expiresIn = reset - (time() - lastCheck)
|
||||
|
||||
return name, expiresIn, reset, lastCheck
|
||||
end
|
||||
|
||||
local function _GetNumActiveCooldowns(profession)
|
||||
assert(type(profession) == "table") -- this is the pointer to a profession table, obtained through addon:GetProfession()
|
||||
return #profession.Cooldowns
|
||||
end
|
||||
|
||||
local function _ClearExpiredCooldowns(profession)
|
||||
assert(type(profession) == "table") -- this is the pointer to a profession table, obtained through addon:GetProfession()
|
||||
|
||||
for i = #profession.Cooldowns, 1, -1 do -- from last to first, to avoid messing up indexes when removing entries
|
||||
local _, expiresIn = _GetCraftCooldownInfo(profession, i)
|
||||
if expiresIn <= 0 then -- already expired ? remove it
|
||||
table.remove(profession.Cooldowns, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _GetCraftInfo(spellID)
|
||||
-- get the id of the item that can be crafted by this spellID
|
||||
local itemID = PT:ItemInSet("-"..spellID, "Tradeskill.RecipeLinks")
|
||||
local reagents
|
||||
|
||||
if itemID then
|
||||
itemID = tonumber(itemID)
|
||||
|
||||
-- ex: itemID 10046 is made with reagents : "2996x2;2318x1;2320x1"
|
||||
reagents = PT:ItemInSet(itemID, "TradeskillResultMats.Forward")
|
||||
|
||||
if itemID == -spellID then -- enchants that do not yield an item will return this, ex: enchant 7420 will return itemID -7420
|
||||
itemID = nil
|
||||
end
|
||||
end
|
||||
|
||||
return itemID, reagents
|
||||
end
|
||||
|
||||
local function _GetCraftLevels(spellID)
|
||||
-- get the id of the item that can be crafted by this spellID
|
||||
local itemID = PT:ItemInSet("-"..spellID, "Tradeskill.RecipeLinks")
|
||||
|
||||
if itemID then
|
||||
itemID = tonumber(itemID)
|
||||
|
||||
-- ex: itemID 10046 : levels = "20/50/67/85", the item turns yellow at 50, green at 67, grey at 85
|
||||
local levels = PT:ItemInSet(itemID, "TradeskillLevels")
|
||||
|
||||
if levels then
|
||||
local orange, yellow, green, grey = strsplit("/", levels)
|
||||
return tonumber(orange), tonumber(yellow), tonumber(green), tonumber(grey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _GetNumRecipesByColor(profession)
|
||||
-- counts the number of orange, yellow, green and grey recipes.
|
||||
local counts = { 0, 0, 0, 0 }
|
||||
|
||||
for i = 1, _GetNumCraftLines(profession) do
|
||||
local isHeader, color = _GetCraftLineInfo(profession, i)
|
||||
|
||||
if not isHeader then
|
||||
counts[color] = counts[color] + 1
|
||||
end
|
||||
end
|
||||
return counts[1], counts[2], counts[3], counts[4] -- orange, yellow, green, grey
|
||||
end
|
||||
|
||||
local function _IsCraftKnown(profession, spellID)
|
||||
-- returns true if a given spell ID is known in the profession passed as first argument
|
||||
for i = 1, _GetNumCraftLines(profession) do
|
||||
local isHeader, _, info = _GetCraftLineInfo(profession, i)
|
||||
|
||||
if not isHeader then
|
||||
if info == spellID then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _GetGuildCrafters(guild)
|
||||
return guild.Members
|
||||
end
|
||||
|
||||
local function _GetGuildMemberProfession(guild, member, index)
|
||||
local m = guild.Members[member]
|
||||
local profession = m.Professions[index]
|
||||
|
||||
if type(profession) == "string" then
|
||||
local spellID = profession:match("trade:(%d+):")
|
||||
return tonumber(spellID), profession, m.lastUpdate -- return the profession spell ID + full link
|
||||
elseif type(profession) == "number" then
|
||||
return profession, nil, m.lastUpdate -- return the profession spell ID
|
||||
end
|
||||
end
|
||||
|
||||
local function _GetProfessionSpellID(name)
|
||||
-- name can be either the english name or the localized name
|
||||
return ProfessionSpellID[name]
|
||||
end
|
||||
|
||||
local PublicMethods = {
|
||||
GetProfession = _GetProfession,
|
||||
GetProfessions = _GetProfessions,
|
||||
GetProfessionInfo = _GetProfessionInfo,
|
||||
GetNumCraftLines = _GetNumCraftLines,
|
||||
GetCraftLineInfo = _GetCraftLineInfo,
|
||||
GetCraftCooldownInfo = _GetCraftCooldownInfo,
|
||||
GetNumActiveCooldowns = _GetNumActiveCooldowns,
|
||||
ClearExpiredCooldowns = _ClearExpiredCooldowns,
|
||||
GetCraftInfo = _GetCraftInfo,
|
||||
GetCraftLevels = _GetCraftLevels,
|
||||
GetNumRecipesByColor = _GetNumRecipesByColor,
|
||||
IsCraftKnown = _IsCraftKnown,
|
||||
GetGuildCrafters = _GetGuildCrafters,
|
||||
GetGuildMemberProfession = _GetGuildMemberProfession,
|
||||
GetProfessionSpellID = _GetProfessionSpellID,
|
||||
}
|
||||
|
||||
-- *** Guild Comm ***
|
||||
local function OnGuildAltsReceived(self, sender, alts)
|
||||
if sender == UnitName("player") then -- if I receive my own list of alts in the same guild, same realm, same account..
|
||||
GuildBroadcast(MSG_SEND_LOGIN, GetVersion())
|
||||
addon:ScheduleTimer(SendAllProfessions, 5, alts) -- broadcast my crafts to the guild 5 seconds later, to decrease the load at startup
|
||||
end
|
||||
end
|
||||
|
||||
local GuildCommCallbacks = {
|
||||
[MSG_SEND_LOGIN] = function(sender, version)
|
||||
local player = UnitName("player")
|
||||
if sender ~= player then -- don't send back to self
|
||||
GuildWhisper(sender, MSG_LOGIN_REPLY, GetVersion())
|
||||
local alts = DataStore:GetGuildMemberAlts(player) -- get my own alts
|
||||
if alts then
|
||||
SendAllProfessions(alts, sender) -- when another player sends me his login, reply with my own crafts
|
||||
end
|
||||
end
|
||||
SaveVersion(sender, version)
|
||||
end,
|
||||
[MSG_LOGIN_REPLY] = function(sender, version)
|
||||
SaveVersion(sender, version)
|
||||
end,
|
||||
[MSG_SEND_PROFESSION] = function(sender, alt, data, index)
|
||||
SaveProfession(sender, alt, data, index)
|
||||
end,
|
||||
}
|
||||
|
||||
function addon:OnInitialize()
|
||||
addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)
|
||||
|
||||
DataStore:RegisterModule(addonName, addon, PublicMethods)
|
||||
DataStore:SetGuildCommCallbacks(commPrefix, GuildCommCallbacks)
|
||||
DataStore:SetCharacterBasedMethod("GetProfession")
|
||||
DataStore:SetCharacterBasedMethod("GetProfessions")
|
||||
|
||||
DataStore:SetGuildBasedMethod("GetGuildCrafters")
|
||||
DataStore:SetGuildBasedMethod("GetGuildMemberProfession")
|
||||
|
||||
addon:RegisterMessage("DATASTORE_GUILD_ALTS_RECEIVED", OnGuildAltsReceived)
|
||||
addon:RegisterComm(commPrefix, DataStore:GetGuildCommHandler())
|
||||
end
|
||||
|
||||
function addon:OnEnable()
|
||||
addon:RegisterEvent("PLAYER_ALIVE", OnPlayerAlive)
|
||||
addon:RegisterEvent("TRADE_SKILL_SHOW", OnTradeSkillShow)
|
||||
addon:RegisterEvent("CHAT_MSG_SKILL", OnChatMsgSkill)
|
||||
|
||||
addon:SetupOptions()
|
||||
ClearExpiredProfessions() -- automatically cleanup guild profession links that are from an older version
|
||||
LocalizeProfessionSpellIDs()
|
||||
end
|
||||
|
||||
function addon:OnDisable()
|
||||
addon:UnregisterEvent("PLAYER_ALIVE")
|
||||
addon:UnregisterEvent("TRADE_SKILL_SHOW")
|
||||
addon:UnregisterEvent("CHAT_MSG_SKILL")
|
||||
end
|
||||
|
||||
function addon:GetSource(searchedID)
|
||||
local PT = LibStub("LibPeriodicTable-3.1")
|
||||
|
||||
-- Returns "Profession, level" ex: "Alchemy", "180"
|
||||
local level, data = PT:ItemInSet(searchedID, "Tradeskill.Crafted")
|
||||
if level and data then
|
||||
local _, _, profession = strsplit(".", data) -- ex: "Tradeskill.Crafted.Inscription"
|
||||
local localizedProfession
|
||||
if ProfessionSpellID[profession] then
|
||||
localizedProfession = GetSpellInfo(ProfessionSpellID[profession])
|
||||
end
|
||||
|
||||
return localizedProfession or profession, level
|
||||
end
|
||||
end
|
||||
|
||||
function addon:IsTradeSkillWindowOpen()
|
||||
-- note : maybe there's a function in the WoW API to test this, but I did not find it :(
|
||||
return addon.isOpen
|
||||
end
|
||||
|
||||
-- *** Hooks ***
|
||||
-- todo : change the hooks, do them the Ace way
|
||||
local Orig_DoTradeSkill = DoTradeSkill
|
||||
|
||||
function DoTradeSkill(index, repeatCount, ...) -- this hook is necessary to get cooldown information after a craft
|
||||
Orig_DoTradeSkill(index, repeatCount, ...)
|
||||
addon:RegisterEvent("TRADE_SKILL_UPDATE", OnTradeSkillUpdate)
|
||||
end
|
||||
|
||||
local Orig_AbandonSkill = AbandonSkill
|
||||
|
||||
function AbandonSkill(index, ...)
|
||||
local skillName = GetSkillLineInfo(index) -- get the name of the profession that is being abandonned
|
||||
|
||||
Orig_AbandonSkill(index, ...)
|
||||
|
||||
-- clear the list of recipes
|
||||
local char = addon.ThisCharacter
|
||||
wipe(char.Professions[skillName])
|
||||
char.Professions[skillName] = nil
|
||||
end
|
||||
Reference in New Issue
Block a user