Files
aux-addon-ClassicAPI/aux-addon/tabs/post/core.lua
T
geojak 02cfc420dd 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
2022-02-20 23:51:19 +01:00

561 lines
18 KiB
Lua

module 'aux.tabs.post'
local T = require 'T'
local aux = require 'aux'
local info = require 'aux.util.info'
local sort_util = require 'aux.util.sort'
local persistence = require 'aux.util.persistence'
local money = require 'aux.util.money'
local scan_util = require 'aux.util.scan'
local post = require 'aux.core.post'
local scan = require 'aux.core.scan'
local history = require 'aux.core.history'
local item_listing = require 'aux.gui.item_listing'
local al = require 'aux.gui.auction_listing'
local gui = require 'aux.gui'
local tab = aux.tab 'Post'
local settings_schema = {'tuple', '#', {duration='number'}, {start_price='number'}, {buyout_price='number'}, {hidden='boolean'}}
local scan_id, inventory_records, bid_records, buyout_records = 0, {}, {}, {}
M.DURATION_2, M.DURATION_8, M.DURATION_24 = 120, 480, 1440
refresh = true
selected_item = nil
function get_default_settings()
return T.map('duration', aux.account_data.post_duration, 'start_price', 0, 'buyout_price', 0, 'hidden', false)
end
function aux.handle.LOAD2()
data = aux.faction_data.post
end
function read_settings(item_key)
item_key = item_key or selected_item.key
return data[item_key] and persistence.read(settings_schema, data[item_key]) or get_default_settings()
end
function write_settings(settings, item_key)
item_key = item_key or selected_item.key
data[item_key] = persistence.write(settings_schema, settings)
end
do
local bid_selections, buyout_selections = {}, {}
function get_bid_selection()
return bid_selections[selected_item.key]
end
function set_bid_selection(record)
bid_selections[selected_item.key] = record
end
function get_buyout_selection()
return buyout_selections[selected_item.key]
end
function set_buyout_selection(record)
buyout_selections[selected_item.key] = record
end
end
function refresh_button_click()
scan.abort(scan_id)
refresh_entries()
refresh = true
end
function tab.OPEN()
frame:Show()
update_inventory_records()
refresh = true
end
function tab.CLOSE()
selected_item = nil
frame:Hide()
end
function tab.USE_ITEM(item_info)
select_item(item_info.item_key)
end
function get_unit_start_price()
return selected_item and read_settings().start_price or 0
end
function set_unit_start_price(amount)
local settings = read_settings()
settings.start_price = amount
write_settings(settings)
end
function get_unit_buyout_price()
return selected_item and read_settings().buyout_price or 0
end
function set_unit_buyout_price(amount)
local settings = read_settings()
settings.buyout_price = amount
write_settings(settings)
end
function update_inventory_listing()
local records = aux.values(aux.filter(aux.copy(inventory_records), function(record)
local settings = read_settings(record.key)
return record.aux_quantity > 0 and (not settings.hidden or show_hidden_checkbox:GetChecked())
end))
sort(records, function(a, b) return a.name < b.name end)
item_listing.populate(inventory_listing, records)
end
function update_auction_listing(listing, records, reference)
local rows = T.acquire()
if selected_item then
local historical_value = history.value(selected_item.key)
local stack_size = stack_size_slider:GetValue()
for _, record in records[selected_item.key] or T.empty do
local price_color = undercut(record, stack_size_slider:GetValue(), listing == 'bid') < reference and aux.color.red
local price = record.unit_price * (listing == 'bid' and record.stack_size / stack_size_slider:GetValue() or 1)
tinsert(rows, T.map(
'cols', T.list(
T.map('value', record.own and aux.color.green(record.count) or record.count),
T.map('value', al.time_left(record.duration)),
T.map('value', record.stack_size == stack_size and aux.color.green(record.stack_size) or record.stack_size),
T.map('value', money.to_string(price, true, nil, price_color)),
T.map('value', historical_value and gui.percentage_historical(aux.round(price / historical_value * 100)) or '---')
),
'record', record
))
end
if historical_value then
tinsert(rows, T.map(
'cols', T.list(
T.map('value', '---'),
T.map('value', '---'),
T.map('value', '---'),
T.map('value', money.to_string(historical_value, true, nil, aux.color.green)),
T.map('value', historical_value and gui.percentage_historical(100) or '---')
),
'record', T.map('historical_value', true, 'stack_size', stack_size, 'unit_price', historical_value, 'own', true)
))
end
sort(rows, function(a, b)
return sort_util.multi_lt(
a.record.unit_price * (listing == 'bid' and a.record.stack_size or 1),
b.record.unit_price * (listing == 'bid' and b.record.stack_size or 1),
a.record.historical_value and 1 or 0,
b.record.historical_value and 1 or 0,
b.record.own and 0 or 1,
a.record.own and 0 or 1,
a.record.stack_size,
b.record.stack_size,
a.record.duration,
b.record.duration
)
end)
end
if listing == 'bid' then
bid_listing:SetData(rows)
elseif listing == 'buyout' then
buyout_listing:SetData(rows)
end
end
function update_auction_listings()
update_auction_listing('bid', bid_records, get_unit_start_price())
update_auction_listing('buyout', buyout_records, get_unit_buyout_price())
end
function M.select_item(item_key)
for _, inventory_record in aux.filter(aux.copy(inventory_records), function(record) return record.aux_quantity > 0 end) do
if inventory_record.key == item_key then
update_item(inventory_record)
return
end
end
end
function price_update()
if selected_item then
local historical_value = history.value(selected_item.key)
if get_bid_selection() or get_buyout_selection() then
set_unit_start_price(undercut(get_bid_selection() or get_buyout_selection(), stack_size_slider:GetValue(), get_bid_selection()))
unit_start_price_input:SetText(money.to_string(get_unit_start_price(), true, nil, nil, true))
end
if get_buyout_selection() then
set_unit_buyout_price(undercut(get_buyout_selection(), stack_size_slider:GetValue()))
unit_buyout_price_input:SetText(money.to_string(get_unit_buyout_price(), true, nil, nil, true))
end
start_price_percentage:SetText(historical_value and gui.percentage_historical(aux.round(get_unit_start_price() / historical_value * 100)) or '---')
buyout_price_percentage:SetText(historical_value and gui.percentage_historical(aux.round(get_unit_buyout_price() / historical_value * 100)) or '---')
end
end
function post_auctions()
if selected_item then
local unit_start_price = get_unit_start_price()
local unit_buyout_price = get_unit_buyout_price()
local stack_size = stack_size_slider:GetValue()
local stack_count
stack_count = stack_count_slider:GetValue()
local duration = UIDropDownMenu_GetSelectedValue(duration_dropdown)
local key = selected_item.key
local duration_code
if duration == DURATION_2 then
duration_code = 2
elseif duration == DURATION_8 then
duration_code = 3
elseif duration == DURATION_24 then
duration_code = 4
end
post.start(
key,
stack_size,
duration,
unit_start_price,
unit_buyout_price,
stack_count,
function(posted)
if not frame:IsShown() then
return
end
for i = 1, posted do
record_auction(key, stack_size, unit_start_price * stack_size, unit_buyout_price, duration_code, UnitName'player')
end
update_inventory_records()
local same
for _, record in inventory_records do
if record.key == key then
same = record
break
end
end
if same then
update_item(same)
else
selected_item = nil
end
refresh = true
end
)
end
end
function validate_parameters()
if not selected_item then
post_button:Disable()
return
end
if get_unit_buyout_price() > 0 and get_unit_start_price() > get_unit_buyout_price() then
post_button:Disable()
return
end
if get_unit_start_price() == 0 then
post_button:Enable()
return
end
if stack_count_slider:GetValue() == 0 then
post_button:Disable()
return
end
post_button:Enable()
end
function update_item_configuration()
if not selected_item then
refresh_button:Disable()
item.texture:SetTexture(nil)
item.count:SetText()
item.name:SetTextColor(aux.color.label.enabled())
item.name:SetText('No item selected')
unit_start_price_input:Hide()
unit_buyout_price_input:Hide()
stack_size_slider:Hide()
stack_count_slider:Hide()
deposit:Hide()
duration_dropdown:Hide()
hide_checkbox:Hide()
else
unit_start_price_input:Show()
unit_buyout_price_input:Show()
stack_size_slider:Show()
stack_count_slider:Show()
deposit:Show()
duration_dropdown:Show()
hide_checkbox:Show()
item.texture:SetTexture(selected_item.texture)
item.name:SetText('[' .. selected_item.name .. ']')
do
local color = ITEM_QUALITY_COLORS[selected_item.quality]
item.name:SetTextColor(color.r, color.g, color.b)
end
if selected_item.aux_quantity > 1 then
item.count:SetText(selected_item.aux_quantity)
else
item.count:SetText()
end
stack_size_slider.editbox:SetNumber(stack_size_slider:GetValue())
stack_count_slider.editbox:SetNumber(stack_count_slider:GetValue())
do
local deposit_factor = UnitFactionGroup'npc' and .05 or .25
local duration_factor = UIDropDownMenu_GetSelectedValue(duration_dropdown) / 120
local stack_size, stack_count = selected_item.max_charges and 1 or stack_size_slider:GetValue(), stack_count_slider:GetValue()
local amount = floor(selected_item.unit_vendor_price * deposit_factor * stack_size) * stack_count * duration_factor
deposit:SetText('Deposit: ' .. money.to_string(amount, nil, nil, aux.color.text.enabled))
end
refresh_button:Enable()
end
end
function undercut(record, stack_size, stack)
local price = ceil(record.unit_price * (stack and record.stack_size or stack_size))
if not record.own then
price = price - 1
end
return price / stack_size
end
function quantity_update(maximize_count)
if selected_item then
local max_stack_count = selected_item.max_charges and selected_item.availability[stack_size_slider:GetValue()] or floor(selected_item.availability[0] / stack_size_slider:GetValue())
stack_count_slider:SetMinMaxValues(1, max_stack_count)
if maximize_count then
stack_count_slider:SetValue(max_stack_count)
end
end
refresh = true
end
function unit_vendor_price(item_key)
for slot in info.inventory() do
T.temp(slot)
local item_info = T.temp-info.container_item(unpack(slot))
if item_info and item_info.item_key == item_key then
if info.auctionable(item_info.tooltip, nil, true) and not item_info.lootable then
ClearCursor()
PickupContainerItem(unpack(slot))
ClickAuctionSellItemButton()
local auction_sell_item = T.temp-info.auction_sell_item()
ClearCursor()
ClickAuctionSellItemButton()
ClearCursor()
if auction_sell_item then
return auction_sell_item.vendor_price / auction_sell_item.count
end
end
end
end
end
function update_item(item)
local settings = read_settings(item.key)
item.unit_vendor_price = unit_vendor_price(item.key)
if not item.unit_vendor_price then
settings.hidden = true
write_settings(settings, item.key)
refresh = true
return
end
scan.abort(scan_id)
selected_item = item
UIDropDownMenu_Initialize(duration_dropdown, initialize_duration_dropdown)
UIDropDownMenu_SetSelectedValue(duration_dropdown, settings.duration)
hide_checkbox:SetChecked(settings.hidden)
if selected_item.max_charges then
for i = selected_item.max_charges, 1, -1 do
if selected_item.availability[i] > 0 then
stack_size_slider:SetMinMaxValues(1, i)
break
end
end
else
stack_size_slider:SetMinMaxValues(1, min(selected_item.max_stack, selected_item.aux_quantity))
end
stack_size_slider:SetValue(aux.huge)
quantity_update(true)
unit_start_price_input:SetText(money.to_string(settings.start_price, true, nil, nil, true))
unit_buyout_price_input:SetText(money.to_string(settings.buyout_price, true, nil, nil, true))
if not bid_records[selected_item.key] then
refresh_entries()
end
write_settings(settings, item.key)
refresh = true
end
function update_inventory_records()
local auctionable_map = T.temp-T.acquire()
for slot in info.inventory() do
T.temp(slot)
local item_info = T.temp-info.container_item(unpack(slot))
if item_info then
local charge_class = item_info.charges or 0
if info.auctionable(item_info.tooltip, nil, true) and not item_info.lootable then
if not auctionable_map[item_info.item_key] then
local availability = T.acquire()
for i = 0, 10 do
availability[i] = 0
end
availability[charge_class] = item_info.count
auctionable_map[item_info.item_key] = T.map(
'item_id', item_info.item_id,
'suffix_id', item_info.suffix_id,
'key', item_info.item_key,
'itemstring', item_info.itemstring,
'name', item_info.name,
'texture', item_info.texture,
'quality', item_info.quality,
'aux_quantity', item_info.charges or item_info.count,
'max_stack', item_info.max_stack,
'max_charges', item_info.max_charges,
'availability', availability
)
else
local auctionable = auctionable_map[item_info.item_key]
auctionable.availability[charge_class] = (auctionable.availability[charge_class] or 0) + item_info.count
auctionable.aux_quantity = auctionable.aux_quantity + (item_info.charges or item_info.count)
end
end
end
end
T.release(inventory_records)
inventory_records = aux.values(auctionable_map)
refresh = true
end
function refresh_entries()
if selected_item then
local item_key = selected_item.key
set_bid_selection()
set_buyout_selection()
bid_records[item_key], buyout_records[item_key] = nil, nil
local query = scan_util.item_query(selected_item.item_id)
status_bar:update_status(0, 0)
status_bar:set_text('Scanning auctions...')
scan_id = scan.start{
type = 'list',
ignore_owner = true,
queries = T.list(query),
on_page_loaded = function(page, total_pages)
status_bar:update_status(page / total_pages, 0) -- TODO
status_bar:set_text(format('Scanning Page %d / %d', page, total_pages))
end,
on_auction = function(auction_record)
if auction_record.item_key == item_key then
record_auction(
auction_record.item_key,
auction_record.aux_quantity,
auction_record.unit_blizzard_bid,
auction_record.unit_buyout_price,
auction_record.duration,
auction_record.owner
)
end
end,
on_abort = function()
bid_records[item_key], buyout_records[item_key] = nil, nil
status_bar:update_status(1, 1)
status_bar:set_text('Scan aborted')
end,
on_complete = function()
bid_records[item_key] = bid_records[item_key] or T.acquire()
buyout_records[item_key] = buyout_records[item_key] or T.acquire()
refresh = true
status_bar:update_status(1, 1)
status_bar:set_text('Scan complete')
end,
}
end
end
function record_auction(key, aux_quantity, unit_blizzard_bid, unit_buyout_price, duration, owner)
bid_records[key] = bid_records[key] or T.acquire()
do
local entry
for _, record in bid_records[key] do
if unit_blizzard_bid == record.unit_price and aux_quantity == record.stack_size and duration == record.duration and info.is_player(owner) == record.own then
entry = record
end
end
if not entry then
entry = T.map('stack_size', aux_quantity, 'unit_price', unit_blizzard_bid, 'duration', duration, 'own', info.is_player(owner), 'count', 0)
tinsert(bid_records[key], entry)
end
entry.count = entry.count + 1
end
buyout_records[key] = buyout_records[key] or T.acquire()
if unit_buyout_price == 0 then return end
do
local entry
for _, record in buyout_records[key] do
if unit_buyout_price == record.unit_price and aux_quantity == record.stack_size and duration == record.duration and info.is_player(owner) == record.own then
entry = record
end
end
if not entry then
entry = T.map('stack_size', aux_quantity, 'unit_price', unit_buyout_price, 'duration', duration, 'own', info.is_player(owner), 'count', 0)
tinsert(buyout_records[key], entry)
end
entry.count = entry.count + 1
end
end
function on_update()
if refresh then
refresh = false
price_update()
update_item_configuration()
update_inventory_listing()
update_auction_listings()
end
validate_parameters()
end
function initialize_duration_dropdown()
local function on_click()
UIDropDownMenu_SetSelectedValue(duration_dropdown, this.value)
local settings = read_settings()
settings.duration = this.value
write_settings(settings)
refresh = true
end
UIDropDownMenu_AddButton{
text = '2 Hours',
value = DURATION_2,
func = on_click,
}
UIDropDownMenu_AddButton{
text = '8 Hours',
value = DURATION_8,
func = on_click,
}
UIDropDownMenu_AddButton{
text = '24 Hours',
value = DURATION_24,
func = on_click,
}
end