1124 lines
30 KiB
Lua
1124 lines
30 KiB
Lua
--[[
|
|
Enchantrix Addon for World of Warcraft(tm).
|
|
Version: 5.9.4961 (WhackyWallaby)
|
|
Revision: $Id: EnxUtil.lua 4933 2010-10-13 17:16:14Z Nechckn $
|
|
URL: http://enchantrix.org/
|
|
|
|
General utility functions
|
|
|
|
License:
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation; either version 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program(see GPL.txt); if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
Note:
|
|
This AddOn's source code is specifically designed to work with
|
|
World of Warcraft's interpreted AddOn system.
|
|
You have an implicit license to use this AddOn with these facilities
|
|
since that is its designated purpose as per:
|
|
http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
|
|
]]
|
|
Enchantrix_RegisterRevision("$URL: http://svn.norganna.org/auctioneer/branches/5.9/Enchantrix/EnxUtil.lua $", "$Rev: 4933 $")
|
|
|
|
-- Global functions
|
|
local getItems
|
|
local getItemType
|
|
local getReagentInfo
|
|
local getSigFromLink
|
|
local getReagentPrice
|
|
local getLinkFromName
|
|
local isDisenchantable
|
|
local getItemIdFromSig
|
|
local getItemHyperlinks
|
|
local getItemIdFromLink
|
|
local saveCraftReagentInfoToCache
|
|
local getCraftReagentInfoFromCache
|
|
|
|
local split
|
|
local chatPrint
|
|
local getRevision
|
|
local spliterator
|
|
|
|
local gcd
|
|
local round
|
|
local roundUp
|
|
local confidenceInterval
|
|
|
|
local createProfiler
|
|
|
|
local tooltip = LibStub("nTipHelper:1")
|
|
|
|
------------------------
|
|
-- Item functions --
|
|
------------------------
|
|
|
|
-- Return false if item id can't be disenchanted
|
|
function isDisenchantable(id)
|
|
if (id) then
|
|
local _, _, quality, _, _, _, _, count, equip = GetItemInfo(id)
|
|
if (not quality) then
|
|
-- GetItemInfo() failed, item might be disenchantable
|
|
return true
|
|
end
|
|
if (not Enchantrix.Constants.InventoryTypes[equip]) then
|
|
-- Neither weapon nor armor
|
|
return false
|
|
end
|
|
if (quality and quality < 2) then
|
|
-- Low quality
|
|
return false
|
|
end
|
|
if (count and count > 1) then
|
|
-- Stackable item
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
-- keep these abstracted in case they need more complex serialization
|
|
local function serializeItemInfo(...)
|
|
local str = strjoin("#", ...);
|
|
return str;
|
|
end
|
|
|
|
local function deserializeItemInfo(str)
|
|
assert(type(str) == "string")
|
|
return strsplit("#", str);
|
|
end
|
|
|
|
|
|
-- Frontend to GetItemInfo()
|
|
-- Information for disenchant reagents are kept in a saved variable cache
|
|
function getReagentInfo(reagentID)
|
|
if (not EnchantConfig) then EnchantConfig = {} end
|
|
if (not EnchantConfig.cache) then EnchantConfig.cache = {} end
|
|
if (not EnchantConfig.cache.names) then EnchantConfig.cache.names = {} end
|
|
|
|
if (not EnchantConfig.cache.reagentItemInfoBackup) then
|
|
EnchantConfig.cache.reagentItemInfoBackup = {}
|
|
-- copy in the default cache data (English only)
|
|
-- the localizations will get updated as the items come into the user's cache
|
|
EnchantConfig.cache.reagentItemInfoBackup = Enchantrix.Constants.BackupReagentItemInfo;
|
|
end
|
|
|
|
local cache = EnchantConfig.cache.reagentItemInfoBackup
|
|
|
|
local id = reagentID;
|
|
|
|
if type(id) == "string" then
|
|
local _, _, i = id:find("item:(%d-):")
|
|
if (not i) then
|
|
Enchantrix.Util.DebugPrintQuick("reagentinfo failed to get item number from string ", id );
|
|
end
|
|
id = i
|
|
end
|
|
|
|
id = tonumber(id)
|
|
|
|
if (not id) then
|
|
Enchantrix.Util.DebugPrintQuick("reagentinfo nil ID: ", type(reagentID), reagentID, tonumber(reagentID) );
|
|
end
|
|
|
|
local sName, sLink, iQuality, iLevel, rLevel, sType, sSubtype, iStack, sEquip, sTexture = GetItemInfo(id)
|
|
|
|
if (id and sName) then
|
|
-- save this reagent data while we have it
|
|
-- full item info by item id
|
|
cache[id] = serializeItemInfo( sName, sLink, iQuality, iLevel, rLevel, sType, sSubtype, iStack, sEquip, sTexture );
|
|
-- name to id mapping for getLinkFromName()
|
|
EnchantConfig.cache.names[sName] = "item:"..id..":0:0:0";
|
|
end
|
|
|
|
if (id and (not sName)) then
|
|
-- last resort is getting the data from our cache, because the WoW cache does not have this item
|
|
-- remember: the cache is not our primary storage for this data, only a backup
|
|
if ( cache[id] ) then
|
|
-- try the user's cache first
|
|
sName, sLink, iQuality, iLevel, rLevel, sType, sSubtype, iStack, sEquip, sTexture = deserializeItemInfo( cache[id] );
|
|
elseif (Enchantrix.Constants.BackupReagentItemInfo[ id ]) then
|
|
-- fallback to hard coded cache, copy to user cache if found
|
|
cache[id] = Enchantrix.Constants.BackupReagentItemInfo[ id ]
|
|
sName, sLink, iQuality, iLevel, rLevel, sType, sSubtype, iStack, sEquip, sTexture = deserializeItemInfo( Enchantrix.Constants.BackupReagentItemInfo[ id ] );
|
|
end
|
|
end
|
|
|
|
if (id and (not sName)) then
|
|
Enchantrix.Util.DebugPrintQuick("Could not find any reagent info for ", id, reagentID );
|
|
|
|
-- fake the info as best we can
|
|
sName = "item:"..id;
|
|
sLink = "item:"..id..":0:0:0"
|
|
iQuality = 0;
|
|
rLevel = 0;
|
|
end
|
|
|
|
return sName, sLink, iQuality, rLevel, sType, sSubtype, iStack, sEquip, sTexture
|
|
end
|
|
|
|
|
|
|
|
|
|
local function checkReagentCacheVersion()
|
|
if (not EnchantConfig) then EnchantConfig = {} end
|
|
if (not EnchantConfig.cache) then EnchantConfig.cache = {} end
|
|
if (not EnchantConfig.cache.CraftReagentCache) then EnchantConfig.cache.CraftReagentCache = {} end
|
|
|
|
local myFormatVersion = 1; -- in case we need to change the format dramatically
|
|
local version,build,date = GetBuildInfo();
|
|
local versionString = strjoin(".", version, build, myFormatVersion );
|
|
|
|
if EnchantConfig.cache.CraftReagentCache.Version then
|
|
-- version stamp exists, check it
|
|
if (EnchantConfig.cache.CraftReagentCache.Version ~= versionString) then
|
|
--Enchantrix.Util.DebugPrintQuick("Found a new WoW version, wiping out reagent cache");
|
|
EnchantConfig.cache.CraftReagentCache = {}
|
|
end
|
|
end
|
|
|
|
EnchantConfig.cache.CraftReagentCache.Version = versionString;
|
|
end
|
|
|
|
|
|
-- ccox - originally I had a timestamp in here, but think that's pointless when reagents don't change without WoW version changes
|
|
-- also, without the timestamp, you're more likely to keep a cache of items you've seen from your alts
|
|
|
|
-- ["itemname"] = "reagent1itemNo:reagent1count;reagent2itemNo:count;reagent3itemNo:reagent3count"
|
|
|
|
local function assembleReagentEntryString( reagentList )
|
|
|
|
local reagentString = nil
|
|
for _, reagent in ipairs(reagentList) do
|
|
|
|
local itemLink = reagent[1]
|
|
local count = reagent[2]
|
|
|
|
local itemNumber
|
|
if type(itemLink) == "string" then
|
|
local _, _, i = itemLink:find("item:(%d-):")
|
|
if (not i) then
|
|
Enchantrix.Util.DebugPrintQuick("assembleReagentEntryString failed to get item number from string ", itemLink );
|
|
end
|
|
itemNumber = i
|
|
else
|
|
itemNumber = Enchantrix.Util.GetItemIdFromLink(itemLink)
|
|
end
|
|
|
|
itemNumber = tonumber(itemNumber)
|
|
if (not itemNumber) then
|
|
-- something failed, bail
|
|
return nil
|
|
end
|
|
|
|
local oneEntry = strjoin(":", itemNumber, count );
|
|
|
|
if (reagentString) then
|
|
reagentString = strjoin(";", reagentString, oneEntry);
|
|
else
|
|
reagentString = oneEntry;
|
|
end
|
|
end
|
|
|
|
return reagentString;
|
|
end
|
|
|
|
|
|
local function createReagentList(...)
|
|
local n = select("#", ... )
|
|
local reagentList = {}
|
|
for i = 1, n do
|
|
local oneEntry = select(i, ...)
|
|
local itemNumber, itemCount = strsplit(":", oneEntry);
|
|
itemNumber = tonumber(itemNumber)
|
|
itemCount = tonumber(itemCount)
|
|
reagentList[i] = { itemNumber, itemCount }
|
|
end
|
|
return reagentList
|
|
end
|
|
|
|
local function dissectReagentEntryString(entryString)
|
|
if not entryString then
|
|
return
|
|
end
|
|
assert(type(entryString) == "string")
|
|
local reagentList = createReagentList( strsplit(";", entryString ) );
|
|
return reagentList
|
|
end
|
|
|
|
|
|
|
|
|
|
function saveCraftReagentInfoToCache(itemname, reagentList)
|
|
checkReagentCacheVersion();
|
|
local entryString = assembleReagentEntryString( reagentList );
|
|
if (entryString) then
|
|
EnchantConfig.cache.CraftReagentCache[ itemname ] = entryString;
|
|
end
|
|
end
|
|
|
|
|
|
function getCraftReagentInfoFromCache(itemname)
|
|
checkReagentCacheVersion();
|
|
local entryString = EnchantConfig.cache.CraftReagentCache[ itemname ];
|
|
if (not entryString) then
|
|
return nil;
|
|
end
|
|
local reagentList = dissectReagentEntryString(entryString);
|
|
return reagentList
|
|
end
|
|
|
|
|
|
|
|
-- TODO: what is the correct limit post TBC?
|
|
-- ccox - 32090 is the highest I can find so far
|
|
-- but we REALLY should get rid of this search!
|
|
Enchantrix.State.MAX_ITEM_ID = 33000
|
|
|
|
function getLinkFromName(name)
|
|
assert(type(name) == "string")
|
|
|
|
if not EnchantConfig.cache then
|
|
EnchantConfig.cache = {}
|
|
end
|
|
if not EnchantConfig.cache.names then
|
|
EnchantConfig.cache.names = {}
|
|
end
|
|
|
|
local link = EnchantConfig.cache.names[name]
|
|
if link then
|
|
local n = GetItemInfo(link)
|
|
if n ~= name then
|
|
EnchantConfig.cache.names[name] = nil
|
|
end
|
|
end
|
|
|
|
if not EnchantConfig.cache.names[name] then
|
|
|
|
-- if we didn't find it in the cache, try something else
|
|
-- first, check our list of reagent item ids, because they're most likely
|
|
for i, _ in ipairs( Enchantrix.Constants.StaticPrices ) do
|
|
local n, link = GetItemInfo(i)
|
|
if n and (n == name) then
|
|
EnchantConfig.cache.names[name] = link
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not EnchantConfig.cache.names[name] then
|
|
--print("DISABLED NAME CACHE LOOKUP FOR BETA SERVER", name)
|
|
-- still no result? Darn.
|
|
-- last resort, check ALL item ids until we find a name match, and cache it!
|
|
-- for i = 1, Enchantrix.State.MAX_ITEM_ID + 4000 do
|
|
-- local n, link = GetItemInfo(i)
|
|
-- if n then
|
|
-- if n == name then
|
|
-- EnchantConfig.cache.names[name] = link
|
|
-- break
|
|
-- end
|
|
-- Enchantrix.State.MAX_ITEM_ID = math.max(Enchantrix.State.MAX_ITEM_ID, i)
|
|
-- end
|
|
-- end
|
|
end
|
|
|
|
return EnchantConfig.cache.names[name]
|
|
end
|
|
|
|
|
|
-- Returns HSP, median and static price for reagent
|
|
-- Auctioneer values are kept in cache for 48h in case Auctioneer isn't loaded
|
|
-- Please remember that this gets called by things outside of enchantrix (barker, btmscan, etc.) and they need valid pricing
|
|
function getReagentPrice(reagentID, extra)
|
|
-- reagentID ::= number | hyperlink
|
|
|
|
if type(reagentID) == "string" then
|
|
local _, _, i = reagentID:find("item:(%d+):")
|
|
reagentID = i
|
|
end
|
|
reagentID = tonumber(reagentID)
|
|
if not reagentID then return nil end
|
|
|
|
if Enchantrix.Settings.GetSetting('fixed.'..reagentID) then
|
|
local myValue = tonumber(Enchantrix.Settings.GetSetting('fixed.'..reagentID..'.value'))
|
|
if myValue then
|
|
local weight = Enchantrix.Settings.GetSetting("weight."..reagentID) / 100
|
|
myValue = myValue * weight
|
|
-- this function can get called by anyone, and must return usable values
|
|
return myValue,myValue,myValue,myValue,myValue
|
|
end
|
|
end
|
|
|
|
local hsp, median, market, price5
|
|
market = Enchantrix.Constants.StaticPrices[reagentID]
|
|
|
|
if ( Enchantrix.Constants.VendorTrash[reagentID] ) then
|
|
-- it's trash, so we'll just use the vendor price
|
|
hsp = market;
|
|
median = market;
|
|
price5 = market;
|
|
else
|
|
-- not trash, lookup the auction price
|
|
if AucAdvanced then
|
|
if extra then
|
|
local _, reagentLink, _, _, _, _, _, _, _ = getReagentInfo(reagentID)
|
|
price5 = AucAdvanced.API.GetAlgorithmValue(extra, reagentLink)
|
|
else
|
|
local _, link = GetItemInfo(reagentID);
|
|
price5 = AucAdvanced.API.GetMarketValue(link);
|
|
end
|
|
end
|
|
if Auctioneer and Enchantrix.State.Auctioneer_Loaded
|
|
and Auctioneer.Util and Auctioneer.Statistic then
|
|
local itemKey = ("%d:0:0"):format(reagentID);
|
|
local realm = Auctioneer.Util.GetAuctionKey()
|
|
hsp = Auctioneer.Statistic.GetHSP(itemKey, realm)
|
|
median = Auctioneer.Statistic.GetUsableMedian(itemKey, realm)
|
|
end
|
|
end
|
|
|
|
if not EnchantConfig.cache then EnchantConfig.cache = {} end
|
|
if not EnchantConfig.cache.prices then EnchantConfig.cache.prices = {} end
|
|
if not EnchantConfig.cache.prices[reagentID] then EnchantConfig.cache.prices[reagentID] = {} end
|
|
local cache = EnchantConfig.cache.prices[reagentID]
|
|
if cache.timestamp and time() - cache.timestamp > 172800 then
|
|
cache = {}
|
|
end
|
|
|
|
cache.hsp = hsp or cache.hsp
|
|
cache.median = median or cache.median
|
|
cache.market = market or cache.market
|
|
cache.price5 = price5 or cache.price5
|
|
cache.timestamp = time()
|
|
|
|
hsp, median, market, price5 = cache.hsp, cache.median, cache.market, cache.price5
|
|
local weight = Enchantrix.Settings.GetSetting("weight."..reagentID) / 100
|
|
if (hsp) then hsp = hsp * weight end
|
|
if (median) then median = median * weight end
|
|
if (market) then market = market * weight end
|
|
if (price5) then price5 = price5 * weight end
|
|
|
|
return hsp, median, market, price5
|
|
end
|
|
|
|
|
|
-- Return item level (rounded up to nearest 5 levels), quality and type as string,
|
|
-- e.g. "20:2:Armor" for uncommon level 20 armor
|
|
function getItemType(id)
|
|
if (id) then
|
|
local _, _, quality, ilevel, _, _, _, _, equip = GetItemInfo(id)
|
|
if (quality and quality >= 2 and Enchantrix.Constants.InventoryTypes[equip]) then
|
|
return ("%d:%d:%s"):format(Enchantrix.Util.RoundUp(ilevel, 5), quality, Enchantrix.Constants.InventoryTypes[equip])
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Return item id as integer
|
|
function getItemIdFromSig(sig)
|
|
if type(sig) == "string" then
|
|
_, _, sig = sig:find("(%d+)")
|
|
end
|
|
return tonumber(sig)
|
|
end
|
|
|
|
function getItemIdFromLink(link)
|
|
local itemType, itemId = tooltip:DecodeLink(link)
|
|
if (itemType == "item") then
|
|
return itemId
|
|
end
|
|
end
|
|
|
|
function getIType(link)
|
|
assert(type(link) == "string")
|
|
local iId = getItemIdFromLink(link)
|
|
local iName,iLink,iQual,iLevel,iMin,iType,iSub,iStack,iEquip,iTex=GetItemInfo(link)
|
|
if (iQual < 2) then
|
|
--Enchantrix.DebugPrint("GetIType", ENX_INFO, "Quality too low", "The quality for " .. link .. " is too low (" .. iQual .. "< 2)")
|
|
return
|
|
end
|
|
if not iEquip then
|
|
--Enchantrix.DebugPrint("GetIType", ENX_INFO, "Item not equippable", "The item " .. link .. " is not equippable")
|
|
return
|
|
end
|
|
local invType = Enchantrix.Constants.InventoryTypes[iEquip]
|
|
if not invType then
|
|
Enchantrix.DebugPrint("GetIType", ENX_INFO, "Unrecognized equip slot", "The item " .. link .. " has an equip slot (" .. iEquip .. ") that is not recognized")
|
|
return
|
|
end
|
|
|
|
return ("%d:%d:%d:%d"):format(iLevel, iQual, invType, iId)
|
|
end
|
|
|
|
function getSigFromLink(link)
|
|
assert(type(link) == "string")
|
|
|
|
local _, _, id, rand = link:find("item:(%d+):%d+:(%d+):%d+")
|
|
if id and rand then
|
|
return id..":0:"..rand
|
|
end
|
|
end
|
|
|
|
function getItems(str)
|
|
if (not str) then return end
|
|
local itemList = {};
|
|
local itemKey;
|
|
|
|
for itemID, randomProp, enchant, uniqID in str:gmatch("|Hitem:(%d+):(%d+):(%d+):(%d+)|h") do
|
|
itemKey = itemID..":"..randomProp..":"..enchant;
|
|
table.insert(itemList, itemKey)
|
|
end
|
|
return itemList;
|
|
end
|
|
|
|
--Many thanks to the guys at irc://irc.datavertex.com/cosmostesters for their help in creating this function
|
|
function getItemHyperlinks(str)
|
|
if (not str) then return nil end
|
|
local itemList = {};
|
|
|
|
for color, item, name in str:gmatch("|c(%x+)|Hitem:(%d+:%d+:%d+:%d+)|h%[(.-)%]|h|r") do
|
|
table.insert(itemList, "|c"..color.."|Hitem:"..item.."|h["..name.."]|h|r")
|
|
end
|
|
return itemList;
|
|
end
|
|
-----------------------------------
|
|
-- General Utility Functions --
|
|
-----------------------------------
|
|
|
|
-- Extract the revision number from SVN keyword string
|
|
function getRevision(str)
|
|
if not str then return 0 end
|
|
local _, _, rev = str:find("Revision: (%d+)")
|
|
return tonumber(rev) or 0
|
|
end
|
|
|
|
function split(str, at)
|
|
local splut = {};
|
|
|
|
if (type(str) ~= "string") then return nil end
|
|
if (not str) then str = "" end
|
|
|
|
if (not at)
|
|
then table.insert(splut, str)
|
|
|
|
else
|
|
for n, c in str:gmatch('([^%'..at..']*)(%'..at..'?)') do
|
|
table.insert(splut, n);
|
|
|
|
if (c == '') then break end
|
|
end
|
|
end
|
|
return splut;
|
|
end
|
|
|
|
-- Iterator version of split()
|
|
-- for i in spliterator(a, b) do
|
|
-- is equivalent to
|
|
-- for _, i in ipairs(split(a, b)) do
|
|
-- but puts less strain on the garbage collector
|
|
function spliterator(str, at)
|
|
local start
|
|
local found = 0
|
|
local done = (type(str) ~= "string")
|
|
return function()
|
|
if done then return nil end
|
|
start = found + 1
|
|
found = str:find(at, start, true)
|
|
if not found then
|
|
found = 0
|
|
done = true
|
|
end
|
|
return str:sub(start, found - 1)
|
|
end
|
|
end
|
|
|
|
function chatPrint(text, cRed, cGreen, cBlue, cAlpha, holdTime)
|
|
local frameIndex = Enchantrix.Config.GetFrameIndex();
|
|
|
|
if (cRed and cGreen and cBlue) then
|
|
if _G["ChatFrame"..frameIndex] then
|
|
_G["ChatFrame"..frameIndex]:AddMessage(text, cRed, cGreen, cBlue, cAlpha, holdTime);
|
|
|
|
elseif (DEFAULT_CHAT_FRAME) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(text, cRed, cGreen, cBlue, cAlpha, holdTime);
|
|
end
|
|
|
|
else
|
|
if _G["ChatFrame"..frameIndex] then
|
|
_G["ChatFrame"..frameIndex]:AddMessage(text, 1.0, 0.5, 0.25);
|
|
elseif (DEFAULT_CHAT_FRAME) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(text, 1.0, 0.5, 0.25);
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
------------------------
|
|
-- Math Functions --
|
|
------------------------
|
|
|
|
function gcd(a, b)
|
|
-- Greatest Common Divisor, Euclidean algorithm
|
|
local m, n = tonumber(a), tonumber(b) or 0
|
|
while (n ~= 0) do
|
|
m, n = n, math.fmod(m, n)
|
|
end
|
|
return m
|
|
end
|
|
|
|
-- Round up m to nearest multiple of n
|
|
function roundUp(m, n)
|
|
return math.ceil(m / n) * n
|
|
end
|
|
|
|
-- Round m to n digits in given base
|
|
function round(m, n, base, offset)
|
|
base = base or 10 -- Default to base 10
|
|
offset = offset or 0.5
|
|
|
|
if (n or 0) == 0 then
|
|
return math.floor(m + offset)
|
|
end
|
|
|
|
if m == 0 then
|
|
return 0
|
|
elseif m < 0 then
|
|
return -round(-m, n, base, offset)
|
|
end
|
|
|
|
-- Get integer and fractional part of n
|
|
local f = math.floor(n)
|
|
n, f = f, n - f
|
|
|
|
-- Pre-rounding multiplier is 1 / f
|
|
local mul = 1
|
|
if f > 0.1 then
|
|
mul = math.floor(1 / f + 0.5)
|
|
end
|
|
|
|
local d
|
|
if n > 0 then
|
|
d = base^(n - math.floor(math.log(m) / math.log(base)) - 1)
|
|
else
|
|
d = 1
|
|
end
|
|
if offset >= 1 then
|
|
return math.ceil(m * d * mul) / (d * mul)
|
|
else
|
|
return math.floor(m * d * mul + offset) / (d * mul)
|
|
end
|
|
end
|
|
|
|
-- Returns confidence interval for binomial distribution given observed
|
|
-- probability p, sample size n, and z-value
|
|
function confidenceInterval(p, n, z)
|
|
if not z then
|
|
--[[
|
|
z conf
|
|
1.282 80%
|
|
1.645 90%
|
|
1.960 95%
|
|
2.326 98%
|
|
2.576 99%
|
|
3.090 99.8%
|
|
3.291 99.9%
|
|
]]
|
|
z = 1.645
|
|
end
|
|
assert(p >= 0 and p <= 1)
|
|
assert(n > 0)
|
|
|
|
local a = p + z^2 / (2 * n)
|
|
local b = z * math.sqrt(p * (1 - p) / n + z^2 / (4 * n^2))
|
|
local c = 1 + z^2 / n
|
|
|
|
return (a - b) / c, (a + b) / c
|
|
end
|
|
|
|
---------------------
|
|
-- Debug functions --
|
|
---------------------
|
|
|
|
-- profiler:Start()
|
|
-- Record start time and memory, set state to running
|
|
local function _profilerStart(this)
|
|
this.t = GetTime()
|
|
this.m = gcinfo()
|
|
this.r = true
|
|
end
|
|
|
|
-- profiler:Stop()
|
|
-- Record time and memory change, set state to stopped
|
|
local function _profilerStop(this)
|
|
this.m = (gcinfo()) - this.m
|
|
this.t = GetTime() - this.t
|
|
this.r = false
|
|
end
|
|
|
|
-- profiler:DebugPrint()
|
|
local function _profilerDebugPrint(this)
|
|
if this.n then
|
|
Enchantrix.Util.DebugPrintQuick("Profiler ["..this.n.."]")
|
|
else
|
|
Enchantrix.Util.DebugPrintQuick("Profiler")
|
|
end
|
|
if this.r == nil then
|
|
Enchantrix.Util.DebugPrintQuick(" Not started")
|
|
else
|
|
Enchantrix.Util.DebugPrintQuick((" Time: %0.3f s"):format(this:Time()))
|
|
Enchantrix.Util.DebugPrintQuick((" Mem: %0.0f KiB"):format(this:Mem()))
|
|
if this.r then
|
|
Enchantrix.Util.DebugPrintQuick(" Running...")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- time = profiler:Time()
|
|
-- Return time (in seconds) from Start() [until Stop(), if stopped]
|
|
local function _profilerTime(this)
|
|
if this.r == false then
|
|
return this.t
|
|
elseif this.r == true then
|
|
return GetTime() - this.t
|
|
end
|
|
end
|
|
|
|
-- mem = profiler:Mem()
|
|
-- Return memory change (in kilobytes) from Start() [until Stop(), if stopped]
|
|
local function _profilerMem(this)
|
|
if this.r == false then
|
|
return this.m
|
|
elseif this.r == true then
|
|
return (gcinfo()) - this.m
|
|
end
|
|
end
|
|
|
|
-- profiler = Enchantrix.Util.CreateProfiler("foobar")
|
|
function createProfiler(name)
|
|
return {
|
|
Start = _profilerStart,
|
|
Stop = _profilerStop,
|
|
DebugPrint = _profilerDebugPrint,
|
|
Time = _profilerTime,
|
|
Mem = _profilerMem,
|
|
n = name,
|
|
}
|
|
end
|
|
|
|
Enchantrix.Util = {
|
|
Revision = "$Revision: 4933 $",
|
|
|
|
GetItems = getItems,
|
|
GetItemType = getItemType,
|
|
SigFromLink = sigFromLink,
|
|
GetReagentInfo = getReagentInfo,
|
|
GetSigFromLink = getSigFromLink,
|
|
GetLinkFromName = getLinkFromName,
|
|
GetReagentPrice = getReagentPrice,
|
|
GetItemIdFromSig = getItemIdFromSig,
|
|
IsDisenchantable = isDisenchantable,
|
|
GetItemIdFromLink = getItemIdFromLink,
|
|
GetItemHyperlinks = getItemHyperlinks,
|
|
SaveCraftReagentInfoToCache = saveCraftReagentInfoToCache,
|
|
GetCraftReagentInfoFromCache = getCraftReagentInfoFromCache,
|
|
|
|
Split = split,
|
|
ChatPrint = chatPrint,
|
|
Spliterator = spliterator,
|
|
GetRevision = getRevision,
|
|
|
|
GCD = gcd,
|
|
Round = round,
|
|
RoundUp = roundUp,
|
|
ConfidenceInterval = confidenceInterval,
|
|
|
|
CreateProfiler = createProfiler,
|
|
}
|
|
|
|
|
|
function Enchantrix.Util.GetIType(link)
|
|
if not link then return end
|
|
local const = Enchantrix.Constants
|
|
local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, invTexture = GetItemInfo(link)
|
|
|
|
if not (itemName and itemEquipLoc and itemRarity and itemLevel) then
|
|
Enchantrix.Util.DebugPrint("GetIType", ENX_INFO, "GetItemInfo failed, bad link", "could not get item info for: " .. link)
|
|
return
|
|
end
|
|
|
|
local class = const.InventoryTypes[itemEquipLoc] or 0
|
|
|
|
if itemRarity < 2 or not (class and (class == const.WEAPON or class == const.ARMOR)) then
|
|
return
|
|
end
|
|
|
|
return ("%d:%d:%d"):format(itemLevel,itemRarity,class)
|
|
end
|
|
|
|
|
|
-- NOTE - ccox - this ignores the skill requirements due to item quality!
|
|
function Enchantrix.Util.MaxDisenchantItemLevel(skill)
|
|
local maxLevel;
|
|
|
|
if (skill >= 375) then
|
|
maxLevel = 220;
|
|
elseif (skill >= 350) then
|
|
maxLevel = 200;
|
|
elseif (skill >= 325) then
|
|
maxLevel = 151;
|
|
elseif (skill >= 300) then
|
|
maxLevel = 129; -- max level for WoW 2.2/BC ??
|
|
elseif (skill >= 125) then
|
|
-- skill 125 to 299
|
|
maxLevel = 19 + (5 * math.floor(skill / 25));
|
|
else
|
|
-- skill 1 to 124
|
|
maxLevel = 15 + (5 * math.floor(skill / 25));
|
|
end
|
|
|
|
return maxLevel;
|
|
end
|
|
|
|
|
|
|
|
function Enchantrix.Util.DisenchantSkillRequiredForItemLevel(level, quality)
|
|
-- should we cache this in a table?
|
|
|
|
if (not level or not quality) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil level or quality", level, quality )
|
|
return 0
|
|
end
|
|
|
|
if (level >= 200) then
|
|
-- rares still bugged Nov 2009
|
|
if (quality ~= 3) then
|
|
return 375;
|
|
else
|
|
return 325;
|
|
end
|
|
|
|
elseif (level >= 152) then
|
|
-- rares still bugged Nov 2009
|
|
if (quality ~= 3) then
|
|
return 350;
|
|
else
|
|
return 325;
|
|
end -- ccox - rare/blue items are still 325 due to a Blizzard bug, hope it gets fixed soon
|
|
|
|
elseif (level >= 130) then
|
|
return 325;
|
|
|
|
elseif (level > 65) then
|
|
-- someone changed their math with the Burning Crusade
|
|
|
|
-- epics are a little different
|
|
if (quality == 4) then
|
|
if (level >= 90) then
|
|
return 300;
|
|
else
|
|
return 225;
|
|
end
|
|
end
|
|
|
|
if (level >= 100) then
|
|
return 275;
|
|
else
|
|
return 225;
|
|
end
|
|
|
|
elseif (level > 20) then
|
|
local temp = level - 21;
|
|
temp = 1 + floor( temp / 5 );
|
|
temp = temp * 25;
|
|
if (temp > 275) then
|
|
temp = 275;
|
|
end
|
|
return temp;
|
|
end
|
|
|
|
return 1;
|
|
end
|
|
|
|
|
|
function Enchantrix.Util.InscriptionSkillRequiredForItem(link)
|
|
if (not link) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil link", link )
|
|
return 0
|
|
end
|
|
local item = getItemIdFromLink(link);
|
|
if (not item) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil item from link", link )
|
|
return 0
|
|
end
|
|
local resultBracket = Enchantrix.Constants.MillableItems[item];
|
|
if (not resultBracket) then
|
|
return 0
|
|
end
|
|
return Enchantrix.Constants.MillingSkillRequired[resultBracket];
|
|
end
|
|
|
|
function Enchantrix.Util.JewelCraftSkillRequiredForItem(link)
|
|
if (not link) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil link", link )
|
|
return 0
|
|
end
|
|
local item = getItemIdFromLink(link);
|
|
if (not item) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil item from link", link )
|
|
return 0
|
|
end
|
|
local minLevel = Enchantrix.Constants.ProspectMinLevels[item];
|
|
return minLevel;
|
|
end
|
|
|
|
function Enchantrix.Util.DisenchantSkillRequiredForItem(link)
|
|
if (not link) then
|
|
--Enchantrix.Util.DebugPrintQuick( "nil link", link )
|
|
return 0
|
|
end
|
|
local _, _, quality, itemLevel = GetItemInfo(link);
|
|
return Enchantrix.Util.DisenchantSkillRequiredForItemLevel(itemLevel, quality);
|
|
end
|
|
|
|
|
|
-- NOTE: this is an expensive function
|
|
-- we try to make it friendlier by caching the value and only checking every 5 seconds
|
|
|
|
Enchantrix.Util.SkillCacheRank = {}
|
|
Enchantrix.Util.SkillCacheTimeStamp = {}
|
|
|
|
function Enchantrix.Util.GetUserSkillByName( name )
|
|
|
|
local cacheRank = Enchantrix.Util.SkillCacheRank[ name ]
|
|
local cacheTime = Enchantrix.Util.SkillCacheTimeStamp[ name ]
|
|
if (cacheRank and cacheTime
|
|
and (GetTime() - cacheTime) < 5) then
|
|
return cacheRank
|
|
end
|
|
|
|
local resultRank = 0
|
|
--[[ WOW 3.0 WOTLK server version runs the old code path SHIM REMOVE]]
|
|
if GetNumSkillLines then
|
|
local MyExpandedHeaders = {}
|
|
local i, j
|
|
|
|
|
|
-- search the skill tree for the named skill
|
|
for i=0, GetNumSkillLines(), 1 do
|
|
local skillName, header, isExpanded, skillRank = GetSkillLineInfo(i)
|
|
-- expand the header if necessary
|
|
if ( header and not isExpanded ) then
|
|
MyExpandedHeaders[i] = skillName
|
|
end
|
|
end
|
|
|
|
ExpandSkillHeader(0)
|
|
for i=1, GetNumSkillLines(), 1 do
|
|
local skillName, header, _, skillRank = GetSkillLineInfo(i)
|
|
-- check for the skill name
|
|
if (skillName and not header) then
|
|
if (skillName == name) then
|
|
resultRank = skillRank
|
|
-- no need to look at the rest of the skills
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- close headers expanded during search process
|
|
for i=0, GetNumSkillLines() do
|
|
local skillName, header, isExpanded = GetSkillLineInfo(i)
|
|
for j in pairs(MyExpandedHeaders) do
|
|
if ( header and skillName == MyExpandedHeaders[j] ) then
|
|
CollapseSkillHeader(i)
|
|
MyExpandedHeaders[j] = nil
|
|
end
|
|
end
|
|
end
|
|
else
|
|
--WOW 4.0 Cataclysm uses the new profession system
|
|
local prof1, prof2 = GetProfessions()
|
|
local skillName1, _, rank1
|
|
local skillName2, _, rank2
|
|
if prof1 then
|
|
skillName1, _, rank1= GetProfessionInfo(prof1)
|
|
end
|
|
if prof2 then
|
|
skillName2, _, rank2 = GetProfessionInfo(prof2)
|
|
end
|
|
|
|
if name == skillName1 then
|
|
resultRank = rank1
|
|
elseif name == skillName2 then
|
|
resultRank = rank2
|
|
end
|
|
end
|
|
|
|
Enchantrix.Util.SkillCacheRank[ name ] = resultRank
|
|
Enchantrix.Util.SkillCacheTimeStamp[ name ] = GetTime()
|
|
|
|
return resultRank
|
|
end
|
|
|
|
|
|
function Enchantrix.Util.GetUserEnchantingSkill()
|
|
return Enchantrix.Util.GetUserSkillByName( _ENCH("Enchanting") )
|
|
end
|
|
|
|
function Enchantrix.Util.GetUserJewelCraftingSkill()
|
|
return Enchantrix.Util.GetUserSkillByName( _ENCH("Jewelcrafting") )
|
|
end
|
|
|
|
function Enchantrix.Util.GetUserInscriptionSkill()
|
|
return Enchantrix.Util.GetUserSkillByName( _ENCH("Inscription") )
|
|
end
|
|
|
|
|
|
-- an attempt to balance the price of essences when doing auction scans
|
|
-- still experimental
|
|
local function balanceEssencePrices(scanReagentTable, style)
|
|
|
|
-- lesser_itemid = greater_itemid
|
|
local essenceTable = {
|
|
[10938] = 10939, -- magic
|
|
[10998] = 11082, -- astral
|
|
[11134] = 11135, -- mystic
|
|
[11174] = 11175, -- nether
|
|
[16202] = 16203, -- eternal
|
|
[22447] = 22446, -- planar
|
|
[34056] = 34055, -- cosmic
|
|
};
|
|
|
|
for lesser, greater in pairs(essenceTable) do
|
|
|
|
local priceLesser = scanReagentTable[ lesser ];
|
|
local priceGreater = scanReagentTable[ greater ];
|
|
|
|
if (style == "min") then
|
|
-- for pessimists who want to hedge their bets (and possibly undervalue their disenchant predictions)
|
|
priceLesser = math.min( priceLesser, priceGreater / 3 );
|
|
elseif (style == "max") then
|
|
-- for optimists who want to maximize profits when selling mats (and possibly overvalue their disenchant predictions)
|
|
priceLesser = math.max( priceLesser, priceGreater / 3 );
|
|
else -- if (style == "avg") then
|
|
-- for those who want the middle of the road
|
|
priceLesser = ( priceLesser + (priceGreater / 3) ) / 2;
|
|
end
|
|
|
|
scanReagentTable[ lesser ] = priceLesser;
|
|
scanReagentTable[ greater ] = 3 * priceLesser;
|
|
|
|
end
|
|
|
|
end
|
|
|
|
function Enchantrix.Util.GetPricingModel()
|
|
local style = Enchantrix.Settings.GetSetting('ScanValueType');
|
|
local extra = nil
|
|
if (not style) then
|
|
style = "average";
|
|
end
|
|
if (style:sub(1,9) == "adv:stat:") then
|
|
extra = style:sub(10)
|
|
end
|
|
return style, extra
|
|
end
|
|
|
|
function Enchantrix.Util.CreateReagentPricingTable(scanReagentTable)
|
|
if not scanReagentTable then
|
|
scanReagentTable = {}
|
|
else
|
|
for k,v in pairs(scanReagentTable) do
|
|
scanReagentTable[k] = nil
|
|
end
|
|
end
|
|
local n = #Enchantrix.Constants.DisenchantReagentList;
|
|
local style, extra = Enchantrix.Util.GetPricingModel();
|
|
|
|
for i = 1, n do
|
|
local reagent = Enchantrix.Constants.DisenchantReagentList[i];
|
|
reagent = tonumber(reagent);
|
|
|
|
local myValue = 0;
|
|
if Enchantrix.Settings.GetSetting('fixed.'..reagent) then
|
|
myValue = tonumber(Enchantrix.Settings.GetSetting('fixed.'..reagent..'.value')) or 0
|
|
else
|
|
local hsp, med, mkt, five = Enchantrix.Util.GetReagentPrice(reagent, extra);
|
|
|
|
if (style == "auc4:hsp") then
|
|
myValue = hsp;
|
|
elseif (style == "auc4:med") then
|
|
myValue = med;
|
|
elseif (style == "baseline") then
|
|
myValue = mkt;
|
|
elseif (AucAdvanced and (style == "adv:market" or extra)) then
|
|
myValue = five;
|
|
else
|
|
local c = 0
|
|
if (hsp) then myValue=myValue+hsp c=c+1 end
|
|
if (med) then myValue=myValue+med c=c+1 end
|
|
if (mkt) then myValue=myValue+mkt c=c+1 end
|
|
if (five) then myValue=myValue+five c=c+1 end
|
|
myValue = myValue / c
|
|
end
|
|
|
|
-- provide fallbacks in case a valuation is missing
|
|
-- don't leave a nil value!
|
|
if (not myValue or myValue == 0) then
|
|
myValue = hsp or five or mkt or 0;
|
|
end
|
|
end
|
|
|
|
scanReagentTable[ reagent ] = myValue;
|
|
end
|
|
|
|
if (Enchantrix.Settings.GetSetting('AuctionBalanceEssencePrices')) then
|
|
balanceEssencePrices(scanReagentTable, Enchantrix.Settings.GetSetting('AuctionBalanceEssenceStyle'));
|
|
end
|
|
|
|
return scanReagentTable;
|
|
end
|
|
|
|
local DebugLib = LibStub("DebugLib")
|
|
local debug, assert, printQuick
|
|
if DebugLib then
|
|
debug, assert, printQuick = DebugLib("Enchantrix")
|
|
else
|
|
function debug() end
|
|
assert = debug
|
|
printQuick = debug
|
|
end
|
|
|
|
ENX_CRITICAL = "Critical"
|
|
ENX_ERROR = "Error"
|
|
ENX_WARNING = "Warning"
|
|
ENX_NOTICE = "Notice"
|
|
-- info will only go to nLog
|
|
ENX_INFO = "Info"
|
|
-- Debug will print to the chat console as well as to nLog
|
|
ENX_DEBUG = "Debug"
|
|
|
|
function Enchantrix.Util.DebugPrint(mType, mLevel, mTitle, ...)
|
|
-- function libDebugPrint(addon, message, category, title, errorCode, level)
|
|
local message = debug:Dump(...)
|
|
debug(message, mType, mTitle, nil, mLevel)
|
|
end
|
|
|
|
-- when you just want to print a message and don't care about the rest
|
|
function Enchantrix.Util.DebugPrintQuick(...)
|
|
printQuick(...)
|
|
end
|
|
|
|
|
|
|
|
|