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
+211
View File
@@ -0,0 +1,211 @@
module 'aux.util.info'
local T = require 'T'
local aux = require 'aux'
local persistence = require 'aux.util.persistence'
local MIN_ITEM_ID = 1
local MAX_ITEM_ID = 30000
local items_schema = {'tuple', '#', {name='string'}, {quality='number'}, {level='number'}, {class='string'}, {subclass='string'}, {slot='string'}, {max_stack='number'}, {texture='string'}}
local merchant_buy_schema = {'tuple', '#', {unit_price='number'}, {limited='boolean'}}
function aux.handle.LOAD()
scan_wdb()
aux.event_listener('MERCHANT_SHOW', on_merchant_show)
aux.event_listener('MERCHANT_CLOSED', on_merchant_closed)
aux.event_listener('MERCHANT_UPDATE', on_merchant_update)
aux.event_listener('BAG_UPDATE', on_bag_update)
CreateFrame('Frame', nil, MerchantFrame):SetScript('OnUpdate', merchant_on_update)
aux.event_listener('NEW_AUCTION_UPDATE', function()
local data = auction_sell_item()
if data then
local item_id = item_id(data.name)
if item_id then
aux.account_data.merchant_sell[item_id] = data.vendor_price / (max_item_charges(item_id) or data.count)
end
end
end)
end
do
local characters = {}
function M.is_player(name)
return not not characters[name]
end
function aux.handle.LOAD()
characters = aux.realm_data.characters
for k, v in characters do
if GetTime() > v + 60 * 60 * 24 * 30 then
characters[k] = nil
end
end
characters[UnitName'player'] = GetTime()
end
end
do
local sell_scan_queued, incomplete_buy_data
function on_merchant_show()
merchant_sell_scan()
incomplete_buy_data = not merchant_buy_scan()
end
function on_merchant_closed()
sell_scan_queued = nil
incomplete_buy_data = false
end
function on_merchant_update()
if incomplete_buy_data then
incomplete_buy_data = not merchant_buy_scan()
end
end
function on_bag_update()
if MerchantFrame:IsVisible() then
sell_scan_queued = true
end
end
function merchant_on_update()
if sell_scan_queued then
sell_scan_queued = nil
merchant_sell_scan()
end
end
end
function merchant_loaded()
for i = 1, GetMerchantNumItems() do
if not GetMerchantItemLink(i) then
return false
end
end
return true
end
function M.merchant_info(item_id)
local buy_info
if aux.account_data.merchant_buy[item_id] then
buy_info = persistence.read(merchant_buy_schema, aux.account_data.merchant_buy[item_id])
end
return aux.account_data.merchant_sell[item_id], buy_info and buy_info.unit_price, buy_info and buy_info.limited
end
function M.item_info(item_id)
local data_string = aux.account_data.items[item_id]
if data_string then
local cached_data = persistence.read(items_schema, data_string)
return T.map(
'name', cached_data.name,
'itemstring', 'item:' .. item_id .. ':0:0:0',
'quality', cached_data.quality,
'level', cached_data.level,
'class', cached_data.class,
'subclass', cached_data.subclass,
'slot', cached_data.slot,
'max_stack', cached_data.max_stack,
'texture', cached_data.texture
)
end
end
function M.item_id(item_name)
return aux.account_data.item_ids[strlower(item_name)]
end
function merchant_buy_scan()
local incomplete_data
for i = 1, GetMerchantNumItems() do
local _, _, price, count, stock = GetMerchantItemInfo(i)
local link = GetMerchantItemLink(i)
if link then
local item_id = parse_link(link)
local new_unit_price, new_limited = price / count, stock >= 0
if aux.account_data.merchant_buy[item_id] then
local buy_info = persistence.read(merchant_buy_schema, aux.account_data.merchant_buy[item_id])
local unit_price
if buy_info.limited and not new_limited then
unit_price = new_unit_price
elseif new_limited and not buy_info.limited then
unit_price = buy_info.unit_price
else
unit_price = min(buy_info.unit_price, new_unit_price)
end
aux.account_data.merchant_buy[item_id] = persistence.write(merchant_buy_schema, T.temp-T.map(
'unit_price', unit_price,
'limited', buy_info.limited and new_limited
))
else
aux.account_data.merchant_buy[item_id] = persistence.write(merchant_buy_schema, T.temp-T.map(
'unit_price', new_unit_price,
'limited', new_limited
))
end
else
incomplete_data = true
end
end
return not incomplete_data
end
function merchant_sell_scan()
for slot in inventory() do
T.temp(slot)
local item_info = T.temp-container_item(unpack(slot))
if item_info then
aux.account_data.merchant_sell[item_info.item_id] = item_info.tooltip_money / item_info.aux_quantity
end
end
end
function scan_wdb(item_id)
item_id = item_id or MIN_ITEM_ID
local processed = 0
while processed <= 100 and item_id <= MAX_ITEM_ID do
local itemstring = 'item:' .. item_id
local name, _, quality, level, class, subclass, max_stack, slot, texture = GetItemInfo(itemstring)
if name and not aux.account_data.item_ids[strlower(name)] then
aux.account_data.item_ids[strlower(name)] = item_id
aux.account_data.items[item_id] = persistence.write(items_schema, T.temp-T.map(
'name', name,
'quality', quality,
'level', level,
'class', class,
'subclass', subclass,
'slot', slot,
'max_stack', max_stack,
'texture', texture
))
local tooltip = tooltip('link', itemstring)
if auctionable(tooltip, quality) then
tinsert(aux.account_data.auctionable_items, strlower(name))
end
processed = processed + 1
end
item_id = item_id + 1
end
if item_id <= MAX_ITEM_ID then
aux.thread(aux.when, aux.later(.5), scan_wdb, item_id)
else
sort(aux.account_data.auctionable_items, function(a, b) return strlen(a) < strlen(b) or (strlen(a) == strlen(b) and a < b) end)
end
end
function M.populate_wdb(item_id)
item_id = item_id or MIN_ITEM_ID
if item_id > MAX_ITEM_ID then
aux.print('Cache populated.')
return
end
if not GetItemInfo('item:' .. item_id) then
aux.print('Fetching item ' .. item_id .. '.')
AuxTooltip:SetHyperlink('item:' .. item_id)
end
aux.thread(populate_wdb, item_id + 1)
end
+99
View File
@@ -0,0 +1,99 @@
module 'aux.core.crafting'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local money = require 'aux.util.money'
local history = require 'aux.core.history'
local search_tab = require 'aux.tabs.search'
function aux.handle.LOAD()
if not aux.account_data.crafting_cost then
return
end
aux.event_listener('ADDON_LOADED', function()
if arg1 == 'Blizzard_CraftUI' then
craft_ui_loaded()
elseif arg1 == 'Blizzard_TradeSkillUI' then
trade_skill_ui_loaded()
end
end)
end
do
local function cost_label(cost)
local label = LIGHTYELLOW_FONT_COLOR_CODE .. '(Total Cost: ' .. FONT_COLOR_CODE_CLOSE
label = label .. (cost and money.to_string2(cost, nil, LIGHTYELLOW_FONT_COLOR_CODE) or GRAY_FONT_COLOR_CODE .. '?' .. FONT_COLOR_CODE_CLOSE)
label = label .. LIGHTYELLOW_FONT_COLOR_CODE .. ')' .. FONT_COLOR_CODE_CLOSE
return label
end
local function hook_quest_item(f)
f:SetScript('OnMouseUp', function()
if arg1 == 'RightButton' then
if aux.get_tab() then
aux.set_tab(1)
search_tab.set_filter(_G[this:GetName() .. 'Name']:GetText() .. '/exact')
search_tab.execute(nil, false)
end
end
end)
end
function craft_ui_loaded()
aux.hook('CraftFrame_SetSelection', T.vararg-function(arg)
local ret = T.temp-T.list(aux.orig.CraftFrame_SetSelection(unpack(arg)))
local id = GetCraftSelectionIndex()
local total_cost = 0
for i = 1, GetCraftNumReagents(id) do
local link = GetCraftReagentItemLink(id, i)
if not link then
total_cost = nil
break
end
local item_id, suffix_id = info.parse_link(link)
local count = aux.select(3, GetCraftReagentInfo(id, i))
local _, price, limited = info.merchant_info(item_id)
local value = price and not limited and price or history.value(item_id .. ':' .. suffix_id)
if not value then
total_cost = nil
break
else
total_cost = total_cost + value * count
end
end
CraftReagentLabel:SetText(SPELL_REAGENTS .. ' ' .. cost_label(total_cost))
return unpack(ret)
end)
for i = 1, 8 do
hook_quest_item(_G['CraftReagent' .. i])
end
end
function trade_skill_ui_loaded()
aux.hook('TradeSkillFrame_SetSelection', T.vararg-function(arg)
local ret = T.temp-T.list(aux.orig.TradeSkillFrame_SetSelection(unpack(arg)))
local id = GetTradeSkillSelectionIndex()
local total_cost = 0
for i = 1, GetTradeSkillNumReagents(id) do
local link = GetTradeSkillReagentItemLink(id, i)
if not link then
total_cost = nil
break
end
local item_id, suffix_id = info.parse_link(link)
local count = aux.select(3, GetTradeSkillReagentInfo(id, i))
local _, price, limited = info.merchant_info(item_id)
local value = price and not limited and price or history.value(item_id .. ':' .. suffix_id)
if not value then
total_cost = nil
break
else
total_cost = total_cost + value * count
end
end
TradeSkillReagentLabel:SetText(SPELL_REAGENTS .. ' ' .. cost_label(total_cost))
return unpack(ret)
end)
for i = 1, 8 do
hook_quest_item(_G['TradeSkillReagent' .. i])
end
end
end
+163
View File
@@ -0,0 +1,163 @@
module 'aux.core.disenchant'
local T = require 'T'
local history = require 'aux.core.history'
local UNCOMMON, RARE, EPIC = 2, 3, 4
local ARMOR = T.set(
'INVTYPE_HEAD',
'INVTYPE_NECK',
'INVTYPE_SHOULDER',
'INVTYPE_BODY',
'INVTYPE_CHEST',
'INVTYPE_ROBE',
'INVTYPE_WAIST',
'INVTYPE_LEGS',
'INVTYPE_FEET',
'INVTYPE_WRIST',
'INVTYPE_HAND',
'INVTYPE_FINGER',
'INVTYPE_TRINKET',
'INVTYPE_CLOAK',
'INVTYPE_HOLDABLE'
)
local WEAPON = T.set(
'INVTYPE_2HWEAPON',
'INVTYPE_WEAPONMAINHAND',
'INVTYPE_WEAPON',
'INVTYPE_WEAPONOFFHAND',
'INVTYPE_SHIELD',
'INVTYPE_RANGED',
'INVTYPE_RANGEDRIGHT'
)
function M.value(slot, quality, level)
local expectation
for _, event in distribution(slot, quality, level) do
local value = history.value(event.item_id .. ':' .. 0)
if not value then
return
else
expectation = (expectation or 0) + event.probability * (event.min_quantity + event.max_quantity) / 2 * value
end
end
return expectation
end
function M.distribution(slot, quality, level)
if not ARMOR[slot] and not WEAPON[slot] or level == 0 then
return T.acquire()
end
local function p(probability_armor, probability_weapon)
if ARMOR[slot] then
return probability_armor
elseif WEAPON[slot] then
return probability_weapon
end
end
if quality == UNCOMMON then
if level <= 10 then
return T.temp-T.list(
T.temp-T.map('item_id', 10940, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.8, .2)),
T.temp-T.map('item_id', 10938, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .8))
)
elseif level <= 15 then
return T.temp-T.list(
T.temp-T.map('item_id', 10940, 'min_quantity', 2, 'max_quantity', 3, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 10939, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 10978, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 20 then
return T.temp-T.list(
T.temp-T.map('item_id', 10940, 'min_quantity', 4, 'max_quantity', 6, 'probability', p(.75, .15)),
T.temp-T.map('item_id', 10998, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.15, .75)),
T.temp-T.map('item_id', 10978, 'min_quantity', 1, 'max_quantity', 1, 'probability', .10)
)
elseif level <= 25 then
return T.temp-T.list(
T.temp-T.map('item_id', 11083, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 11082, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 11084, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 30 then
return T.temp-T.list(
T.temp-T.map('item_id', 11083, 'min_quantity', 2, 'max_quantity', 5, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 11134, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 11138, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 35 then
return T.temp-T.list(
T.temp-T.map('item_id', 11137, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 11135, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 11139, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 40 then
return T.temp-T.list(
T.temp-T.map('item_id', 11137, 'min_quantity', 2, 'max_quantity', 5, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 11174, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 11177, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 45 then
return T.temp-T.list(
T.temp-T.map('item_id', 11176, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.75, .2)),
T.temp-T.map('item_id', 11175, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 11178, 'min_quantity', 1, 'max_quantity', 1, 'probability', .05)
)
elseif level <= 50 then
return T.temp-T.list(
T.temp-T.map('item_id', 11176, 'min_quantity', 2, 'max_quantity', 5, 'probability', p(.75, .22)),
T.temp-T.map('item_id', 16202, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 14343, 'min_quantity', 1, 'max_quantity', 1, 'probability', p(.05, .03))
)
elseif level <= 55 then
return T.temp-T.list(
T.temp-T.map('item_id', 16204, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.75, .22)),
T.temp-T.map('item_id', 16203, 'min_quantity', 1, 'max_quantity', 2, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 14344, 'min_quantity', 1, 'max_quantity', 1, 'probability', p(.05, .03))
)
elseif level <= 60 then
return T.temp-T.list(
T.temp-T.map('item_id', 16204, 'min_quantity', 2, 'max_quantity', 5, 'probability', p(.75, .22)),
T.temp-T.map('item_id', 16203, 'min_quantity', 2, 'max_quantity', 3, 'probability', p(.2, .75)),
T.temp-T.map('item_id', 14344, 'min_quantity', 1, 'max_quantity', 1, 'probability', p(.05, .03))
)
end
elseif quality == RARE then
if level <= 20 then
return T.temp-T.list(T.temp-T.map('item_id', 10978, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 25 then
return T.temp-T.list(T.temp-T.map('item_id', 11084, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 30 then
return T.temp-T.list(T.temp-T.map('item_id', 11138, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 35 then
return T.temp-T.list(T.temp-T.map('item_id', 11139, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 40 then
return T.temp-T.list(T.temp-T.map('item_id', 11177, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 45 then
return T.temp-T.list(T.temp-T.map('item_id', 11178, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 50 then
return T.temp-T.list(T.temp-T.map('item_id', 14343, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 55 then
return T.temp-T.list(T.temp-T.map('item_id', 14344, 'min_quantity', 1, 'max_quantity', 1, 'probability', .995), T.temp-T.map('item_id', 20725, 'min_quantity', 1, 'max_quantity', 1, 'probability', .005))
elseif level <= 60 then
return T.temp-T.list(T.temp-T.map('item_id', 14344, 'min_quantity', 1, 'max_quantity', 1, 'probability', .995), T.temp-T.map('item_id', 20725, 'min_quantity', 1, 'max_quantity', 1, 'probability', .005))
end
elseif quality == EPIC then
if level <= 40 then
return T.temp-T.list(T.temp-T.map('item_id', 11177, 'min_quantity', 2, 'max_quantity', 4, 'probability', 1))
elseif level <= 45 then
return T.temp-T.list(T.temp-T.map('item_id', 11178, 'min_quantity', 2, 'max_quantity', 4, 'probability', 1))
elseif level <= 50 then
return T.temp-T.list(T.temp-T.map('item_id', 14343, 'min_quantity', 2, 'max_quantity', 4, 'probability', 1))
elseif level <= 55 then
return T.temp-T.list(T.temp-T.map('item_id', 20725, 'min_quantity', 1, 'max_quantity', 1, 'probability', 1))
elseif level <= 60 then
return T.temp-T.list(T.temp-T.map('item_id', 20725, 'min_quantity', 1, 'max_quantity', 2, 'probability', 1))
end
end
return T.acquire()
end
+109
View File
@@ -0,0 +1,109 @@
module 'aux.core.history'
local T = require 'T'
local aux = require 'aux'
local persistence = require 'aux.util.persistence'
local history_schema = {'tuple', '#', {next_push='number'}, {daily_min_buyout='number'}, {data_points={'list', ';', {'tuple', '@', {value='number'}, {time='number'}}}}}
local value_cache = {}
function aux.handle.LOAD2()
data = aux.faction_data.history
end
do
local next_push = 0
function get_next_push()
if time() > next_push then
local date = date('*t')
date.hour, date.min, date.sec = 24, 0, 0
next_push = time(date)
end
return next_push
end
end
function new_record()
return T.temp-T.map('next_push', get_next_push(), 'data_points', T.acquire())
end
function read_record(item_key)
local record = data[item_key] and persistence.read(history_schema, data[item_key]) or new_record()
if record.next_push <= time() then
push_record(record)
write_record(item_key, record)
end
return record
end
function write_record(item_key, record)
data[item_key] = persistence.write(history_schema, record)
if value_cache[item_key] then
T.release(value_cache[item_key])
value_cache[item_key] = nil
end
end
function M.process_auction(auction_record)
local item_record = read_record(auction_record.item_key)
local unit_buyout_price = ceil(auction_record.buyout_price / auction_record.aux_quantity)
if unit_buyout_price > 0 and unit_buyout_price < (item_record.daily_min_buyout or aux.huge) then
item_record.daily_min_buyout = unit_buyout_price
write_record(auction_record.item_key, item_record)
end
end
function M.data_points(item_key)
return read_record(item_key).data_points
end
function M.value(item_key)
if not value_cache[item_key] or value_cache[item_key].next_push <= time() then
local item_record, value
item_record = read_record(item_key)
if getn(item_record.data_points) > 0 then
local total_weight, weighted_values = 0, T.temp-T.acquire()
for _, data_point in item_record.data_points do
local weight = .99 ^ aux.round((item_record.data_points[1].time - data_point.time) / (60 * 60 * 24))
total_weight = total_weight + weight
tinsert(weighted_values, T.map('value', data_point.value, 'weight', weight))
end
for _, weighted_value in weighted_values do
weighted_value.weight = weighted_value.weight / total_weight
end
value = weighted_median(weighted_values)
else
value = item_record.daily_min_buyout
end
value_cache[item_key] = T.map('value', value, 'next_push', item_record.next_push)
end
return value_cache[item_key].value
end
function M.market_value(item_key)
return read_record(item_key).daily_min_buyout
end
function weighted_median(list)
sort(list, function(a,b) return a.value < b.value end)
local weight = 0
for _, v in ipairs(list) do
weight = weight + v.weight
if weight >= .5 then
return v.value
end
end
end
function push_record(item_record)
if item_record.daily_min_buyout then
tinsert(item_record.data_points, 1, T.map('value', item_record.daily_min_buyout, 'time', item_record.next_push))
while getn(item_record.data_points) > 11 do
T.release(item_record.data_points[getn(item_record.data_points)])
tremove(item_record.data_points)
end
end
item_record.next_push, item_record.daily_min_buyout = get_next_push(), nil
end
+165
View File
@@ -0,0 +1,165 @@
module 'aux.core.post'
local aux = require 'aux'
local info = require 'aux.util.info'
local stack = require 'aux.core.stack'
local history = require 'aux.core.history'
local disenchant = require 'aux.core.disenchant'
local state
function aux.handle.CLOSE()
stop()
end
function process()
if state.posted < state.count then
local stacking_complete
local send_signal, signal_received = aux.signal()
aux.when(signal_received, function()
local slot = signal_received()[1]
if slot then
return post_auction(slot, process)
else
return stop()
end
end)
return stack.start(state.item_key, state.stack_size, send_signal)
end
return stop()
end
function post_auction2(slot, k)
local item_info = info.container_item(unpack(slot))
if item_info.item_key == state.item_key and info.auctionable(item_info.tooltip, nil, true) and item_info.aux_quantity == state.stack_size then
ClearCursor()
ClickAuctionSellItemButton()
ClearCursor()
PickupContainerItem(unpack(slot))
ClickAuctionSellItemButton()
ClearCursor()
StartAuction(max(1, aux.round(state.unit_start_price * item_info.aux_quantity)), aux.round(state.unit_buyout_price * item_info.aux_quantity), state.duration)
local send_signal, signal_received = aux.signal()
aux.when(signal_received, function()
state.posted = state.posted + 1
return k()
end)
local posted
aux.event_listener('CHAT_MSG_SYSTEM', function(kill)
if arg1 == ERR_AUCTION_STARTED then
send_signal()
kill()
end
end)
else
return stop()
end
end
function post_auction(slot, k)
local item_info = info.container_item(unpack(slot))
if item_info.item_key == state.item_key and info.auctionable(item_info.tooltip, nil, true) and item_info.aux_quantity == state.stack_size then
ClearCursor()
ClickAuctionSellItemButton()
ClearCursor()
PickupContainerItem(unpack(slot))
ClickAuctionSellItemButton()
ClearCursor()
local start_price = state.unit_start_price
local buyout_price = state.unit_buyout_price
local kz_undercut = 0
if start_price == 0 then
if history.market_value(state.item_key) ~= nil then
start_price = max(start_price,0.93*tonumber(history.market_value(state.item_key)))
buyout_price = max(buyout_price,0.99*tonumber(history.market_value(state.item_key)))
kz_undercut = 1
end
if disenchant.value(item_info.slot, item_info.quality, item_info.level) ~= nil then
buyout_price = max(buyout_price,0.80* tonumber(disenchant.value(item_info.slot, item_info.quality, item_info.level)))
end
if aux.account_data.merchant_sell[item_info.item_id] ~= nil then
start_price = max(start_price,tonumber(aux.account_data.merchant_sell[item_info.item_id]) * 1.35)
buyout_price = max(buyout_price,start_price)
if kz_undercut == 0 then
start_price = max(start_price,tonumber(aux.account_data.merchant_sell[item_info.item_id]) * (1.35+3.65*math.exp(-(1/4000)* tonumber(aux.account_data.merchant_sell[item_info.item_id] ))))
buyout_price = max(buyout_price,start_price)
end
end
if history.value(state.item_key) ~= nil then
start_price = max(start_price,0.89*tonumber(history.value(state.item_key)))
buyout_price = max(buyout_price,0.89*tonumber(history.value(state.item_key)))
if kz_undercut == 0 then
buyout_price = max(buyout_price,1.0* tonumber(history.value(state.item_key)))
end
end
start_price = max(start_price,0.75 * buyout_price)
end
StartAuction(max(1, aux.round(start_price * item_info.aux_quantity)), aux.round(buyout_price * item_info.aux_quantity), state.duration)
local send_signal, signal_received = aux.signal()
aux.when(signal_received, function()
state.posted = state.posted + 1
return k()
end)
local posted
aux.event_listener('CHAT_MSG_SYSTEM', function(kill)
if arg1 == ERR_AUCTION_STARTED then
send_signal()
kill()
end
end)
else
return stop()
end
end
function M.stop()
if state then
aux.kill_thread(state.thread_id)
local callback = state.callback
local posted = state.posted
state = nil
if callback then
callback(posted)
end
end
end
function M.start(item_key, stack_size, duration, unit_start_price, unit_buyout_price, count, callback)
stop()
state = {
thread_id = aux.thread(process),
item_key = item_key,
stack_size = stack_size,
duration = duration,
unit_start_price = unit_start_price,
unit_buyout_price = unit_buyout_price,
count = count,
posted = 0,
callback = callback,
}
end
+236
View File
@@ -0,0 +1,236 @@
module 'aux.core.scan'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local history = require 'aux.core.history'
local PAGE_SIZE = 50
function aux.handle.CLOSE()
abort()
end
do
local scan_states = {}
function M.start(params)
local old_state = scan_states[params.type]
if old_state then
abort(old_state.id)
end
do (params.on_scan_start or pass)() end
local thread_id = aux.thread(scan)
scan_states[params.type] = {
id = thread_id,
params = params,
}
return thread_id
end
function M.abort(scan_id)
local aborted = T.acquire()
for type, state in scan_states do
if not scan_id or state.id == scan_id then
aux.kill_thread(state.id)
scan_states[type] = nil
tinsert(aborted, state)
end
end
for _, state in aborted do
do (state.params.on_abort or pass)() end
end
end
function M.stop()
get_state().stopped = true
end
function complete()
local on_complete = get_state().params.on_complete
scan_states[get_state().params.type] = nil
do (on_complete or pass)() end
end
function get_state()
for _, state in scan_states do
if state.id == aux.thread_id() then
return state
end
end
end
end
function get_query()
return get_state().params.queries[get_state().query_index]
end
function total_pages(total_auctions)
return ceil(total_auctions / PAGE_SIZE)
end
function last_page(total_auctions)
local last_page = max(total_pages(total_auctions) - 1, 0)
local last_page_limit = get_query().blizzard_query.last_page or last_page
return min(last_page_limit, last_page)
end
function scan()
get_state().query_index = get_state().query_index and get_state().query_index + 1 or 1
if get_query() and not get_state().stopped then
do (get_state().params.on_start_query or pass)(get_state().query_index) end
if get_query().blizzard_query then
if (get_query().blizzard_query.first_page or 0) <= (get_query().blizzard_query.last_page or aux.huge) then
get_state().page = get_query().blizzard_query.first_page or 0
return submit_query()
end
else
get_state().page = nil
return scan_page()
end
end
return complete()
end
do
local function submit()
if get_state().params.type == 'bidder' then
GetBidderAuctionItems(get_state().page)
elseif get_state().params.type == 'owner' then
GetOwnerAuctionItems(get_state().page)
else
get_state().last_list_query = GetTime()
local blizzard_query = get_query().blizzard_query or T.acquire()
QueryAuctionItems(
blizzard_query.name,
blizzard_query.min_level,
blizzard_query.max_level,
blizzard_query.slot,
blizzard_query.class,
blizzard_query.subclass,
get_state().page,
blizzard_query.usable,
blizzard_query.quality
)
end
return wait_for_results()
end
function submit_query()
if get_state().stopped then return end
if get_state().params.type ~= 'list' then
return submit()
else
return aux.when(CanSendAuctionQuery, submit)
end
end
end
function scan_page(i)
i = i or 1
if i > PAGE_SIZE then
do (get_state().params.on_page_scanned or pass)() end
if get_query().blizzard_query and get_state().page < last_page(get_state().total_auctions) then
get_state().page = get_state().page + 1
return submit_query()
else
return scan()
end
end
local auction_info = info.auction(i, get_state().params.type)
if auction_info and (auction_info.owner or get_state().params.ignore_owner or aux.account_data.ignore_owner) then
auction_info.index = i
auction_info.page = get_state().page
auction_info.blizzard_query = get_query().blizzard_query
auction_info.query_type = get_state().params.type
history.process_auction(auction_info)
if (get_state().params.auto_bid_validator or pass)(auction_info) then
local send_signal, signal_received = aux.signal()
aux.when(signal_received, scan_page, i)
return aux.place_bid(auction_info.query_type, auction_info.index, auction_info.bid_price, send_signal)
elseif (get_state().params.auto_buy_validator or pass)(auction_info) then
local send_signal, signal_received = aux.signal()
aux.when(signal_received, scan_page, i)
return aux.place_bid(auction_info.query_type, auction_info.index, auction_info.buyout_price, send_signal)
elseif not get_query().validator or get_query().validator(auction_info) then
do (get_state().params.on_auction or pass)(auction_info) end
end
end
return scan_page(i + 1)
end
function wait_for_results()
if get_state().params.type == 'bidder' then
return aux.when(function() return aux.bids_loaded() end, accept_results)
elseif get_state().params.type == 'owner' then
return wait_for_owner_results()
elseif get_state().params.type == 'list' then
return wait_for_list_results()
end
end
function accept_results()
_, get_state().total_auctions = GetNumAuctionItems(get_state().params.type)
do
(get_state().params.on_page_loaded or pass)(
get_state().page - (get_query().blizzard_query.first_page or 0) + 1,
last_page(get_state().total_auctions) - (get_query().blizzard_query.first_page or 0) + 1,
total_pages(get_state().total_auctions) - 1
)
end
return scan_page()
end
function wait_for_owner_results()
if get_state().page == aux.current_owner_page() then
return accept_results()
else
local updated
aux.on_next_event('AUCTION_OWNED_LIST_UPDATE', function() updated = true end)
return aux.when(function() return updated end, accept_results)
end
end
function wait_for_list_results()
local updated, last_update
local listener_id = aux.event_listener('AUCTION_ITEM_LIST_UPDATE', function()
last_update = GetTime()
updated = true
end)
local timeout = aux.later(5, get_state().last_list_query)
local ignore_owner = get_state().params.ignore_owner or aux.account_data.ignore_owner
return aux.when(function()
if not last_update and timeout() then
return true
end
if last_update and GetTime() - last_update > 5 then
return true
end
-- short circuiting order important, owner_data_complete must be called iif an update has happened.
if updated and (ignore_owner or owner_data_complete()) then
return true
end
updated = false
end, function()
aux.kill_listener(listener_id)
if not last_update and timeout() then
return submit_query()
else
return accept_results()
end
end)
end
function owner_data_complete()
for i = 1, PAGE_SIZE do
local auction_info = info.auction(i, 'list')
if auction_info and not auction_info.owner then
return false
end
end
return true
end
+31
View File
@@ -0,0 +1,31 @@
module 'aux.core.shortcut'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
do
local orig = SetItemRef
_G.SetItemRef = T.vararg-function(arg)
if arg[3] ~= 'RightButton' or not aux.index(aux.get_tab(), 'CLICK_LINK') or not strfind(arg[1], '^item:%d+') then
return orig(unpack(arg))
end
local item_info = info.item(tonumber(aux.select(3, strfind(arg[1], '^item:(%d+)'))))
if item_info then
return aux.get_tab().CLICK_LINK(item_info)
end
end
end
do
local orig = UseContainerItem
_G.UseContainerItem = T.vararg-function(arg)
if aux.modified() or not aux.get_tab() then
return orig(unpack(arg))
end
local item_info = info.container_item(arg[1], arg[2])
if item_info and aux.get_tab().USE_ITEM then
aux.get_tab().USE_ITEM(item_info)
end
end
end
+74
View File
@@ -0,0 +1,74 @@
module 'aux.core.slash'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local post = require 'aux.tabs.post'
function status(enabled)
return (enabled and aux.color.green'on' or aux.color.red'off')
end
_G.SLASH_AUX1 = '/aux'
function SlashCmdList.AUX(command)
if not command then return end
local arguments = aux.tokenize(command)
local tooltip_settings = aux.character_data.tooltip
if arguments[1] == 'scale' and tonumber(arguments[2]) then
local scale = tonumber(arguments[2])
aux.frame:SetScale(scale)
aux.account_data.scale = scale
elseif arguments[1] == 'ignore' and arguments[2] == 'owner' then
aux.account_data.ignore_owner = not aux.account_data.ignore_owner
aux.print('ignore owner ' .. status(aux.account_data.ignore_owner))
elseif arguments[1] == 'post' and arguments[2] == 'bid' then
aux.account_data.post_bid = not aux.account_data.post_bid
aux.print('post bid ' .. status(aux.account_data.post_bid))
elseif arguments[1] == 'post' and arguments[2] == 'duration' and T.map('2', post.DURATION_2, '8', post.DURATION_8, '24', post.DURATION_24)[arguments[3]] then
aux.account_data.post_duration = T.map('2', post.DURATION_2, '8', post.DURATION_8, '24', post.DURATION_24)[arguments[3]]
aux.print('post duration ' .. aux.color.blue(aux.account_data.post_duration / 60 .. 'h'))
elseif arguments[1] == 'crafting' and arguments[2] == 'cost' then
aux.account_data.crafting_cost = not aux.account_data.crafting_cost
aux.print('crafting cost ' .. status(aux.account_data.crafting_cost))
elseif arguments[1] == 'tooltip' and arguments[2] == 'value' then
tooltip_settings.value = not tooltip_settings.value
aux.print('tooltip value ' .. status(tooltip_settings.value))
elseif arguments[1] == 'tooltip' and arguments[2] == 'daily' then
tooltip_settings.daily = not tooltip_settings.daily
aux.print('tooltip daily ' .. status(tooltip_settings.daily))
elseif arguments[1] == 'tooltip' and arguments[2] == 'merchant' and arguments[3] == 'buy' then
tooltip_settings.merchant_buy = not tooltip_settings.merchant_buy
aux.print('tooltip merchant buy ' .. status(tooltip_settings.merchant_buy))
elseif arguments[1] == 'tooltip' and arguments[2] == 'merchant' and arguments[3] == 'sell' then
tooltip_settings.merchant_sell = not tooltip_settings.merchant_sell
aux.print('tooltip merchant sell ' .. status(tooltip_settings.merchant_sell))
elseif arguments[1] == 'tooltip' and arguments[2] == 'disenchant' and arguments[3] == 'value' then
tooltip_settings.disenchant_value = not tooltip_settings.disenchant_value
aux.print('tooltip disenchant value ' .. status(tooltip_settings.disenchant_value))
elseif arguments[1] == 'tooltip' and arguments[2] == 'disenchant' and arguments[3] == 'distribution' then
tooltip_settings.disenchant_distribution = not tooltip_settings.disenchant_distribution
aux.print('tooltip disenchant distribution ' .. status(tooltip_settings.disenchant_distribution))
elseif arguments[1] == 'clear' and arguments[2] == 'item' and arguments[3] == 'cache' then
aux.account_data.items = {}
aux.account_data.item_ids = {}
aux.account_data.auctionable_items = {}
aux.print('Item cache cleared.')
elseif arguments[1] == 'populate' and arguments[2] == 'wdb' then
info.populate_wdb()
else
aux.print('Usage:')
aux.print('- scale [' .. aux.color.blue(aux.account_data.scale) .. ']')
aux.print('- ignore owner [' .. status(aux.account_data.ignore_owner) .. ']')
aux.print('- post bid [' .. status(aux.account_data.post_bid) .. ']')
aux.print('- post duration [' .. aux.color.blue(aux.account_data.post_duration / 60 .. 'h') .. ']')
aux.print('- crafting cost [' .. status(aux.account_data.crafting_cost) .. ']')
aux.print('- tooltip value [' .. status(tooltip_settings.value) .. ']')
aux.print('- tooltip daily [' .. status(tooltip_settings.daily) .. ']')
aux.print('- tooltip merchant buy [' .. status(tooltip_settings.merchant_buy) .. ']')
aux.print('- tooltip merchant sell [' .. status(tooltip_settings.merchant_sell) .. ']')
aux.print('- tooltip disenchant value [' .. status(tooltip_settings.disenchant_value) .. ']')
aux.print('- tooltip disenchant distribution [' .. status(tooltip_settings.disenchant_distribution) .. ']')
aux.print('- clear item cache')
aux.print('- populate wdb')
end
end
+130
View File
@@ -0,0 +1,130 @@
module 'aux.core.stack'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local state
function aux.handle.CLOSE()
stop()
end
function stack_size(slot)
local container_item_info = T.temp-info.container_item(unpack(slot))
return container_item_info and container_item_info.count or 0
end
function charges(slot)
local container_item_info = T.temp-info.container_item(unpack(slot))
return container_item_info and container_item_info.charges
end
function max_stack(slot)
local container_item_info = T.temp-info.container_item(unpack(slot))
return container_item_info and container_item_info.max_stack
end
function locked(slot)
local container_item_info = T.temp-info.container_item(unpack(slot))
return container_item_info and container_item_info.locked
end
function find_item_slot(partial)
for slot in info.inventory() do
if matching_item(slot, partial) and not aux.eq(slot, state.target_slot) then
return slot
end
end
end
function matching_item(slot, partial)
local item_info = T.temp-info.container_item(unpack(slot))
return item_info and item_info.item_key == state.item_key and info.auctionable(item_info.tooltip, nil, true) and (not partial or item_info.count < item_info.max_stack)
end
function find_empty_slot()
for slot, type in info.inventory() do
if type == 1 and not GetContainerItemInfo(unpack(slot)) then
return slot
end
end
end
function find_charge_item_slot()
for slot in info.inventory() do
if matching_item(slot) and charges(slot) == state.target_size then
return slot
end
end
end
function move_item(from_slot, to_slot, amount, k)
if locked(from_slot) or locked(to_slot) then
return aux.wait(k)
end
amount = min(max_stack(from_slot) - stack_size(to_slot), stack_size(from_slot), amount)
local expected_size = stack_size(to_slot) + amount
ClearCursor()
SplitContainerItem(from_slot[1], from_slot[2], amount)
PickupContainerItem(unpack(to_slot))
return aux.when(function() return stack_size(to_slot) == expected_size end, k)
end
function process()
if not state.target_slot or not matching_item(state.target_slot) then
state.target_slot = find_item_slot()
if not state.target_slot then
return stop()
end
end
if charges(state.target_slot) then
state.target_slot = find_charge_item_slot()
return stop()
end
if stack_size(state.target_slot) > state.target_size then
local slot = find_item_slot(true) or find_empty_slot()
if slot then
return move_item(
state.target_slot,
slot,
stack_size(state.target_slot) - state.target_size,
process
)
end
elseif stack_size(state.target_slot) < state.target_size then
local slot = find_item_slot()
if slot then
return move_item(
slot,
state.target_slot,
state.target_size - stack_size(state.target_slot),
process
)
end
end
return stop()
end
function M.stop()
if state then
aux.kill_thread(state.thread_id)
local callback, slot = state.callback, state.target_slot
slot = slot and matching_item(slot) and slot or nil
state = nil
do (callback or pass)(slot) end
end
end
function M.start(item_key, size, callback)
stop()
state = {
thread_id = aux.thread(process),
item_key = item_key,
target_size = size,
callback = callback,
}
end
+216
View File
@@ -0,0 +1,216 @@
module 'aux.core.tooltip'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local money = require 'aux.util.money'
local disenchant = require 'aux.core.disenchant'
local history = require 'aux.core.history'
local gui = require 'aux.gui'
local UNKNOWN = GRAY_FONT_COLOR_CODE .. '?' .. FONT_COLOR_CODE_CLOSE
local game_tooltip_hooks, game_tooltip_money = {}, 0
function aux.handle.LOAD()
settings = aux.character_data.tooltip
do
local inside_hook = false
for name, f in game_tooltip_hooks do
local name, f = name, f
aux.hook(name, GameTooltip, T.vararg-function(arg)
game_tooltip_money = 0
inside_hook = true
local tmp = T.list(aux.orig[GameTooltip][name](unpack(arg)))
inside_hook = false
f(unpack(arg))
return T.unpack(tmp)
end)
end
SetTooltipMoney = SetTooltipMoney
_G.SetTooltipMoney = T.vararg-function(arg)
if inside_hook then
game_tooltip_money = arg[2]
else
return SetTooltipMoney(unpack(arg))
end
end
end
local orig = SetItemRef
setglobal('SetItemRef', T.vararg-function(arg)
local name, _, quality = GetItemInfo(arg[1])
local tmp = T.list(orig(unpack(arg)))
if not IsShiftKeyDown() and not IsControlKeyDown() and name then
local color_code = aux.select(4, GetItemQualityColor(quality))
local link = color_code .. '|H' .. arg[1] .. '|h[' .. name .. ']|h' .. FONT_COLOR_CODE_CLOSE
extend_tooltip(ItemRefTooltip, link, 1)
end
return T.unpack(tmp)
end)
end
function M.extend_tooltip(tooltip, link, quantity)
local item_id, suffix_id = info.parse_link(link)
quantity = IsShiftKeyDown() and quantity or 1
local item_info = T.temp-info.item(item_id)
if item_info then
local distribution = disenchant.distribution(item_info.slot, item_info.quality, item_info.level)
if getn(distribution) > 0 then
if settings.disenchant_distribution then
tooltip:AddLine('Disenchants into:', aux.color.tooltip.disenchant.distribution())
sort(distribution, function(a,b) return a.probability > b.probability end)
for _, event in ipairs(distribution) do
tooltip:AddLine(format(' %s%% %s (%s-%s)', event.probability * 100, info.display_name(event.item_id, true) or 'item:' .. event.item_id, event.min_quantity, event.max_quantity), aux.color.tooltip.disenchant.distribution())
end
end
if settings.disenchant_value then
local disenchant_value = disenchant.value(item_info.slot, item_info.quality, item_info.level)
tooltip:AddLine('Disenchant: ' .. (disenchant_value and money.to_string2(disenchant_value) or UNKNOWN), aux.color.tooltip.disenchant.value())
end
end
end
if settings.merchant_buy then
local _, price, limited = info.merchant_info(item_id)
if price then
tooltip:AddLine('Vendor Buy ' .. (limited and '(limited): ' or ': ') .. money.to_string2(price * quantity), aux.color.tooltip.merchant())
end
end
if settings.merchant_sell then
local price = info.merchant_info(item_id)
if price ~= 0 then
tooltip:AddLine('Vendor: ' .. (price and money.to_string2(price * quantity) or UNKNOWN), aux.color.tooltip.merchant())
end
end
local auctionable = not item_info or info.auctionable(T.temp-info.tooltip('link', item_info.itemstring), item_info.quality)
local item_key = (item_id or 0) .. ':' .. (suffix_id or 0)
local value = history.value(item_key)
if auctionable then
if settings.value then
tooltip:AddLine('Value: ' .. (value and money.to_string2(value * quantity) or UNKNOWN), aux.color.tooltip.value())
end
if settings.daily then
local market_value = history.market_value(item_key)
tooltip:AddLine('Today: ' .. (market_value and money.to_string2(market_value * quantity) .. ' (' .. gui.percentage_historical(aux.round(market_value / value * 100)) .. ')' or UNKNOWN), aux.color.tooltip.value())
end
end
if tooltip == GameTooltip and game_tooltip_money > 0 then
SetTooltipMoney(tooltip, game_tooltip_money)
end
tooltip:Show()
end
function game_tooltip_hooks:SetHyperlink(itemstring)
local name, _, quality = GetItemInfo(itemstring)
if name then
local hex = aux.select(4, GetItemQualityColor(quality))
local link = hex .. '|H' .. itemstring .. '|h[' .. name .. ']|h' .. FONT_COLOR_CODE_CLOSE
extend_tooltip(GameTooltip, link, 1)
end
end
function game_tooltip_hooks:SetAuctionItem(type, index)
local link = GetAuctionItemLink(type, index)
if link then
extend_tooltip(GameTooltip, link, aux.select(3, GetAuctionItemInfo(type, index)))
end
end
function game_tooltip_hooks:SetLootItem(slot)
local link = GetLootSlotLink(slot)
if link then
extend_tooltip(GameTooltip, link, aux.select(3, GetLootSlotInfo(slot)))
end
end
function game_tooltip_hooks:SetQuestItem(qtype, slot)
local link = GetQuestItemLink(qtype, slot)
if link then
extend_tooltip(GameTooltip, link, aux.select(3, GetQuestItemInfo(qtype, slot)))
end
end
function game_tooltip_hooks:SetQuestLogItem(qtype, slot)
local link = GetQuestLogItemLink(qtype, slot)
if link then
extend_tooltip(GameTooltip, link, aux.select(3, GetQuestLogRewardInfo(slot)))
end
end
function game_tooltip_hooks:SetBagItem(bag, slot)
local link = GetContainerItemLink(bag, slot)
if link then
extend_tooltip(GameTooltip, link, aux.select(2, GetContainerItemInfo(bag, slot)))
end
end
function game_tooltip_hooks:SetInboxItem(index)
local name, _, quantity = GetInboxItem(index)
local id = name and info.item_id(name)
if id then
local _, itemstring, quality = GetItemInfo(id)
local hex = aux.select(4, GetItemQualityColor(tonumber(quality)))
local link = hex .. '|H' .. itemstring .. '|h[' .. name .. ']|h' .. FONT_COLOR_CODE_CLOSE
extend_tooltip(GameTooltip, link, quantity)
end
end
function game_tooltip_hooks:SetInventoryItem(unit, slot)
local link = GetInventoryItemLink(unit, slot)
if link then
extend_tooltip(GameTooltip, link, 1)
end
end
function game_tooltip_hooks:SetMerchantItem(slot)
local link = GetMerchantItemLink(slot)
if link then
local quantity = aux.select(4, GetMerchantItemInfo(slot))
extend_tooltip(GameTooltip, link, quantity)
end
end
function game_tooltip_hooks:SetCraftItem(skill, slot)
local link, quantity
if slot then
link, quantity = GetCraftReagentItemLink(skill, slot), aux.select(3, GetCraftReagentInfo(skill, slot))
else
link, quantity = GetCraftItemLink(skill), 1
end
if link then
extend_tooltip(GameTooltip, link, quantity)
end
end
function game_tooltip_hooks:SetCraftSpell(slot)
local link = GetCraftItemLink(slot)
if link then
extend_tooltip(GameTooltip, link, 1)
end
end
function game_tooltip_hooks:SetTradeSkillItem(skill, slot)
local link, quantity
if slot then
link, quantity = GetTradeSkillReagentItemLink(skill, slot), aux.select(3, GetTradeSkillReagentInfo(skill, slot))
else
link, quantity = GetTradeSkillItemLink(skill), 1
end
if link then
extend_tooltip(GameTooltip, link, quantity)
end
end
function game_tooltip_hooks:SetAuctionSellItem()
local name, _, quantity = GetAuctionSellItemInfo()
if name then
for slot in info.inventory() do
T.temp(slot)
local link = GetContainerItemLink(unpack(slot))
if link and aux.select(5, info.parse_link(link)) == name then
extend_tooltip(GameTooltip, link, quantity)
return
end
end
end
end