443 lines
14 KiB
Lua
443 lines
14 KiB
Lua
-- ------------------------------------------------------------------------------ --
|
|
-- TradeSkillMaster --
|
|
-- https://tradeskillmaster.com --
|
|
-- All Rights Reserved - Detailed license information included with addon. --
|
|
-- ------------------------------------------------------------------------------ --
|
|
|
|
local _, TSM = ...
|
|
local Destroying = TSM:NewPackage("Destroying")
|
|
local Database = TSM.Include("Util.Database")
|
|
local Event = TSM.Include("Util.Event")
|
|
local SlotId = TSM.Include("Util.SlotId")
|
|
local TempTable = TSM.Include("Util.TempTable")
|
|
local ItemString = TSM.Include("Util.ItemString")
|
|
local Future = TSM.Include("Util.Future")
|
|
local Threading = TSM.Include("Service.Threading")
|
|
local ItemInfo = TSM.Include("Service.ItemInfo")
|
|
local CustomPrice = TSM.Include("Service.CustomPrice")
|
|
local Conversions = TSM.Include("Service.Conversions")
|
|
local BagTracking = TSM.Include("Service.BagTracking")
|
|
local Settings = TSM.Include("Service.Settings")
|
|
local private = {
|
|
combineThread = nil,
|
|
destroyThread = nil,
|
|
destroyThreadRunning = false,
|
|
settings = nil,
|
|
canDestroyCache = {},
|
|
destroyQuantityCache = {},
|
|
pendingCombines = {},
|
|
newBagUpdate = false,
|
|
bagUpdateCallback = nil,
|
|
pendingSpellId = nil,
|
|
ignoreDB = nil,
|
|
destroyInfoDB = nil,
|
|
combineFuture = Future.New("DESTROYING_COMBINE_FUTURE"),
|
|
destroyFuture = Future.New("DESTROYING_DESTROY_FUTURE"),
|
|
}
|
|
local SPELL_IDS = {
|
|
milling = 51005,
|
|
prospect = 31252,
|
|
disenchant = 13262,
|
|
}
|
|
local ITEM_SUB_CLASS_METAL_AND_STONE = 7
|
|
local ITEM_SUB_CLASS_HERB = 9
|
|
local TARGET_SLOT_ID_MULTIPLIER = 1000000
|
|
local GEM_CHIPS = {
|
|
["i:129099"] = "i:129100",
|
|
["i:130200"] = "i:129100",
|
|
["i:130201"] = "i:129100",
|
|
["i:130202"] = "i:129100",
|
|
["i:130203"] = "i:129100",
|
|
["i:130204"] = "i:129100",
|
|
}
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Functions
|
|
-- ============================================================================
|
|
|
|
function Destroying.OnInitialize()
|
|
private.combineThread = Threading.New("COMBINE_STACKS", private.CombineThread)
|
|
Threading.SetCallback(private.combineThread, private.CombineThreadDone)
|
|
private.destroyThread = Threading.New("DESTROY", private.DestroyThread)
|
|
Threading.SetCallback(private.destroyThread, private.DestroyThreadDone)
|
|
BagTracking.RegisterCallback(private.UpdateBagDB)
|
|
|
|
private.settings = Settings.NewView()
|
|
:AddKey("global", "internalData", "destroyingHistory")
|
|
:AddKey("global", "destroyingOptions", "deAbovePrice")
|
|
:AddKey("global", "destroyingOptions", "deMaxQuality")
|
|
:AddKey("global", "destroyingOptions", "includeSoulbound")
|
|
:AddKey("global", "userData", "destroyingIgnore")
|
|
:RegisterCallback("deAbovePrice", private.UpdateBagDB)
|
|
:RegisterCallback("deMaxQuality", private.UpdateBagDB)
|
|
:RegisterCallback("includeSoulbound", private.UpdateBagDB)
|
|
|
|
private.ignoreDB = Database.NewSchema("DESTROYING_IGNORE")
|
|
:AddUniqueStringField("itemString")
|
|
:AddBooleanField("ignoreSession")
|
|
:AddBooleanField("ignorePermanent")
|
|
:Commit()
|
|
private.ignoreDB:BulkInsertStart()
|
|
local used = TempTable.Acquire()
|
|
for itemString in pairs(private.settings.destroyingIgnore) do
|
|
itemString = ItemString.Get(itemString)
|
|
if not used[itemString] then
|
|
used[itemString] = true
|
|
private.ignoreDB:BulkInsertNewRow(itemString, false, true)
|
|
end
|
|
end
|
|
TempTable.Release(used)
|
|
private.ignoreDB:BulkInsertEnd()
|
|
|
|
private.destroyInfoDB = Database.NewSchema("DESTROYING_INFO")
|
|
:AddUniqueStringField("itemString")
|
|
:AddNumberField("minQuantity")
|
|
:AddNumberField("spellId")
|
|
:Commit()
|
|
|
|
Event.Register("LOOT_CLOSED", private.SendEventToThread)
|
|
Event.Register("BAG_UPDATE_DELAYED", private.SendEventToThread)
|
|
Event.Register("UNIT_SPELLCAST_START", private.SpellCastEventHandler)
|
|
Event.Register("UNIT_SPELLCAST_FAILED", private.SpellCastEventHandler)
|
|
Event.Register("UNIT_SPELLCAST_FAILED_QUIET", private.SpellCastEventHandler)
|
|
Event.Register("UNIT_SPELLCAST_INTERRUPTED", private.SpellCastEventHandler)
|
|
Event.Register("UNIT_SPELLCAST_SUCCEEDED", private.SpellCastEventHandler)
|
|
|
|
private.destroyFuture:SetScript("OnCleanup", function()
|
|
private.destroyThreadRunning = false
|
|
Threading.Kill(private.destroyThread)
|
|
end)
|
|
private.combineFuture:SetScript("OnCleanup", function()
|
|
Threading.Kill(private.combineThread)
|
|
end)
|
|
end
|
|
|
|
function Destroying.SetBagUpdateCallback(callback)
|
|
assert(not private.bagUpdateCallback)
|
|
private.bagUpdateCallback = callback
|
|
end
|
|
|
|
function Destroying.CreateBagQuery()
|
|
return BagTracking.CreateQueryBags()
|
|
:LeftJoin(private.ignoreDB, "itemString")
|
|
:InnerJoin(private.destroyInfoDB, "itemString")
|
|
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
|
|
:NotEqual("ignoreSession", true)
|
|
:NotEqual("ignorePermanent", true)
|
|
:GreaterThanOrEqual("quantity", Database.OtherFieldQueryParam("minQuantity"))
|
|
end
|
|
|
|
function Destroying.CanCombine()
|
|
return #private.pendingCombines > 0
|
|
end
|
|
|
|
function Destroying.StartCombine()
|
|
private.combineFuture:Start()
|
|
Threading.Start(private.combineThread)
|
|
return private.combineFuture
|
|
end
|
|
|
|
function Destroying.StartDestroy(button, row, callback)
|
|
private.destroyFuture:Start()
|
|
private.destroyThreadRunning = true
|
|
Threading.Start(private.destroyThread, button, row)
|
|
-- we need the thread to run now so send it a sync message
|
|
Threading.SendSyncMessage(private.destroyThread)
|
|
return private.destroyFuture
|
|
end
|
|
|
|
function Destroying.IgnoreItemSession(itemString)
|
|
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
|
|
if row then
|
|
assert(not row:GetField("ignoreSession"))
|
|
row:SetField("ignoreSession", true)
|
|
row:Update()
|
|
row:Release()
|
|
else
|
|
private.ignoreDB:NewRow()
|
|
:SetField("itemString", itemString)
|
|
:SetField("ignoreSession", true)
|
|
:SetField("ignorePermanent", false)
|
|
:Create()
|
|
end
|
|
end
|
|
|
|
function Destroying.IgnoreItemPermanent(itemString)
|
|
assert(not private.settings.destroyingIgnore[itemString])
|
|
private.settings.destroyingIgnore[itemString] = true
|
|
|
|
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
|
|
if row then
|
|
assert(not row:GetField("ignorePermanent"))
|
|
row:SetField("ignorePermanent", true)
|
|
row:Update()
|
|
row:Release()
|
|
else
|
|
private.ignoreDB:NewRow()
|
|
:SetField("itemString", itemString)
|
|
:SetField("ignoreSession", false)
|
|
:SetField("ignorePermanent", true)
|
|
:Create()
|
|
end
|
|
end
|
|
|
|
function Destroying.ForgetIgnoreItemPermanent(itemString)
|
|
assert(private.settings.destroyingIgnore[itemString])
|
|
private.settings.destroyingIgnore[itemString] = nil
|
|
|
|
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
|
|
assert(row and row:GetField("ignorePermanent"))
|
|
if row:GetField("ignoreSession") then
|
|
row:SetField("ignorePermanent", false)
|
|
row:Update()
|
|
else
|
|
private.ignoreDB:DeleteRow(row)
|
|
end
|
|
row:Release()
|
|
end
|
|
|
|
function Destroying.CreateIgnoreQuery()
|
|
return private.ignoreDB:NewQuery()
|
|
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
|
|
:Equal("ignorePermanent", true)
|
|
:OrderBy("name", true)
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Combine Stacks Thread
|
|
-- ============================================================================
|
|
|
|
function private.CombineThread()
|
|
while Destroying.CanCombine() do
|
|
for _, combineSlotId in ipairs(private.pendingCombines) do
|
|
local sourceBag, sourceSlot, targetBag, targetSlot = private.CombineSlotIdToBagSlot(combineSlotId)
|
|
PickupContainerItem(sourceBag, sourceSlot)
|
|
PickupContainerItem(targetBag, targetSlot)
|
|
end
|
|
-- wait for the bagDB to change
|
|
private.newBagUpdate = false
|
|
Threading.WaitForFunction(private.HasNewBagUpdate)
|
|
end
|
|
end
|
|
|
|
function private.CombineSlotIdToBagSlot(combineSlotId)
|
|
local sourceSlotId = combineSlotId % TARGET_SLOT_ID_MULTIPLIER
|
|
local targetSlotId = floor(combineSlotId / TARGET_SLOT_ID_MULTIPLIER)
|
|
local sourceBag, sourceSlot = SlotId.Split(sourceSlotId)
|
|
local targetBag, targetSlot = SlotId.Split(targetSlotId)
|
|
return sourceBag, sourceSlot, targetBag, targetSlot
|
|
end
|
|
|
|
function private.HasNewBagUpdate()
|
|
return private.newBagUpdate
|
|
end
|
|
|
|
function private.CombineThreadDone(result)
|
|
private.combineFuture:Done(result)
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Destroy Thread
|
|
-- ============================================================================
|
|
|
|
function private.DestroyThread(button, row)
|
|
-- we get sent a sync message so we run right away
|
|
Threading.ReceiveMessage()
|
|
|
|
local itemString, spellId, bag, slot = row:GetFields("itemString", "spellId", "bag", "slot")
|
|
local spellName = GetSpellInfo(spellId)
|
|
button:SetMacroText(format("/cast %s;\n/use %d %d", spellName, bag, slot))
|
|
|
|
-- wait for the spell cast to start or fail
|
|
private.pendingSpellId = spellId
|
|
local event = Threading.ReceiveMessage()
|
|
if event ~= "UNIT_SPELLCAST_START" then
|
|
-- the spell cast failed for some reason
|
|
ClearCursor()
|
|
return false
|
|
end
|
|
|
|
-- discard any other messages
|
|
Threading.Yield(true)
|
|
while Threading.HasPendingMessage() do
|
|
Threading.ReceiveMessage()
|
|
end
|
|
|
|
-- wait for the spell cast to finish
|
|
event = Threading.ReceiveMessage()
|
|
if event ~= "UNIT_SPELLCAST_SUCCEEDED" then
|
|
-- the spell cast was interrupted
|
|
return false
|
|
end
|
|
|
|
-- wait for the loot window to open
|
|
Threading.WaitForEvent("LOOT_READY")
|
|
|
|
-- add to the log
|
|
local newEntry = {
|
|
item = itemString,
|
|
time = time(),
|
|
result = {},
|
|
}
|
|
assert(GetNumLootItems() > 0)
|
|
for i = 1, GetNumLootItems() do
|
|
local lootItemString = ItemString.Get(GetLootSlotLink(i))
|
|
local _, _, quantity = GetLootSlotInfo(i)
|
|
if lootItemString and (quantity or 0) > 0 then
|
|
lootItemString = GEM_CHIPS[lootItemString] or lootItemString
|
|
newEntry.result[lootItemString] = quantity
|
|
end
|
|
end
|
|
private.settings.destroyingHistory[spellName] = private.settings.destroyingHistory[spellName] or {}
|
|
tinsert(private.settings.destroyingHistory[spellName], newEntry)
|
|
|
|
-- wait for the loot window to close
|
|
local hasLootClosed, hasBagUpdateDelayed = false, false
|
|
while not hasLootClosed or not hasBagUpdateDelayed do
|
|
event = Threading.ReceiveMessage()
|
|
if event == "LOOT_CLOSED" then
|
|
hasLootClosed = true
|
|
elseif event == "BAG_UPDATE_DELAYED" then
|
|
hasBagUpdateDelayed = true
|
|
end
|
|
end
|
|
|
|
-- we're done
|
|
return true
|
|
end
|
|
|
|
function private.SendEventToThread(event)
|
|
if not private.destroyThreadRunning then
|
|
return
|
|
end
|
|
Threading.SendMessage(private.destroyThread, event)
|
|
end
|
|
|
|
function private.SpellCastEventHandler(event, unit, _, spellId)
|
|
if unit ~= "player" or spellId ~= private.pendingSpellId then
|
|
return
|
|
end
|
|
private.SendEventToThread(event)
|
|
end
|
|
|
|
function private.DestroyThreadDone(result)
|
|
private.destroyThreadRunning = false
|
|
private.destroyFuture:Done(result)
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Bag Update Functions
|
|
-- ============================================================================
|
|
|
|
function private.UpdateBagDB()
|
|
wipe(private.pendingCombines)
|
|
private.destroyInfoDB:TruncateAndBulkInsertStart()
|
|
local itemPrevSlotId = TempTable.Acquire()
|
|
local checkedItem = TempTable.Acquire()
|
|
local query = BagTracking.CreateQueryBags()
|
|
:OrderBy("slotId", true)
|
|
:Select("slotId", "itemString", "quantity")
|
|
if not private.settings.includeSoulbound then
|
|
query:Equal("isBoP", false)
|
|
:Equal("isBoA", false)
|
|
end
|
|
for _, slotId, itemString, quantity in query:Iterator() do
|
|
local minQuantity = nil
|
|
if checkedItem[itemString] then
|
|
minQuantity = private.destroyInfoDB:GetUniqueRowField("itemString", itemString, "minQuantity")
|
|
else
|
|
checkedItem[itemString] = true
|
|
local spellId = nil
|
|
minQuantity, spellId = private.ProcessBagItem(itemString)
|
|
if minQuantity then
|
|
private.destroyInfoDB:BulkInsertNewRow(itemString, minQuantity, spellId)
|
|
end
|
|
end
|
|
if minQuantity and quantity % minQuantity ~= 0 then
|
|
if itemPrevSlotId[itemString] then
|
|
-- we can combine this with the previous partial stack
|
|
tinsert(private.pendingCombines, itemPrevSlotId[itemString] * TARGET_SLOT_ID_MULTIPLIER + slotId)
|
|
itemPrevSlotId[itemString] = nil
|
|
else
|
|
itemPrevSlotId[itemString] = slotId
|
|
end
|
|
end
|
|
end
|
|
query:Release()
|
|
TempTable.Release(checkedItem)
|
|
TempTable.Release(itemPrevSlotId)
|
|
private.destroyInfoDB:BulkInsertEnd()
|
|
|
|
private.newBagUpdate = true
|
|
if private.bagUpdateCallback then
|
|
private.bagUpdateCallback()
|
|
end
|
|
end
|
|
|
|
function private.ProcessBagItem(itemString)
|
|
if private.ignoreDB:HasUniqueRow("itemString", itemString) then
|
|
return
|
|
end
|
|
|
|
local spellId, minQuantity = private.IsDestroyable(itemString)
|
|
if not spellId then
|
|
return
|
|
elseif spellId == SPELL_IDS.disenchant then
|
|
local deAbovePrice = CustomPrice.GetValue(private.settings.deAbovePrice, itemString) or 0
|
|
local deValue = CustomPrice.GetValue("Destroy", itemString) or math.huge
|
|
if deValue < deAbovePrice then
|
|
return
|
|
end
|
|
end
|
|
return minQuantity, spellId
|
|
end
|
|
|
|
function private.IsDestroyable(itemString)
|
|
if private.destroyQuantityCache[itemString] then
|
|
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
|
|
end
|
|
|
|
-- disenchanting
|
|
local quality = ItemInfo.GetQuality(itemString)
|
|
if ItemInfo.IsDisenchantable(itemString) and quality <= private.settings.deMaxQuality then
|
|
private.canDestroyCache[itemString] = IsSpellKnown(SPELL_IDS.disenchant) and SPELL_IDS.disenchant
|
|
private.destroyQuantityCache[itemString] = 1
|
|
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
|
|
end
|
|
|
|
local conversionMethod, destroySpellId = nil, nil
|
|
local classId = ItemInfo.GetClassId(itemString)
|
|
local subClassId = ItemInfo.GetSubClassId(itemString)
|
|
if classId == LE_ITEM_CLASS_TRADEGOODS and subClassId == ITEM_SUB_CLASS_HERB then
|
|
conversionMethod = Conversions.METHOD.MILL
|
|
destroySpellId = SPELL_IDS.milling
|
|
elseif classId == LE_ITEM_CLASS_TRADEGOODS and subClassId == ITEM_SUB_CLASS_METAL_AND_STONE then
|
|
conversionMethod = Conversions.METHOD.PROSPECT
|
|
destroySpellId = SPELL_IDS.prospect
|
|
else
|
|
private.canDestroyCache[itemString] = false
|
|
private.destroyQuantityCache[itemString] = nil
|
|
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
|
|
end
|
|
|
|
local hasSourceItem = false
|
|
for _ in Conversions.TargetItemsByMethodIterator(itemString, conversionMethod) do
|
|
hasSourceItem = true
|
|
end
|
|
if hasSourceItem then
|
|
private.canDestroyCache[itemString] = IsSpellKnown(destroySpellId) and destroySpellId
|
|
private.destroyQuantityCache[itemString] = 5
|
|
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
|
|
end
|
|
|
|
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
|
|
end
|