Files
2026-07-15 23:00:59 -07:00

533 lines
12 KiB
Lua

GuildRecipesOcto = GuildRecipesOcto or {}
---@class GuildRecipesOcto
local M = GuildRecipesOcto
local QUALITY_COLORS = {
[ "ff9d9d9d" ] = 0, -- poor (gray)
[ "ffffffff" ] = 1, -- common (white)
[ "ff1eff00" ] = 2, -- uncommon (green)
[ "ff0070dd" ] = 3, -- rare (blue)
[ "ffa335ee" ] = 4, -- epic (purple)
[ "ffff8000" ] = 5, -- legendary (orange)
}
M.TRADE_SKILL_LOCALIZATION = {
Alchemy = {
enUS = "Alchemy",
deDE = "Alchimie",
frFR = "Alchimie",
esES = "Alquimia",
koKR = "연금술",
zhCN = "炼金术",
zhTW = "鍊金術",
},
Blacksmithing = {
enUS = "Blacksmithing",
deDE = "Schmiedekunst",
frFR = "Forge",
esES = "Herrería",
koKR = "대장기술",
zhCN = "锻造",
zhTW = "鍛造",
},
Engineering = {
enUS = "Engineering",
deDE = "Ingenieurskunst",
frFR = "Ingénierie",
esES = "Ingeniería",
koKR = "기계공학",
zhCN = "工程学",
zhTW = "工程學",
},
Leatherworking = {
enUS = "Leatherworking",
deDE = "Lederverarbeitung",
frFR = "Travail du cuir",
esES = "Peletería",
koKR = "가죽세공",
zhCN = "制皮",
zhTW = "製皮",
},
Tailoring = {
enUS = "Tailoring",
deDE = "Schneiderei",
frFR = "Couture",
esES = "Sastrería",
koKR = "재봉술",
zhCN = "裁缝",
zhTW = "裁縫",
},
Enchanting = {
enUS = "Enchanting",
deDE = "Verzauberkunst",
frFR = "Enchantement",
esES = "Encantamiento",
koKR = "마법부여",
zhCN = "附魔",
zhTW = "附魔",
},
Jewelcrafting = {
enUS = "Jewelcrafting",
},
}
function M.build_reverse_trade_map( locale )
local map = {}
for internal, locs in pairs( M.TRADE_SKILL_LOCALIZATION ) do
local localized = locs[ locale ]
if localized then
map[ localized ] = internal
end
end
return map
end
---@param item_link string
---@return integer? id
---@return string? name
---@return integer? quality
function M.parse_item_link( item_link )
if type( item_link ) ~= "string" then return nil, nil, nil end
local item_id = tonumber( string.match( item_link, ":(%d+)" ) )
local name = string.match( item_link, "|h%[(.-)%]|h" )
local quality
local color = string.match( item_link, "|c(%x%x%x%x%x%x%x%x)" )
if color then
quality = QUALITY_COLORS[ string.lower( color ) ]
else
local _, _, q = GetItemInfo( item_id )
quality = q or 1
end
return item_id, name, quality
end
---@param item Item
---@return string
function M.get_item_name_colorized( item )
local color = ITEM_QUALITY_COLORS[ item.quality or 1 ]
local hex = string.format( "%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 )
return string.format( "|cFF%s%s|r", hex, item.name )
end
---@param id integer
---@param name string
---@param quality integer
---@return string
function M.make_item_link( id, name, quality )
local color = ITEM_QUALITY_COLORS[ quality or 1 ]
local hex = string.format( "%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 )
return string.format( "|cFF%s|Hitem:%d:0:0:0|h[%s]|h|r", hex, id, name )
end
function M.make_enchant_link( id, name )
return string.format( "|cFF%s|Henchant:%d|h[%s]|h|r", "71d5ff", id, name )
end
function M.colorize_player_by_class( name, class )
if not class then return name end
local color = RAID_CLASS_COLORS[ string.upper( class ) ]
if not color.colorStr then
color.colorStr = string.format( "ff%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 )
end
return "|c" .. color.colorStr .. name .. "|r"
end
function M.clean_string( input )
input = string.gsub( input, "^%s+", "" )
input = string.gsub( input, "%s+$", "" )
input = string.gsub( input, "|c%x%x%x%x%x%x%x%x", "" )
input = string.gsub( input, "|r", "" )
return input
end
---@param player_name string
---@return boolean
---@nodiscard
function M.guild_member_online( player_name )
for i = 1, GetNumGuildMembers() do
local name, _, _, _, _, _, _, _, online = GetGuildRosterInfo( i )
if player_name == name and online == 1 then
return true
end
end
return false
end
---@return table<string, boolean> -- name -> online, built from a single pass over the guild roster
---@nodiscard
-- Use this instead of calling guild_member_online() in a loop (e.g. once per
-- synced player, or once per recipe row) -- each guild_member_online() call
-- re-scans the entire roster, so doing that repeatedly is O(n*m) and can
-- visibly stutter the game when triggered by a frequent event.
function M.build_online_roster()
local online = {}
for i = 1, GetNumGuildMembers() do
local name, _, _, _, _, _, _, _, is_online = GetGuildRosterInfo( i )
if name and is_online == 1 then
online[ name ] = true
end
end
return online
end
function M.get_server_timestamp()
local server_hour, server_min = GetGameTime()
local local_time = date( "*t" )
local t = {
year = local_time.year,
month = local_time.month,
day = local_time.day,
hour = server_hour,
min = server_min,
sec = 0,
}
local hour_diff = server_hour - local_time.hour
if hour_diff <= -20 then
t.day = t.day + 1
elseif hour_diff >= 20 then
t.day = t.day - 1
end
return time( t )
end
---@param message string
---@param short boolean?
function M.info( message, short )
local tag = string.format( "|c%s%s|r", M.tagcolor, short and "GRO" or "GuildRecipesOcto" )
DEFAULT_CHAT_FRAME:AddMessage( string.format( "%s: %s", tag, message ) )
end
---@param message string
function M.error( message )
local tag = string.format( "|c%s%s|r|cffff0000%s|r", M.tagcolor, "GR", "ERROR" )
DEFAULT_CHAT_FRAME:AddMessage( string.format( "%s: %s", tag, message ) )
end
function M.debug( message )
if M.debug_enabled then
M.info( message, true )
end
end
---@param t string[]
---@return string
---@nodiscard
function M.table_to_comma_separated( t )
local s = ""
for _, v in pairs( t ) do
if s ~= "" then s = s .. "," end
s = s .. tostring( v )
end
return s
end
---@param s string
---@return string[]
---@nodiscard
function M.comma_separated_to_table( s )
local t = {}
for v in string.gmatch( s, "([^,]+)" ) do
table.insert( t, v )
end
return t
end
---@param value string|number
---@param t table
---@param extract_field string?
---@nodiscard
function M.find( value, t, extract_field )
if type( t ) ~= "table" or M.count( t ) == 0 then return nil end
for i, v in pairs( t ) do
local val = extract_field and v[ extract_field ] or v
if val == value then return v, i end
end
return nil
end
---@param t table
---@param field string?
---@return number
function M.count( t, field )
local count = 0
for _, e in pairs( t ) do
if field and e[ field ] and e[ field ] > 0 or not field then
count = count + 1
end
end
return count
end
---@param recipes table
---@param player string
---@return number
function M.count_recipes( recipes, player )
local _, player_id = M.find( player, M.db.players )
local count = 0
if player_id and recipes then
for _, recipe in pairs( recipes ) do
if M.find( tostring( player_id ), M.comma_separated_to_table( recipe.p ) ) then
count = count + 1
end
end
end
return count
end
---@param id integer
---@param ufunc function
---@param data table?
---@return nil
function M.get_item_info( id, ufunc, data )
local function callback( item_id, cbfunc, _data )
local info = { GetItemInfo( item_id ) }
cbfunc( {
id = item_id,
name = info[ 1 ],
link = info[ 2 ],
quality = info[ 3 ],
texture = info[ 9 ],
}, _data )
end
if GetItemInfo( id ) then
callback( id, ufunc, data )
return
end
if not M.item_info_frame then
M.item_info_frame = CreateFrame( "Frame" )
end
if not M.get_item_info_items then
M.get_item_info_items = {}
end
table.insert( M.get_item_info_items, {
id = id,
ufunc = ufunc,
data = data
} )
if not M.item_info_frame:GetScript( "OnUpdate" ) then
local elapsed = 0
local poll_interval = 0.1
M.item_info_frame:SetScript( "OnUpdate", function()
local item_data = M.get_item_info_items[ 1 ]
if not item_data then
M.item_info_frame:SetScript( "OnUpdate", nil )
return
end
elapsed = elapsed + arg1
if elapsed < poll_interval then return end
elapsed = 0
M.tooltip:SetOwner( WorldFrame, "ANCHOR_NONE" )
M.tooltip:SetHyperlink( "item:" .. item_data.id )
if GetItemInfo( item_data.id ) then
callback( item_data.id, item_data.ufunc, item_data.data )
table.remove( M.get_item_info_items, 1 )
end
end )
end
end
---@param link string -- item link or spell link
---@param callback function -- function(lines: string[]|nil) end
---@param max_attempts number|nil -- retry limit (default 5)
---@param delay number|nil
function M.scan_tooltip( link, callback, max_attempts, delay )
---@class TooltipScan
---@field link string
---@field callback fun(lines: string[]|nil)
---@field max_attempts number
---@field delay number
---@field attempt number
---@field elapsed number
---@field initialized boolean
---@type TooltipScan | nil
local current_scan = nil
local scan_queue = {}
local start_next_scan
local function process_scan()
if not current_scan then
M.tooltip_scan_frame:SetScript( "OnUpdate", nil )
return
end
current_scan.elapsed = current_scan.elapsed + arg1
if current_scan.elapsed < current_scan.delay then return end
current_scan.elapsed = 0
current_scan.attempt = current_scan.attempt + 1
M.tooltip:SetOwner( WorldFrame, "ANCHOR_NONE" )
M.tooltip:ClearLines()
M.tooltip:SetHyperlink( current_scan.link )
local num_lines = M.tooltip:NumLines()
if num_lines and num_lines > 0 then
local lines = {}
for i = 1, num_lines do
local line = getglobal( "GuildRecipesOctoTooltipTextLeft" .. i )
if line then
table.insert( lines, line:GetText() or "" )
end
end
local scan_callback = current_scan.callback
current_scan = nil
M.tooltip_scan_frame:SetScript( "OnUpdate", nil )
scan_callback( lines )
start_next_scan()
return
end
if current_scan.attempt >= current_scan.max_attempts then
M.tooltip_scan_frame:SetScript( "OnUpdate", nil )
M.debug( "Failed to scan tooltip for " .. current_scan.link )
if current_scan.callback then
current_scan.callback( nil )
end
current_scan = nil
start_next_scan()
end
end
function start_next_scan()
current_scan = table.remove( scan_queue, 1 )
if not current_scan then
M.tooltip_scan_frame:SetScript( "OnUpdate", nil )
return
end
current_scan.attempt = 0
current_scan.elapsed = 0
M.tooltip_scan_frame:SetScript( "OnUpdate", process_scan )
end
if not M.tooltip_scan_frame then
M.tooltip_scan_frame = CreateFrame( "Frame" )
end
table.insert( scan_queue, {
link = link,
callback = callback,
max_attempts = max_attempts or 5,
delay = delay or 0.1,
} )
if not current_scan then
start_next_scan()
end
end
---@param tradeskill string
---@return integer
---@nodiscard
function M.tradeskill_hash( tradeskill )
local keys = {}
local s = ""
local ts = M.db.tradeskills[ tradeskill ]
if ts then
for k in pairs( ts ) do
table.insert( keys, k )
end
table.sort( keys )
for i = 1, getn( keys ) do
local _, c = string.gsub( ts[ keys[ i ] ].p, ",", "" )
s = s .. keys[ i ] .. "=" .. c .. ";"
end
end
return M.hash_string( s )
end
---@param str string
---@return integer
function M.hash_string( str )
local hash = 5381
for i = 1, string.len( str ) do
local c = string.byte( str, i )
hash = mod( hash * 33 + c, 4294967296 )
end
return hash
end
--- @param hex string
--- @return number r
--- @return number g
--- @return number b
--- @return number a
function M.hex_to_rgba( hex )
local r, g, b, a = string.match( hex, "^#?(%x%x)(%x%x)(%x%x)(%x?%x?)$" )
r, g, b = tonumber( r, 16 ) / 255, tonumber( g, 16 ) / 255, tonumber( b, 16 ) / 255
a = a ~= "" and tonumber( a, 16 ) / 255 or 1
return r, g, b, a
end
---@param o any
---@return string
function M.dump( o )
if not o then return "nil" end
if type( o ) ~= 'table' then return tostring( o ) end
local entries = 0
local s = "{"
for k, v in pairs( o ) do
if (entries == 0) then s = s .. " " end
local key = type( k ) ~= "number" and '"' .. k .. '"' or k
if (entries > 0) then s = s .. ", " end
s = s .. "[" .. key .. "] = " .. M.dump( v )
entries = entries + 1
end
if (entries > 0) then s = s .. " " end
return s .. "}"
end
---@diagnostic disable-next-line: undefined-field
if not string.gmatch then string.gmatch = string.gfind end
---@diagnostic disable-next-line: duplicate-set-field
string.match = function( str, pattern )
if not str then return nil end
local _, _, r1, r2, r3, r4, r5, r6, r7, r8, r9 = string.find( str, pattern )
return r1, r2, r3, r4, r5, r6, r7, r8, r9
end
return M