aux auto bid and price heuristics

this commit does two things
1. it implements auto bid just like auto buy
2. it has some minimum pricing heuristics when posting items. this behavoiur actiaves when the post bid price is set to 0, the buyout price behaves like a minimum
This commit is contained in:
geojak
2022-02-20 23:51:19 +01:00
committed by GitHub
parent 9771521c78
commit 02cfc420dd
42 changed files with 8791 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
module 'aux.util.completion'
local aux = require 'aux'
local filter_util = require 'aux.util.filter'
function M:complete_filter()
if IsControlKeyDown() then -- TODO problem is ctrl-v, maybe find a better solution
return
end
local filter_string = this:GetText()
local completed_filter_string = aux.select(3, strfind(filter_string, '([^;]*)/[^/;]*$'))
local _, suggestions = filter_util.query(completed_filter_string)
local start_index, _, current_modifier = strfind(filter_string, '([^/;]*)$')
current_modifier = current_modifier or ''
for _, suggestion in ipairs(suggestions) do
if strsub(strupper(suggestion), 1, strlen(current_modifier)) == strupper(current_modifier) then
this:SetText(strlower(strsub(filter_string, 1, start_index - 1) .. suggestion))
this:HighlightText(strlen(filter_string), -1)
return
end
end
end
function M.complete(candidates)
return function(self)
if IsControlKeyDown() then -- TODO problem is ctrl-v, maybe find a better solution
return
end
local text = self:GetText()
local t = candidates()
for i = 1, getn(t) do
if strsub(strupper(t[i]), 1, strlen(text)) == strupper(text) then
self:SetText(strlower(t[i]))
self:HighlightText(strlen(text), -1)
return
end
end
end
end
+549
View File
@@ -0,0 +1,549 @@
module 'aux.util.filter'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local money = require 'aux.util.money'
local filter_util = require 'aux.util.filter'
local history = require 'aux.core.history'
local disenchant = require 'aux.core.disenchant'
function default_filter(str)
return {
input_type = '',
validator = function()
return function(auction_record)
return aux.any(auction_record.tooltip, function(entry)
return strfind(strlower(entry.left_text or ''), str, 1, true) or strfind(strlower(entry.right_text or ''), str, 1, true)
end)
end
end,
}
end
M.filters = {
['utilizable'] = {
input_type = '',
validator = function()
return function(auction_record)
return auction_record.usable and not info.tooltip_match(ITEM_SPELL_KNOWN, auction_record.tooltip)
end
end,
},
['tooltip'] = {
input_type = 'string',
validator = function(str)
return default_filter(str).validator()
end,
},
['item'] = {
input_type = 'string',
validator = function(name)
return function(auction_record)
return strlower(info.item(auction_record.item_id).name) == name
end
end
},
['left'] = {
input_type = T.list('30m', '2h', '8h', '24h'),
validator = function(index)
return function(auction_record)
return auction_record.duration == index
end
end
},
['rarity'] = {
input_type = T.list('poor', 'common', 'uncommon', 'rare', 'epic'),
validator = function(index)
return function(auction_record)
return auction_record.quality == index - 1
end
end
},
['min-level'] = {
input_type = 'number',
validator = function(level)
return function(auction_record)
return auction_record.level >= level
end
end
},
['max-level'] = {
input_type = 'number',
validator = function(level)
return function(auction_record)
return auction_record.level <= level
end
end
},
['bid-price'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
return auction_record.unit_bid_price <= amount
end
end
},
['price'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
return auction_record.buyout_price > 0 and auction_record.unit_buyout_price <= amount
end
end
},
['bid-percent'] = {
input_type = 'number',
validator = function(pct)
return function(auction_record)
return history.value(auction_record.item_key) and auction_record.unit_bid_price / history.value(auction_record.item_key) * 100 <= pct
end
end
},
['percent'] = {
input_type = 'number',
validator = function(pct)
return function(auction_record)
return auction_record.unit_buyout_price > 0
and history.value(auction_record.item_key)
and auction_record.unit_buyout_price / history.value(auction_record.item_key) * 100 <= pct
end
end
},
['bid-profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
return history.value(auction_record.item_key) and history.value(auction_record.item_key) * auction_record.aux_quantity - auction_record.bid_price >= amount
end
end
},
['profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
return auction_record.buyout_price > 0 and history.value(auction_record.item_key) and history.value(auction_record.item_key) * auction_record.aux_quantity - auction_record.buyout_price >= amount
end
end
},
['bid-disenchant-profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
local disenchant_value = disenchant.value(auction_record.slot, auction_record.quality, auction_record.level)
return disenchant_value and disenchant_value - auction_record.bid_price >= amount
end
end
},
['disenchant-profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
local disenchant_value = disenchant.value(auction_record.slot, auction_record.quality, auction_record.level)
return auction_record.buyout_price > 0 and disenchant_value and disenchant_value - auction_record.buyout_price >= amount
end
end
},
['bid-vendor-profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
local vendor_price = info.merchant_info(auction_record.item_id)
return vendor_price and vendor_price * auction_record.aux_quantity - auction_record.bid_price >= amount
end
end
},
['vendor-profit'] = {
input_type = 'money',
validator = function(amount)
return function(auction_record)
local vendor_price = info.merchant_info(auction_record.item_id)
return auction_record.buyout_price > 0 and vendor_price and vendor_price * auction_record.aux_quantity - auction_record.buyout_price >= amount
end
end
},
['seller'] = {
input_type = 'string',
validator = function(name)
return function(auction_record)
return auction_record.owner and strupper(name) == strupper(auction_record.owner)
end
end
},
}
function operator(str)
local operator = str == 'not' and T.list('operator', 'not', 1)
for name in T.temp-T.set('and', 'or') do
local arity = aux.select(3, strfind(str, '^' .. name .. '(%d*)$'))
if arity then
arity = tonumber(arity)
operator = not (arity and arity < 2) and T.list('operator', name, arity)
end
end
return operator or nil
end
do
local mt = {
__call = function(self, str, i)
if not str then
self.max_level = self.max_level or self.min_level
return self
end
if self.exact then return end
local number = tonumber(aux.select(3, strfind(str, '^(%d+)$')))
if number then
if number >= 1 and number <= 60 then
for _, key in ipairs(T.temp-T.list('min_level', 'max_level')) do
if not self[key] then
self[key] = T.list(str, number)
return T.list('blizzard', key, str, number)
end
end
end
end
for _, parser in T.temp-T.list(
T.temp-T.list('class', info.item_class_index),
T.temp-T.list('subclass', T.vararg-function(arg) return info.item_subclass_index(aux.index(self.class, 2) or 0, unpack(arg)) end),
T.temp-T.list('slot', T.vararg-function(arg) return info.item_slot_index(aux.index(self.class, 2) == 2 and 2 or 0, aux.index(self.subclass, 2) or 0, unpack(arg)) end),
T.temp-T.list('quality', info.item_quality_index)
) do
if not self[parser[1]] then
tinsert(parser, str)
local index, label = parser[2](aux.select(3, unpack(parser)))
if index then
self[parser[1]] = T.list(label, index)
return T.list('blizzard', parser[1], label, index)
end
end
end
if not self[str] and (str == 'usable' or str == 'exact' and self.name and aux.size(self) == 1) then
self[str] = T.list(str, 1)
return T.list('blizzard', str, str, 1)
elseif i == 1 and strlen(str) <= 63 then
self.name = unquote(str)
return T.list('blizzard', 'name', unquote(str), str)
-- return nil, 'The name filter must not be longer than 63 characters' TODO
end
end,
}
function blizzard_filter_parser()
return setmetatable(T.acquire(), mt)
end
end
function parse_parameter(input_type, str)
if input_type == 'money' then
local money = money.from_string(str)
return money and money > 0 and money or nil
elseif input_type == 'number' then
local number = tonumber(str)
return number and number > 0 and mod(number, 1) == 0 and number or nil
elseif input_type == 'string' then
return str ~= '' and str or nil
elseif type(input_type) == 'table' then
return aux.key(input_type, str)
end
end
function M.parse_filter_string(str)
local filter, post_filter = T.acquire(), T.acquire()
local blizzard_filter_parser = blizzard_filter_parser()
local parts = str and aux.map(aux.split(str, '/'), function(part) return strlower(aux.trim(part)) end) or T.acquire()
local i = 1
while parts[i] do
local operator = operator(parts[i])
if operator then
tinsert(post_filter, operator)
tinsert(filter, operator)
elseif filters[parts[i]] then
local input_type = filters[parts[i]].input_type
if input_type ~= '' then
if not parts[i + 1] or not parse_parameter(input_type, parts[i + 1]) then
if parts[i] == 'item' then
return nil, 'Invalid item name', aux.account_data.auctionable_items
elseif type(input_type) == 'table' then
return nil, 'Invalid choice for ' .. parts[i], input_type
else
return nil, 'Invalid input for ' .. parts[i] .. '. Expecting: ' .. input_type
end
end
tinsert(post_filter, T.list('filter', parts[i], parts[i + 1]))
i = i + 1
else
tinsert(post_filter, T.list('filter', parts[i]))
end
tinsert(filter, post_filter[getn(post_filter)])
else
local part = blizzard_filter_parser(parts[i], i)
if part then
tinsert(filter, part)
elseif parts[i] ~= '' then
tinsert(post_filter, T.list('filter', 'tooltip', parts[i]))
tinsert(filter, post_filter[getn(post_filter)])
else
return nil, 'Empty modifier'
end
end
i = i + 1
end
return T.map('components', filter, 'blizzard', blizzard_filter_parser(), 'post', post_filter)
end
function M.query(filter_string)
local filter, error, suggestions = parse_filter_string(filter_string)
if not filter then
return nil, suggestions or T.acquire(), error
end
local polish_notation_counter = 0
for _, component in ipairs(filter.post) do
if component[1] == 'operator' then
polish_notation_counter = max(polish_notation_counter, 1)
polish_notation_counter = polish_notation_counter + (tonumber(component[2]) or 1) - 1
elseif component[1] == 'filter' then
polish_notation_counter = polish_notation_counter - 1
end
end
if polish_notation_counter > 0 then
local suggestions = T.acquire()
for key in filters do
tinsert(suggestions, strlower(key))
end
tinsert(suggestions, 'and')
tinsert(suggestions, 'or')
tinsert(suggestions, 'not')
return nil, suggestions, 'Malformed expression'
end
return {
blizzard_query = blizzard_query(filter),
validator = validator(filter),
prettified = prettified_filter_string(filter),
}, _M.suggestions(filter)
end
function M.queries(filter_string)
local parts = aux.split(filter_string, ';')
local queries = T.acquire()
for _, str in ipairs(parts) do
local query, _, error = query(str)
if not query then
return nil, error
else
tinsert(queries, query)
end
end
return queries
end
function suggestions(filter)
local suggestions = T.acquire()
if filter.blizzard.name and aux.size(filter.blizzard) == 1 then tinsert(suggestions, 'exact') end
tinsert(suggestions, 'and'); tinsert(suggestions, 'or'); tinsert(suggestions, 'not'); tinsert(suggestions, 'tooltip')
for key in filters do tinsert(suggestions, key) end
-- classes
if not filter.blizzard.class then
for _, class in ipairs(T.temp-T.list(GetAuctionItemClasses())) do tinsert(suggestions, class) end
end
-- subclasses
if not filter.blizzard.subclass then
for _, subclass in ipairs(T.temp-T.list(GetAuctionItemSubClasses(aux.index(filter.blizzard.class, 2) or 0))) do
tinsert(suggestions, subclass)
end
end
-- slots
if not filter.blizzard.slot then
for _, invtype in ipairs(T.temp-T.list(GetAuctionInvTypes(aux.index(filter.blizzard.class, 2) == 2 and 2 or 0, aux.index(filter.blizzard.subclass, 2) or 0))) do
tinsert(suggestions, _G[invtype])
end
end
-- usable
if not filter.blizzard.usable then tinsert(suggestions, 'usable') end
-- rarities
if not filter.blizzard.quality then
for i = 0, 4 do tinsert(suggestions, _G['ITEM_QUALITY' .. i .. '_DESC']) end
end
-- item names
if getn(filter.components) == 0 then
for _, name in ipairs(aux.account_data.auctionable_items) do
tinsert(suggestions, name .. '/exact')
end
end
return suggestions
end
function M.filter_string(components)
local query_builder = query_builder()
for _, component in ipairs(components) do
if component[1] == 'blizzard' then
query_builder.append(component[4] or component[3])
elseif component[1] == 'operator' then
query_builder.append(component[2] .. (component[2] ~= 'not' and tonumber(component[3]) or ''))
elseif component[1] == 'filter' then
query_builder.append(component[2])
local parameter = component[3]
if parameter then
if filter_util.filters[component[2]].input_type == 'money' then
parameter = money.to_string(money.from_string(parameter), nil, true, nil, true)
end
query_builder.append(parameter)
end
end
end
return query_builder.get()
end
function prettified_filter_string(filter)
local prettified = query_builder()
for i, component in ipairs(filter.components) do
if component[1] == 'blizzard' then
if component[2] == 'name' then
if filter.blizzard.exact then
prettified.append(info.display_name(info.item_id(component[3])) or aux.color.orange('[' .. component[3] .. ']'))
elseif component[3] ~= '' then
prettified.append(aux.color.label.enabled(component[3]))
end
elseif component[2] ~= 'exact' then
prettified.append(aux.color.orange(component[3]))
end
elseif component[1] == 'operator' then
prettified.append(aux.color.orange(component[2] .. (component[2] ~= 'not' and tonumber(component[3]) or '')))
elseif component[1] == 'filter' then
if i == 1 or component[2] ~= 'tooltip' then
prettified.append(aux.color.orange(component[2]))
end
local parameter = component[3]
if parameter then
if component[2] == 'item' then
prettified.append(info.display_name(info.item_id(parameter)) or aux.color.label.enabled('[' .. parameter .. ']'))
else
if filters[component[2]].input_type == 'money' then
prettified.append(money.to_string(money.from_string(parameter), nil, true, aux.color.label.enabled))
else
prettified.append(aux.color.label.enabled(parameter))
end
end
end
end
end
if prettified.get() == '' then
return aux.color.orange'<>'
else
return prettified.get()
end
end
function M.quote(name)
return '<' .. name .. '>'
end
function M.unquote(name)
return aux.select(3, strfind(name, '^<(.*)>$')) or name
end
function blizzard_query(filter)
local filters = filter.blizzard
local query = T.map('name', filters.name)
local item_id = filters.name and info.item_id(filters.name)
local item_info = item_id and info.item(item_id)
if filters.exact and item_info then
local class_index = info.item_class_index(item_info.class)
local subclass_index = info.item_subclass_index(class_index or 0, item_info.subclass)
local slot_index = info.item_slot_index(class_index or 0, subclass_index or 0, item_info.slot)
query.min_level = item_info.level
query.max_level = item_info.level
query.usable = item_info.usable
query.class = class_index
query.subclass = subclass_index
query.slot = slot_index
query.quality = item_info.quality
else
for key in T.temp-T.set('min_level', 'max_level', 'class', 'subclass', 'slot', 'usable', 'quality') do
query[key] = aux.index(filters[key], 2)
end
end
return query
end
function validator(filter)
local validators = T.acquire()
for i, component in filter.post do
local type, name, param = unpack(component)
if type == 'filter' then
validators[i] = filters[name].validator(parse_parameter(filters[name].input_type, param))
end
end
return function(record)
if filter.blizzard.exact and strlower(info.item(record.item_id).name) ~= filter.blizzard.name then
return false
end
local stack = T.temp-T.acquire()
for i = getn(filter.post), 1, -1 do
local type, name, param = unpack(filter.post[i])
if type == 'operator' then
local args = T.temp-T.acquire()
while (not param or param > 0) and getn(stack) > 0 do
tinsert(args, tremove(stack))
param = param and param - 1
end
if name == 'not' then
tinsert(stack, not args[1])
elseif name == 'and' then
tinsert(stack, aux.all(args))
elseif name == 'or' then
tinsert(stack, aux.any(args))
end
elseif type == 'filter' then
tinsert(stack, not not validators[i](record))
end
end
return aux.all(stack)
end
end
function M.query_builder()
local filter
return T.map(
'append', function(part)
filter = not filter and part or filter .. '/' .. part
end,
'get', function()
return filter or ''
end
)
end
+443
View File
@@ -0,0 +1,443 @@
module 'aux.util.info'
local T = require 'T'
local aux = require 'aux'
CreateFrame('GameTooltip', 'AuxTooltip', nil, 'GameTooltipTemplate')
AuxTooltip:SetScript('OnTooltipAddMoney', function()
this.money = arg1
end)
do
local inventory_index_map = {
INVTYPE_AMMO = {0},
INVTYPE_HEAD = {1},
INVTYPE_NECK = {2},
INVTYPE_SHOULDER = {3},
INVTYPE_BODY = {4},
INVTYPE_CHEST = {5},
INVTYPE_ROBE = {5},
INVTYPE_WAIST = {6},
INVTYPE_LEGS = {7},
INVTYPE_FEET = {8},
INVTYPE_WRIST = {9},
INVTYPE_HAND = {10},
INVTYPE_FINGER = {11, 12},
INVTYPE_TRINKET = {13, 14},
INVTYPE_CLOAK = {15},
INVTYPE_2HWEAPON = {16, 17},
INVTYPE_WEAPONMAINHAND = {16, 17},
INVTYPE_WEAPON = {16, 17},
INVTYPE_WEAPONOFFHAND = {16, 17},
INVTYPE_HOLDABLE = {16, 17},
INVTYPE_SHIELD = {16, 17},
INVTYPE_RANGED = {18},
INVTYPE_RANGEDRIGHT = {18},
INVTYPE_TABARD = {19},
}
function M.inventory_index(slot)
return unpack(inventory_index_map[slot] or T.temp-T.acquire())
end
end
function M.container_item(bag, slot)
local link = GetContainerItemLink(bag, slot)
if link then
local item_id, suffix_id, unique_id, enchant_id = parse_link(link)
local item_info = T.temp-item(item_id, suffix_id, unique_id, enchant_id)
local texture, count, locked, quality, readable, lootable = GetContainerItemInfo(bag, slot) -- quality not working?
local tooltip, tooltip_money = tooltip('bag', bag, slot)
local max_charges = max_item_charges(item_id)
local charges = max_charges and item_charges(tooltip)
local aux_quantity = charges or count
return T.map(
'item_id', item_id,
'suffix_id', suffix_id,
'unique_id', unique_id,
'enchant_id', enchant_id,
'link', link,
'itemstring', item_info.itemstring,
'item_key', item_id .. ':' .. suffix_id,
'name', item_info.name,
'texture', texture,
'level', item_info.level,
'type', item_info.type,
'subtype', item_info.subtype,
'slot', item_info.slot,
'quality', item_info.quality,
'max_stack', item_info.max_stack,
'count', count,
'locked', locked,
'readable', readable,
'lootable', lootable,
'tooltip', tooltip,
'tooltip_money', tooltip_money,
'max_charges', max_charges,
'charges', charges,
'aux_quantity', aux_quantity
)
end
end
function M.auction_sell_item()
for name, texture, count, quality, usable, vendor_price in GetAuctionSellItemInfo do
return T.map(
'name', name,
'texture', texture,
'quality', quality,
'count', count,
'usable', usable,
'vendor_price', vendor_price -- it seems for charge items this is always the price for full charges
)
end
end
function M.auction(index, query_type)
query_type = query_type or 'list'
local link = GetAuctionItemLink(query_type, index)
if link then
local item_id, suffix_id, unique_id, enchant_id = parse_link(link)
local item_info = T.temp-item(item_id, suffix_id, unique_id, enchant_id)
local name, texture, count, quality, usable, level, start_price, min_increment, buyout_price, high_bid, high_bidder, owner, sale_status = GetAuctionItemInfo(query_type, index)
local duration = GetAuctionItemTimeLeft(query_type, index)
local tooltip, tooltip_money = tooltip('auction', query_type, index)
local max_charges = max_item_charges(item_id)
local charges = max_charges and item_charges(tooltip)
local aux_quantity = charges or count
local blizzard_bid = high_bid > 0 and high_bid or start_price
local bid_price = high_bid > 0 and (high_bid + min_increment) or start_price
return T.map(
'item_id', item_id,
'suffix_id', suffix_id,
'unique_id', unique_id,
'enchant_id', enchant_id,
'link', link,
'itemstring', item_info.itemstring,
'item_key', item_id .. ':' .. suffix_id,
'search_signature', aux.join(T.temp-T.list(item_id, suffix_id, enchant_id, start_price, buyout_price, bid_price, aux_quantity, duration, query_type == 'owner' and high_bidder or (high_bidder and 1 or 0), aux.account_data.ignore_owner and (is_player(owner) and 0 or 1) or (owner or '?')), ':'),
'sniping_signature', aux.join(T.temp-T.list(item_id, suffix_id, enchant_id, start_price, buyout_price, aux_quantity, aux.account_data.ignore_owner and (is_player(owner) and 0 or 1) or (owner or '?')), ':'),
'name', name,
'texture', texture,
'level', item_info.level,
'type', item_info.type,
'subtype', item_info.subtype,
'slot', item_info.slot,
'quality', quality,
'max_stack', item_info.max_stack,
'count', count,
'start_price', start_price,
'high_bid', high_bid,
'min_increment', min_increment,
'blizzard_bid', blizzard_bid,
'bid_price', bid_price,
'buyout_price', buyout_price,
'unit_blizzard_bid', blizzard_bid / aux_quantity,
'unit_bid_price', bid_price / aux_quantity,
'unit_buyout_price', buyout_price / aux_quantity,
'high_bidder', high_bidder,
'owner', owner,
'sale_status', sale_status,
'duration', duration,
'usable', usable,
'tooltip', tooltip,
'tooltip_money', tooltip_money,
'max_charges', max_charges,
'charges', charges,
'aux_quantity', aux_quantity
)
end
end
function M.bid_update(auction_record)
auction_record.high_bid = auction_record.bid_price
auction_record.blizzard_bid = auction_record.bid_price
auction_record.min_increment = max(1, floor(auction_record.bid_price / 100) * 5)
auction_record.bid_price = auction_record.bid_price + auction_record.min_increment
auction_record.unit_blizzard_bid = auction_record.blizzard_bid / auction_record.aux_quantity
auction_record.unit_bid_price = auction_record.bid_price / auction_record.aux_quantity
auction_record.high_bidder = 1
auction_record.search_signature = aux.join(T.temp-T.list(auction_record.item_id, auction_record.suffix_id, auction_record.enchant_id, auction_record.start_price, auction_record.buyout_price, auction_record.bid_price, auction_record.aux_quantity, auction_record.duration, 1, aux.account_data.ignore_owner and (is_player(auction_record.owner) and 0 or 1) or (auction_record.owner or '?')), ':')
end
function M.set_tooltip(itemstring, owner, anchor)
GameTooltip:SetOwner(owner, anchor)
GameTooltip:SetHyperlink(itemstring)
end
function M.set_shopping_tooltip(slot)
local index1, index2 = inventory_index(slot)
local tooltips = T.temp-T.acquire()
if index1 then
local tooltip = tooltip('inventory', 'player', index1)
if getn(tooltip) > 0 then
tinsert(tooltips, tooltip)
end
end
if index2 then
local tooltip = tooltip('inventory', 'player', index2)
if getn(tooltip) > 0 then
tinsert(tooltips, tooltip)
end
end
if tooltips[1] then
tinsert(tooltips[1], 1, T.temp-T.map('left_text', 'Currently Equipped', 'left_color', T.temp-T.list(.5, .5, .5)))
ShoppingTooltip1:SetOwner(GameTooltip, 'ANCHOR_NONE')
ShoppingTooltip1:SetPoint('TOPLEFT', GameTooltip, 'TOPRIGHT', 0, -10)
load_tooltip(ShoppingTooltip1, tooltips[1])
end
if tooltips[2] then
tinsert(tooltips[2], 1, T.temp-T.map('left_text', 'Currently Equipped', 'left_color', T.temp-T.list(.5, .5, .5)))
ShoppingTooltip2:SetOwner(ShoppingTooltip1, 'ANCHOR_NONE')
ShoppingTooltip2:SetPoint('TOPLEFT', ShoppingTooltip1, 'TOPRIGHT')
load_tooltip(ShoppingTooltip2, tooltips[2])
end
end
function M.tooltip_match(entry, tooltip)
return aux.any(tooltip, function(line)
local left_match = line.left_text and strupper(line.left_text) == strupper(entry)
local right_match = line.right_text and strupper(line.right_text) == strupper(entry)
return left_match or right_match
end)
end
function M.tooltip_find(pattern, tooltip)
local count = 0
for _, line in tooltip do
if line.left_text and strfind(line.left_text, pattern) then
count = count + 1
end
if line.right_text and strfind(line.right_text, pattern) then
count = count + 1
end
end
return count
end
function M.load_tooltip(frame, tooltip)
frame:ClearLines()
for _, line in ipairs(tooltip) do
if line.right_text then
frame:AddDoubleLine(line.left_text, line.right_text, line.left_color[1], line.left_color[2], line.left_color[3], line.right_color[1], line.right_color[2], line.right_color[3])
else
frame:AddLine(line.left_text, line.left_color[1], line.left_color[2], line.left_color[3], true)
end
end
for i = 1, getn(tooltip) do -- TODO why is this needed?
_G[frame:GetName() .. 'TextLeft' .. i]:SetJustifyH('LEFT')
_G[frame:GetName() .. 'TextRight' .. i]:SetJustifyH('LEFT')
end
frame:Show()
end
function M.display_name(item_id, no_brackets, no_color)
local item_info = item(item_id)
if item_info then
local name = item_info.name
if not no_brackets then
name = '[' .. name .. ']'
end
if not no_color then
name = aux.select(4, GetItemQualityColor(item_info.quality)) .. name .. FONT_COLOR_CODE_CLOSE
end
return name
end
end
function M.auctionable(tooltip, quality, strict)
local status = tooltip[2] and tooltip[2].left_text
local durability, max_durability = durability(tooltip)
return (not quality or quality < 6)
and status ~= ITEM_BIND_ON_PICKUP
and status ~= ITEM_BIND_QUEST
and status ~= ITEM_SOULBOUND
and (not tooltip_match(ITEM_CONJURED, tooltip) or tooltip_find(ITEM_MIN_LEVEL, tooltip) > 1)
and not (strict and durability and durability < max_durability)
end
function M.tooltip(setter, arg1, arg2)
AuxTooltip:SetOwner(UIParent, 'ANCHOR_NONE')
AuxTooltip.money = 0
if setter == 'auction' then
AuxTooltip:SetAuctionItem(arg1, arg2)
elseif setter == 'bag' then
AuxTooltip:SetBagItem(arg1, arg2)
elseif setter == 'inventory' then
AuxTooltip:SetInventoryItem(arg1, arg2)
elseif setter == 'link' then
AuxTooltip:SetHyperlink(arg1)
end
local tooltip = T.acquire()
for i = 1, AuxTooltip:NumLines() do
tinsert(tooltip, T.map(
'left_text', _G['AuxTooltipTextLeft' .. i]:GetText(),
'left_color', T.list(_G['AuxTooltipTextLeft' .. i]:GetTextColor()),
'right_text', _G['AuxTooltipTextRight' .. i]:IsVisible() and _G['AuxTooltipTextRight' .. i]:GetText(),
'right_color', T.list(_G['AuxTooltipTextRight' .. i]:GetTextColor())
))
end
return tooltip, AuxTooltip.money
end
do
local pattern = '^' .. gsub(gsub(ITEM_SPELL_CHARGES_P1, '%%d', '(%%d+)'), '%%%d+%$d', '(%%d+)') .. '$'
function item_charges(tooltip)
for _, line in tooltip do
local _, _, left_charges_string = strfind(line.left_text or '', pattern)
local _, _, right_charges_string = strfind(line.right_text or '', pattern)
local charges = tonumber(left_charges_string) or tonumber(right_charges_string)
if charges then
return max(1, charges) -- TODO kronos bug? should never be 0
end
end
return 1
end
end
do
local data = {
-- wizard oil
[20744] = 5,
[20746] = 5,
[20750] = 5,
[20749] = 5,
-- mana oil
[20745] = 5,
[20747] = 5,
[20748] = 5,
-- discombobulator
[4388] = 5,
-- recombobulator
[4381] = 10,
[18637] = 10,
-- deflector
[4376] = 5,
[4386] = 5,
-- ... TODO
}
function M.max_item_charges(item_id)
return data[item_id]
end
end
do
local pattern = '^' .. gsub(gsub(DURABILITY_TEMPLATE, '%%d', '(%%d+)'), '%%%d+%$d', '(%%d+)') .. '$'
function M.durability(tooltip)
for _, line in tooltip do
local _, _, left_durability_string, left_max_durability_string = strfind(line.left_text or '', pattern)
local _, _, right_durability_string, right_max_durability_string = strfind(line.right_text or '', pattern)
local durability = tonumber(left_durability_string) or tonumber(right_durability_string)
local max_durability = tonumber(left_max_durability_string) or tonumber(right_max_durability_string)
if durability then
return durability, max_durability
end
end
end
end
function M.item_key(link)
local item_id, suffix_id = parse_link(link)
return item_id .. ':' .. suffix_id
end
function M.parse_link(link)
local _, _, item_id, enchant_id, suffix_id, unique_id, name = strfind(link, '|c%x%x%x%x%x%x%x%x|Hitem:(%d*):(%d*):(%d*):(%d*)[:0-9]*|h%[(.-)%]|h|r')
return tonumber(item_id) or 0, tonumber(suffix_id) or 0, tonumber(unique_id) or 0, tonumber(enchant_id) or 0, name
end
function M.itemstring(item_id, suffix_id, unique_id, enchant_id)
return 'item:' .. (item_id or 0) .. ':' .. (enchant_id or 0) .. ':' .. (suffix_id or 0) .. ':' .. (unique_id or 0)
end
function M.item(item_id, suffix_id)
local itemstring = 'item:' .. (item_id or 0) .. ':0:' .. (suffix_id or 0) .. ':0'
local name, itemstring, quality, level, class, subclass, max_stack, slot, texture = GetItemInfo(itemstring)
return name and T.map(
'name', name,
'itemstring', itemstring,
'quality', quality,
'level', level,
'class', class,
'subclass', subclass,
'slot', slot,
'max_stack', max_stack,
'texture', texture
) or item_info(item_id)
end
function M.item_class_index(item_class)
for i, class in T.temp-T.list(GetAuctionItemClasses()) do
if strupper(class) == strupper(item_class) then
return i, class
end
end
end
function M.item_subclass_index(class_index, item_subclass)
for i, subclass in T.temp-T.list(GetAuctionItemSubClasses(class_index)) do
if strupper(subclass) == strupper(item_subclass) then
return i, subclass
end
end
end
function M.item_slot_index(class_index, subclass_index, slot_name)
for i, slot in T.temp-T.list(GetAuctionInvTypes(class_index, subclass_index)) do
if strupper(_G[slot]) == strupper(slot_name) then
return i, _G[slot]
end
end
end
function M.item_quality_index(item_quality)
for i = 0, 4 do
local quality = _G['ITEM_QUALITY' .. i .. '_DESC']
if strupper(item_quality) == strupper(quality) then
return i, quality
end
end
end
function M.inventory()
local bag, slot = 0, 0
return function()
if not GetBagName(bag) or slot >= GetContainerNumSlots(bag) then
repeat bag = bag + 1 until GetBagName(bag) or bag > 4
slot = 1
else
slot = slot + 1
end
if bag <= 4 then return T.list(bag, slot), bag_type(bag) end
end
end
function M.bag_type(bag)
if bag == 0 then return 1 end
local link = GetInventoryItemLink('player', ContainerIDToInventoryID(bag))
if link then
local item_id = parse_link(link)
local item_info = item(item_id)
return item_subclass_index(3, item_info.subclass)
end
end
+131
View File
@@ -0,0 +1,131 @@
module 'aux.util.money'
local T = require 'T'
local aux = require 'aux'
M.GOLD_TEXT = '|cffffd70ag|r'
M.SILVER_TEXT = '|cffc7c7cfs|r'
M.COPPER_TEXT = '|cffeda55fc|r'
local COPPER_PER_GOLD = 10000
local COPPER_PER_SILVER = 100
function M.to_gsc(money)
local gold = floor(money / COPPER_PER_GOLD)
local silver = floor(mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER)
local copper = mod(money, COPPER_PER_SILVER)
return gold, silver, copper
end
function M.from_gsc(gold, silver, copper)
return gold * COPPER_PER_GOLD + silver * COPPER_PER_SILVER + copper
end
function M.to_string2(money, exact, color)
color = color or FONT_COLOR_CODE_CLOSE
local TEXT_NONE = '0'
local GOLD = 'ffd100'
local SILVER = 'e6e6e6'
local COPPER = 'c8602c'
local START = '|cff%s%d' .. FONT_COLOR_CODE_CLOSE
local PART = color .. '.|cff%s%02d' .. FONT_COLOR_CODE_CLOSE
local NONE = '|cffa0a0a0' .. TEXT_NONE .. FONT_COLOR_CODE_CLOSE
if not exact and money >= COPPER_PER_GOLD then
money = aux.round(money / COPPER_PER_SILVER) * COPPER_PER_SILVER
end
local g, s, c = to_gsc(money)
local str = ''
local fmt = START
if g > 0 then
str = str .. format(fmt, GOLD, g)
fmt = PART
end
if s > 0 or c > 0 then
str = str .. format(fmt, SILVER, s)
fmt = PART
end
if c > 0 then
str = str .. format(fmt, COPPER, c)
end
if str == '' then
str = NONE
end
return str
end
function M.to_string(money, pad, trim, color, no_color)
local is_negative = money < 0
money = abs(money)
local gold, silver, copper = to_gsc(money)
local gold_text, silver_text, copper_text
if no_color then
gold_text, silver_text, copper_text = 'g', 's', 'c'
else
gold_text, silver_text, copper_text = GOLD_TEXT, SILVER_TEXT, COPPER_TEXT
end
local text
if trim then
local parts = T.temp-T.acquire()
if gold > 0 then
tinsert(parts, format_number(gold, false, color) .. gold_text)
end
if silver > 0 then
tinsert(parts, format_number(silver, pad, color) .. silver_text)
end
if copper > 0 or gold == 0 and silver == 0 then
tinsert(parts, format_number(copper, pad, color) .. copper_text)
end
text = aux.join(parts, ' ')
else
if gold > 0 then
text = format_number(gold, false, color) .. gold_text .. ' ' .. format_number(silver, pad, color) .. silver_text .. ' ' .. format_number(copper, pad, color) .. copper_text
elseif silver > 0 then
text = format_number(silver, false, color) .. silver_text .. ' ' .. format_number(copper, pad, color) .. copper_text
else
text = format_number(copper, false, color) .. copper_text
end
end
if is_negative then
text = (color and color'-' or '-') .. text
end
return text
end
function M.from_string(value)
local number = tonumber(value)
if number and number >= 0 then
return number * COPPER_PER_GOLD
end
value = gsub(gsub(strlower(value), '|c%x%x%x%x%x%x%x%x', ''), FONT_COLOR_CODE_CLOSE, '')
local gold = tonumber(aux.select(3, strfind(value, '(%d*%.?%d+)g')))
local silver = tonumber(aux.select(3, strfind(value, '(%d*%.?%d+)s')))
local copper = tonumber(aux.select(3, strfind(value, '(%d*%.?%d+)c')))
if not gold and not silver and not copper then return end
value = gsub(value, '%d*%.?%d+g', '', 1)
value = gsub(value, '%d*%.?%d+s', '', 1)
value = gsub(value, '%d*%.?%d+c', '', 1)
if strfind(value, '%S') then return end
return from_gsc(gold or 0, silver or 0, copper or 0)
end
function M.format_number(num, pad, color)
num = format('%0' .. (pad and 2 or 0) .. 'd', num)
if color then
return color .. num .. FONT_COLOR_CODE_CLOSE
else
return num
end
end
+75
View File
@@ -0,0 +1,75 @@
module 'aux.util.persistence'
local T = require 'T'
local aux = require 'aux'
function M.read(schema, str)
if schema == 'string' then
return str
elseif schema == 'boolean' then
return str == '1'
elseif schema == 'number' then
return tonumber(str)
elseif type(schema) == 'table' and schema[1] == 'list' then
return T.temp-read_list(schema, str)
elseif type(schema) == 'table' and schema[1] == 'tuple' then
return T.temp-read_tuple(schema, str)
else
error('Invalid schema.', 2)
end
end
function M.write(schema, obj)
if schema == 'string' then
return obj or ''
elseif schema == 'boolean' then
return obj and '1' or '0'
elseif schema == 'number' then
return obj and tostring(obj) or ''
elseif type(schema) == 'table' and schema[1] == 'list' then
return write_list(schema, obj)
elseif type(schema) == 'table' and schema[1] == 'tuple' then
return write_tuple(schema, obj)
else
error('Invalid schema.', 2)
end
end
function read_list(schema, str)
if str == '' then return T.acquire() end
local separator = schema[2]
local element_type = schema[3]
return aux.map(aux.split(str, separator), function(part)
return read(element_type, part)
end)
end
function write_list(schema, list)
local separator = schema[2]
local element_type = schema[3]
local parts = aux.map(T.temp-aux.copy(list), function(element)
return write(element_type, element)
end)
return aux.join(parts, separator)
end
function read_tuple(schema, str)
local separator = schema[2]
local tuple = T.acquire()
local parts = T.temp-aux.split(str, separator)
for i = 3, getn(schema) do
local key, type = next(schema[i])
tuple[key] = read(type, parts[i - 2])
end
return tuple
end
function write_tuple(schema, tuple)
local separator = schema[2]
local parts = T.temp-T.acquire()
for i = 3, getn(schema) do
local key, type = next(schema[i])
tinsert(parts, write(type, tuple[key]))
end
return aux.join(parts, separator)
end
+82
View File
@@ -0,0 +1,82 @@
module 'aux.util.scan'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local filter_util = require 'aux.util.filter'
local scan = require 'aux.core.scan'
function M.test(record, index)
local auction_record = T.temp-info.auction(index, record.query_type)
return auction_record and auction_record.search_signature == record.search_signature
end
function M.find(auction_record, status_bar, on_abort, on_failure, on_success)
local queries = T.list(T.acquire())
if auction_record.blizzard_query then
local blizzard_query1 = aux.copy(auction_record.blizzard_query)
blizzard_query1.first_page = auction_record.page
blizzard_query1.last_page = auction_record.page
tinsert(queries, T.map('blizzard_query', blizzard_query1))
if auction_record.page > 0 then
local blizzard_query2 = aux.copy(auction_record.blizzard_query)
blizzard_query2.first_page = auction_record.page - 1
blizzard_query2.last_page = auction_record.page - 1
tinsert(queries, T.map('blizzard_query', blizzard_query2))
end
local item_query = item_query(auction_record.item_id, 0, 0)
if not aux.eq(auction_record.blizzard_query, item_query.blizzard_query) then
tinsert(queries, item_query)
end
end
local found
return scan.start{
type = auction_record.query_type,
queries = queries,
on_scan_start = function()
status_bar:update_status(0, 0)
status_bar:set_text('Searching auction...')
end,
on_start_query = function(query_index)
status_bar:update_status((query_index - 1) / getn(queries), 0)
end,
on_auction = function(record)
if test(auction_record, record.index) then
found = true
scan.stop()
status_bar:update_status(1, 1)
status_bar:set_text('Auction found')
return on_success(record.index)
end
end,
on_abort = function()
if not found then
status_bar:update_status(1, 1)
status_bar:set_text('Auction not found')
return on_abort()
end
end,
on_complete = function()
if not found then
status_bar:update_status(1, 1)
status_bar:set_text('Auction not found')
return on_failure()
end
end,
}
end
function M.item_query(item_id, first_page, last_page)
local item_info = info.item(item_id)
if item_info then
local query = filter_util.query(item_info.name .. '/exact')
query.blizzard_query.first_page = first_page
query.blizzard_query.last_page = last_page
return T.map('validator', query.validator, 'blizzard_query', query.blizzard_query)
end
end
+30
View File
@@ -0,0 +1,30 @@
module 'aux.util.sort'
local T = require 'T'
M.EQ = 0
M.LT = 1
M.GT = 2
function M.compare(a, b, desc)
if a < b then
return desc and GT or LT
elseif a > b then
return desc and LT or GT
else
return EQ
end
end
M.multi_lt = T.vararg-function(arg)
for i = 1, getn(arg), 2 do
if arg[i] and arg[i + 1] and arg[i] ~= arg[i + 1] then
return arg[i] < arg[i + 1]
elseif not arg[i] and arg[i + 1] then
return true
elseif not arg[i + 1] then
return false
end
end
return false
end