mirror of
https://codeberg.org/Duvelcorp/MetaHunt.git
synced 2026-09-22 15:46:59 +00:00
1313 lines
37 KiB
Lua
1313 lines
37 KiB
Lua
------------------------------------------------------
|
|
-- MetaHunt: AutoBuy Engine
|
|
-- Foundation for vendor auto-buy logic (food + ammo)
|
|
------------------------------------------------------
|
|
|
|
MTH_AutoBuyEngine = MTH_AutoBuyEngine or {}
|
|
|
|
local Engine = MTH_AutoBuyEngine
|
|
local AB_AMMO_BY_NAME = nil
|
|
local AB_FOOD_DIET_BY_ITEM_ID = nil
|
|
|
|
|
|
local function AB_ParseItemIdFromLink(link)
|
|
if type(link) ~= "string" then
|
|
return nil
|
|
end
|
|
local _, _, itemId = string.find(link, "item:(%d+)")
|
|
if not itemId then
|
|
return nil
|
|
end
|
|
return tonumber(itemId) or itemId
|
|
end
|
|
|
|
local function AB_GetAmmoSubtypeByItemId(itemId)
|
|
if not itemId then
|
|
return nil
|
|
end
|
|
local row = MTH_DS_AmmoItems and MTH_DS_AmmoItems[itemId] or nil
|
|
if type(row) ~= "table" then
|
|
row = MTH_DS_AmmoItems and MTH_DS_AmmoItems[tonumber(itemId) or -1] or nil
|
|
end
|
|
if type(row) ~= "table" then
|
|
return nil
|
|
end
|
|
local subtype = string.lower(tostring(row.subtype or ""))
|
|
if subtype == "arrow" or subtype == "bullet" then
|
|
return subtype
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function AB_GetAmmoLookupByName()
|
|
if type(AB_AMMO_BY_NAME) == "table" then
|
|
return AB_AMMO_BY_NAME
|
|
end
|
|
AB_AMMO_BY_NAME = {}
|
|
local data = MTH_DS_AmmoItems or {}
|
|
for id, row in pairs(data) do
|
|
if type(row) == "table" then
|
|
local nameKey = string.lower(tostring(row.name or ""))
|
|
local subtype = string.lower(tostring(row.subtype or ""))
|
|
if nameKey ~= "" and (subtype == "arrow" or subtype == "bullet") then
|
|
if type(AB_AMMO_BY_NAME[nameKey]) ~= "table" then
|
|
AB_AMMO_BY_NAME[nameKey] = {
|
|
itemId = tonumber(id) or id,
|
|
subtype = subtype,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return AB_AMMO_BY_NAME
|
|
end
|
|
|
|
local function AB_NormalizeDietToken(token)
|
|
local value = string.lower(tostring(token or ""))
|
|
value = string.gsub(value, "^%s+", "")
|
|
value = string.gsub(value, "%s+$", "")
|
|
return value
|
|
end
|
|
|
|
local function AB_GetFoodDietMap()
|
|
if type(AB_FOOD_DIET_BY_ITEM_ID) == "table" then
|
|
return AB_FOOD_DIET_BY_ITEM_ID
|
|
end
|
|
|
|
AB_FOOD_DIET_BY_ITEM_ID = {}
|
|
if type(FOM_Foods) ~= "table" then
|
|
return AB_FOOD_DIET_BY_ITEM_ID
|
|
end
|
|
|
|
for dietName, itemIds in pairs(FOM_Foods) do
|
|
local normalizedDiet = AB_NormalizeDietToken(dietName)
|
|
if normalizedDiet ~= "" and type(itemIds) == "table" then
|
|
for _, itemId in ipairs(itemIds) do
|
|
local numericId = tonumber(itemId)
|
|
if numericId then
|
|
AB_FOOD_DIET_BY_ITEM_ID[numericId] = normalizedDiet
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return AB_FOOD_DIET_BY_ITEM_ID
|
|
end
|
|
|
|
local function AB_GetItemFoodLevel(itemId)
|
|
if itemId and type(MTH_FEED_GetFoodLevel) == "function" then
|
|
local ok, level = pcall(MTH_FEED_GetFoodLevel, itemId)
|
|
local numeric = ok and tonumber(level) or nil
|
|
if numeric and numeric > 0 then
|
|
return numeric
|
|
end
|
|
end
|
|
if not itemId or type(GetItemInfo) ~= "function" then
|
|
return 1
|
|
end
|
|
local _, _, _, itemLevel, minLevel = GetItemInfo(itemId)
|
|
local resolved = tonumber(minLevel) or tonumber(itemLevel) or 1
|
|
if resolved < 1 then resolved = 1 end
|
|
return resolved
|
|
end
|
|
|
|
local function AB_GetItemMaxStack(itemId, fallbackStack)
|
|
local fallback = math.floor(tonumber(fallbackStack) or 1)
|
|
if fallback < 1 then
|
|
fallback = 1
|
|
end
|
|
|
|
local numericId = tonumber(itemId)
|
|
if not numericId or type(GetItemInfo) ~= "function" then
|
|
if fallback < 20 then
|
|
return 20
|
|
end
|
|
return fallback
|
|
end
|
|
|
|
local _, _, _, _, _, _, _, maxStack = GetItemInfo(numericId)
|
|
if (not maxStack or tonumber(maxStack) == nil or tonumber(maxStack) < 1) and type(MTH_PrimeItemCache) == "function" then
|
|
pcall(MTH_PrimeItemCache, numericId)
|
|
_, _, _, _, _, _, _, maxStack = GetItemInfo(numericId)
|
|
end
|
|
maxStack = math.floor(tonumber(maxStack) or 0)
|
|
if maxStack < 1 then
|
|
if fallback < 20 then
|
|
return 20
|
|
end
|
|
return fallback
|
|
end
|
|
|
|
return maxStack
|
|
end
|
|
|
|
local function AB_IsFoodExceptionRejected(itemId)
|
|
if not itemId then
|
|
return false
|
|
end
|
|
if type(MTH_FEED_IsExceptionReject) ~= "function" then
|
|
return false
|
|
end
|
|
local ok, rejected = pcall(MTH_FEED_IsExceptionReject, itemId)
|
|
return ok and rejected and true or false
|
|
end
|
|
|
|
local function AB_IsFoodCompatibleByRules(petLevel, foodLevel)
|
|
local numericPetLevel = tonumber(petLevel) or 1
|
|
local numericFoodLevel = tonumber(foodLevel) or 0
|
|
if numericPetLevel < 1 then
|
|
numericPetLevel = 1
|
|
end
|
|
|
|
if type(MTH_FEED_GetFoodCompatibilityWindow) == "function" then
|
|
local ok, minAcceptedLevel, maxAcceptedLevel = pcall(MTH_FEED_GetFoodCompatibilityWindow, numericPetLevel)
|
|
if ok then
|
|
local minLevel = tonumber(minAcceptedLevel)
|
|
local maxLevel = tonumber(maxAcceptedLevel)
|
|
if minLevel and numericFoodLevel < minLevel then
|
|
return false
|
|
end
|
|
if maxLevel and numericFoodLevel > maxLevel then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
if type(MTH_FEED_IsFoodCompatible) == "function" then
|
|
local ok, isCompatible = pcall(MTH_FEED_IsFoodCompatible, petLevel, foodLevel)
|
|
if ok then
|
|
return isCompatible and true or false
|
|
end
|
|
end
|
|
local minAcceptedLevel = numericPetLevel - 15
|
|
if minAcceptedLevel < 1 then
|
|
minAcceptedLevel = 1
|
|
end
|
|
return numericFoodLevel >= minAcceptedLevel and numericFoodLevel <= numericPetLevel
|
|
end
|
|
|
|
local function AB_NormalizeFamilyToken(text)
|
|
local value = string.lower(tostring(text or ""))
|
|
value = string.gsub(value, "%b()", " ")
|
|
value = string.gsub(value, "[^%a]", "")
|
|
return value
|
|
end
|
|
|
|
local function AB_SingularizeFamilyToken(token)
|
|
local value = tostring(token or "")
|
|
if string.len(value) <= 3 then
|
|
return value
|
|
end
|
|
if string.sub(value, -3) == "ves" and string.len(value) > 4 then
|
|
return string.sub(value, 1, -4) .. "f"
|
|
end
|
|
if string.sub(value, -3) == "ies" and string.len(value) > 4 then
|
|
return string.sub(value, 1, -4) .. "y"
|
|
end
|
|
if string.sub(value, -3) == "xes" and string.len(value) > 4 then
|
|
return string.sub(value, 1, -3)
|
|
end
|
|
if string.sub(value, -4) == "ches" and string.len(value) > 5 then
|
|
return string.sub(value, 1, -3)
|
|
end
|
|
if string.sub(value, -4) == "shes" and string.len(value) > 5 then
|
|
return string.sub(value, 1, -3)
|
|
end
|
|
if string.sub(value, -3) == "zes" and string.len(value) > 4 then
|
|
return string.sub(value, 1, -3)
|
|
end
|
|
if string.sub(value, -3) == "ses" then
|
|
return string.sub(value, 1, -3)
|
|
end
|
|
if string.sub(value, -1) == "s" then
|
|
return string.sub(value, 1, -2)
|
|
end
|
|
return value
|
|
end
|
|
|
|
local function AB_FindFamilyDietRow(familyName)
|
|
if type(MTH_DS_Families) ~= "table" then
|
|
return nil, nil
|
|
end
|
|
|
|
local needle = AB_NormalizeFamilyToken(familyName)
|
|
if needle == "" then
|
|
return nil, nil
|
|
end
|
|
local needleSingular = AB_SingularizeFamilyToken(needle)
|
|
|
|
for key, row in pairs(MTH_DS_Families) do
|
|
local candidate = AB_NormalizeFamilyToken(key)
|
|
if candidate ~= "" then
|
|
local candidateSingular = AB_SingularizeFamilyToken(candidate)
|
|
if candidate == needle or candidate == needleSingular or candidateSingular == needle or candidateSingular == needleSingular then
|
|
return row, tostring(key)
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
local function AB_GetMerchantFoodsSnapshot(merchantSnapshot)
|
|
if type(MTH_GetMerchantFoodsByDiet) == "function" then
|
|
local ok, result = pcall(MTH_GetMerchantFoodsByDiet)
|
|
if ok and type(result) == "table" then
|
|
return result
|
|
end
|
|
end
|
|
|
|
local dietMap = AB_GetFoodDietMap()
|
|
local result = {
|
|
byDiet = {},
|
|
items = {},
|
|
unknown = {},
|
|
}
|
|
|
|
if type(merchantSnapshot) ~= "table" or type(merchantSnapshot.items) ~= "table" then
|
|
return result
|
|
end
|
|
|
|
for i = 1, table.getn(merchantSnapshot.items) do
|
|
local row = merchantSnapshot.items[i]
|
|
if type(row) == "table" and row.itemId then
|
|
local numericId = tonumber(row.itemId)
|
|
local diet = numericId and dietMap[numericId] or nil
|
|
local foodRow = {
|
|
index = row.index,
|
|
id = numericId,
|
|
name = row.name,
|
|
link = row.link,
|
|
diet = diet,
|
|
price = row.price,
|
|
stack = row.stackCount,
|
|
available = row.numAvailable,
|
|
}
|
|
if diet then
|
|
if type(result.byDiet[diet]) ~= "table" then
|
|
result.byDiet[diet] = {}
|
|
end
|
|
table.insert(result.byDiet[diet], foodRow)
|
|
table.insert(result.items, foodRow)
|
|
else
|
|
table.insert(result.unknown, foodRow)
|
|
end
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function AB_CollectPetRows(scope)
|
|
if type(MTH_PETS_RefreshCurrentPet) == "function" then
|
|
pcall(MTH_PETS_RefreshCurrentPet)
|
|
end
|
|
|
|
local rows = {}
|
|
local petsRoot = type(MTH_PETS_GetRootStore) == "function" and MTH_PETS_GetRootStore() or nil
|
|
local petStore = petsRoot and petsRoot.petStore or nil
|
|
local activeById = petStore and petStore.activeById or nil
|
|
local stableSlotIndex = petStore and petStore.stableSlotIndex or nil
|
|
|
|
local currentInfo = type(MTH_GetCurrentPetInfo) == "function" and MTH_GetCurrentPetInfo() or nil
|
|
if currentInfo and currentInfo.liveExists then
|
|
local currentName = tostring(currentInfo.name or "")
|
|
if currentName ~= "" then
|
|
table.insert(rows, {
|
|
name = currentName,
|
|
family = tostring(currentInfo.family or ""),
|
|
level = tonumber(currentInfo.level) or 0,
|
|
slot = 0,
|
|
})
|
|
end
|
|
end
|
|
|
|
if scope == "all" then
|
|
for slot = 1, 4 do
|
|
local petId = (type(stableSlotIndex) == "table") and stableSlotIndex[slot] or nil
|
|
local row = (petId and type(activeById) == "table") and activeById[petId] or nil
|
|
if type(row) ~= "table" and type(activeById) == "table" then
|
|
for _, candidate in pairs(activeById) do
|
|
if type(candidate) == "table" and tonumber(candidate.stableSlot) == slot then
|
|
row = candidate
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if type(row) == "table" and tostring(row.name or "") ~= "" then
|
|
table.insert(rows, {
|
|
name = tostring(row.name or ""),
|
|
family = tostring(row.family or ""),
|
|
level = tonumber(row.level) or 0,
|
|
slot = slot,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
if scope ~= "all" and table.getn(rows) > 1 then
|
|
local filtered = {}
|
|
for i = 1, table.getn(rows) do
|
|
if rows[i].slot == 0 then
|
|
table.insert(filtered, rows[i])
|
|
break
|
|
end
|
|
end
|
|
rows = filtered
|
|
end
|
|
|
|
return rows
|
|
end
|
|
|
|
local function AB_SelectBestFoodRowForDiet(dietRows, petLevel)
|
|
if type(dietRows) ~= "table" then
|
|
return nil, 0
|
|
end
|
|
|
|
local numericPetLevel = tonumber(petLevel) or 1
|
|
if numericPetLevel < 1 then
|
|
numericPetLevel = 1
|
|
end
|
|
|
|
local bestRow = nil
|
|
local bestLevel = -1
|
|
for i = 1, table.getn(dietRows) do
|
|
local row = dietRows[i]
|
|
if type(row) == "table" and row.id and row.index then
|
|
if AB_IsFoodExceptionRejected(row.id) then
|
|
-- learned exception: skip this item from autobuy candidates
|
|
else
|
|
local level = AB_GetItemFoodLevel(tonumber(row.id))
|
|
local isCompatible = AB_IsFoodCompatibleByRules(numericPetLevel, level)
|
|
local underPetCap = (tonumber(level) or 0) <= numericPetLevel
|
|
if isCompatible and underPetCap and level >= bestLevel then
|
|
if level > bestLevel then
|
|
bestLevel = level
|
|
bestRow = row
|
|
elseif bestRow == nil then
|
|
bestRow = row
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if bestRow then
|
|
return bestRow, bestLevel
|
|
end
|
|
|
|
return nil, 0
|
|
end
|
|
|
|
local function AB_CountDietItemsInBags(dietName)
|
|
local normalizedDiet = AB_NormalizeDietToken(dietName)
|
|
if normalizedDiet == "" then
|
|
return 0
|
|
end
|
|
|
|
local dietMap = AB_GetFoodDietMap()
|
|
local getSlots = _G and _G["GetContainerNumSlots"] or nil
|
|
local getLink = _G and _G["GetContainerItemLink"] or nil
|
|
local getInfo = _G and _G["GetContainerItemInfo"] or nil
|
|
if type(getSlots) ~= "function" or type(getLink) ~= "function" or type(getInfo) ~= "function" then
|
|
return 0
|
|
end
|
|
|
|
local count = 0
|
|
for bag = 0, 4 do
|
|
local slots = tonumber(getSlots(bag)) or 0
|
|
for slot = 1, slots do
|
|
local link = getLink(bag, slot)
|
|
local itemId = AB_ParseItemIdFromLink(link)
|
|
local numericId = tonumber(itemId)
|
|
if numericId and dietMap[numericId] == normalizedDiet then
|
|
local _, itemCount = getInfo(bag, slot)
|
|
itemCount = math.floor(tonumber(itemCount) or 1)
|
|
if itemCount < 1 then
|
|
itemCount = 1
|
|
end
|
|
count = count + itemCount
|
|
end
|
|
end
|
|
end
|
|
|
|
return count
|
|
end
|
|
|
|
local function AB_ResolveAmmoByName(name)
|
|
if type(name) ~= "string" or name == "" then
|
|
return nil, nil
|
|
end
|
|
local lookup = AB_GetAmmoLookupByName()
|
|
local row = lookup[string.lower(name)]
|
|
if type(row) ~= "table" then
|
|
return nil, nil
|
|
end
|
|
return row.itemId, row.subtype
|
|
end
|
|
|
|
local function AB_GetInventoryBagSlotByContainerId(containerId)
|
|
local toInv = _G and _G["ContainerIDToInventoryID"] or nil
|
|
if type(toInv) == "function" then
|
|
local invSlot = tonumber(toInv(containerId))
|
|
if invSlot and invSlot > 0 then
|
|
return invSlot
|
|
end
|
|
end
|
|
local bag = tonumber(containerId)
|
|
if bag and bag >= 1 and bag <= 4 then
|
|
return bag + 19
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function AB_GetSpecialAmmoBagTargets()
|
|
local targets = {
|
|
arrow = {},
|
|
bullet = {},
|
|
}
|
|
local getInvLink = _G and _G["GetInventoryItemLink"] or nil
|
|
if type(getInvLink) ~= "function" then
|
|
return targets
|
|
end
|
|
for bag = 1, 4 do
|
|
local invSlot = AB_GetInventoryBagSlotByContainerId(bag)
|
|
if invSlot then
|
|
local bagLink = getInvLink("player", invSlot)
|
|
local bagItemId = AB_ParseItemIdFromLink(bagLink)
|
|
local bagRow = (MTH_DS_BagItems and bagItemId) and MTH_DS_BagItems[tonumber(bagItemId) or bagItemId] or nil
|
|
local bagSubtype = bagRow and string.lower(tostring(bagRow.subtype or "")) or ""
|
|
if bagSubtype == "quiver" then
|
|
table.insert(targets.arrow, { bag = bag, invSlot = invSlot, itemId = bagItemId, subtype = bagSubtype })
|
|
elseif bagSubtype == "ammo pouch" then
|
|
table.insert(targets.bullet, { bag = bag, invSlot = invSlot, itemId = bagItemId, subtype = bagSubtype })
|
|
end
|
|
end
|
|
end
|
|
return targets
|
|
end
|
|
|
|
local function AB_MoveAmmoStacksToSpecialBags(maxMoves)
|
|
local getSlots = _G and _G["GetContainerNumSlots"] or nil
|
|
local getLink = _G and _G["GetContainerItemLink"] or nil
|
|
local pick = _G and _G["PickupContainerItem"] or nil
|
|
local putInBag = _G and _G["PutItemInBag"] or nil
|
|
local hasCursor = _G and _G["CursorHasItem"] or nil
|
|
local clearCursor = _G and _G["ClearCursor"] or nil
|
|
if type(getSlots) ~= "function" or type(getLink) ~= "function"
|
|
or type(pick) ~= "function" or type(putInBag) ~= "function"
|
|
or type(hasCursor) ~= "function" or type(clearCursor) ~= "function" then
|
|
return 0
|
|
end
|
|
|
|
local targets = AB_GetSpecialAmmoBagTargets()
|
|
local arrowTargets = targets.arrow or {}
|
|
local bulletTargets = targets.bullet or {}
|
|
if table.getn(arrowTargets) == 0 and table.getn(bulletTargets) == 0 then
|
|
return 0
|
|
end
|
|
|
|
local moves = 0
|
|
local moveLimit = math.floor(tonumber(maxMoves) or 200)
|
|
if moveLimit < 1 then moveLimit = 200 end
|
|
local isTargetBag = {}
|
|
for i = 1, table.getn(arrowTargets) do
|
|
local bagId = tonumber(arrowTargets[i].bag)
|
|
if bagId then
|
|
isTargetBag[bagId] = true
|
|
end
|
|
end
|
|
for i = 1, table.getn(bulletTargets) do
|
|
local bagId = tonumber(bulletTargets[i].bag)
|
|
if bagId then
|
|
isTargetBag[bagId] = true
|
|
end
|
|
end
|
|
|
|
for sourceBag = 0, 4 do
|
|
local isSpecialSource = isTargetBag[tonumber(sourceBag)] and true or false
|
|
if not isSpecialSource then
|
|
local slots = tonumber(getSlots(sourceBag)) or 0
|
|
for sourceSlot = 1, slots do
|
|
local link = getLink(sourceBag, sourceSlot)
|
|
local itemId = AB_ParseItemIdFromLink(link)
|
|
local ammoSubtype = itemId and AB_GetAmmoSubtypeByItemId(itemId) or nil
|
|
if ammoSubtype then
|
|
local destinationTargets = ammoSubtype == "arrow" and arrowTargets or bulletTargets
|
|
if table.getn(destinationTargets) > 0 then
|
|
pick(sourceBag, sourceSlot)
|
|
if hasCursor() then
|
|
for i = 1, table.getn(destinationTargets) do
|
|
putInBag(destinationTargets[i].invSlot)
|
|
if not hasCursor() then
|
|
moves = moves + 1
|
|
break
|
|
end
|
|
end
|
|
if hasCursor() then
|
|
clearCursor()
|
|
end
|
|
if moves >= moveLimit then
|
|
return moves
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return moves
|
|
end
|
|
|
|
local function AB_DefaultRule()
|
|
return {
|
|
enabled = false,
|
|
itemId = nil,
|
|
stacks = 1,
|
|
}
|
|
end
|
|
|
|
local function AB_EnsureRuleList(bucket)
|
|
if type(bucket) ~= "table" then
|
|
bucket = {}
|
|
end
|
|
if type(bucket.rules) ~= "table" then
|
|
bucket.rules = {}
|
|
end
|
|
for i = 1, 3 do
|
|
if type(bucket.rules[i]) ~= "table" then
|
|
bucket.rules[i] = AB_DefaultRule()
|
|
end
|
|
if bucket.rules[i].enabled == nil then
|
|
bucket.rules[i].enabled = false
|
|
end
|
|
if bucket.rules[i].stacks == nil then
|
|
bucket.rules[i].stacks = 1
|
|
end
|
|
end
|
|
return bucket
|
|
end
|
|
|
|
local function AB_DeepCopy(value)
|
|
if type(value) ~= "table" then
|
|
return value
|
|
end
|
|
local copy = {}
|
|
for key, entry in pairs(value) do
|
|
copy[key] = AB_DeepCopy(entry)
|
|
end
|
|
return copy
|
|
end
|
|
|
|
function Engine:GetConfigStore()
|
|
if MTH and MTH.GetModuleCharSavedVariables then
|
|
local charStore = MTH:GetModuleCharSavedVariables("autobuy")
|
|
if type(charStore) == "table" then
|
|
if MTH.GetModuleSavedVariables then
|
|
local accountStore = MTH:GetModuleSavedVariables("autobuy")
|
|
if type(accountStore) == "table" and next(charStore) == nil and next(accountStore) ~= nil then
|
|
for key, value in pairs(accountStore) do
|
|
charStore[key] = AB_DeepCopy(value)
|
|
end
|
|
end
|
|
end
|
|
return charStore
|
|
end
|
|
end
|
|
if MTH and MTH.GetModuleSavedVariables then
|
|
local accountStore = MTH:GetModuleSavedVariables("autobuy")
|
|
if type(accountStore) == "table" then
|
|
return accountStore
|
|
end
|
|
end
|
|
self._transientStore = self._transientStore or {}
|
|
return self._transientStore
|
|
end
|
|
|
|
function Engine:EnsureDefaults()
|
|
local store = self:GetConfigStore()
|
|
if store.enabled == nil then
|
|
store.enabled = false
|
|
end
|
|
if type(store.food) ~= "table" then
|
|
store.food = {}
|
|
end
|
|
if type(store.ammo) ~= "table" then
|
|
store.ammo = {}
|
|
end
|
|
if type(store.projectiles) ~= "table" then
|
|
store.projectiles = {}
|
|
end
|
|
if type(store.petFood) ~= "table" then
|
|
store.petFood = {}
|
|
end
|
|
if store.food.enabled == nil then
|
|
store.food.enabled = false
|
|
end
|
|
if store.ammo.enabled == nil then
|
|
store.ammo.enabled = false
|
|
end
|
|
if store.projectiles.enabled == nil then
|
|
store.projectiles.enabled = false
|
|
end
|
|
if store.petFood.enabled == nil then
|
|
store.petFood.enabled = false
|
|
end
|
|
if store.petFood.stacks == nil then
|
|
store.petFood.stacks = 1
|
|
end
|
|
if store.petFood.scope == nil then
|
|
store.petFood.scope = "current"
|
|
end
|
|
store.petFood.stacks = math.floor(tonumber(store.petFood.stacks) or 1)
|
|
if store.petFood.stacks < 0 then
|
|
store.petFood.stacks = 0
|
|
end
|
|
if store.petFood.scope ~= "current" and store.petFood.scope ~= "all" then
|
|
store.petFood.scope = "current"
|
|
end
|
|
if store.food.minStock == nil then
|
|
store.food.minStock = 200
|
|
end
|
|
if store.ammo.minStock == nil then
|
|
store.ammo.minStock = 1000
|
|
end
|
|
if store.food.preferredDiet == nil then
|
|
store.food.preferredDiet = nil
|
|
end
|
|
if store.ammo.preferredType == nil then
|
|
store.ammo.preferredType = nil
|
|
end
|
|
store.projectiles.arrows = AB_EnsureRuleList(store.projectiles.arrows)
|
|
store.projectiles.bullets = AB_EnsureRuleList(store.projectiles.bullets)
|
|
return store
|
|
end
|
|
|
|
function Engine:GetBuyableProjectileItems(subtype)
|
|
local wanted = string.lower(tostring(subtype or ""))
|
|
if wanted ~= "arrow" and wanted ~= "bullet" then
|
|
return {}
|
|
end
|
|
|
|
local result = {}
|
|
local data = MTH_DS_AmmoItems or {}
|
|
for itemId, row in pairs(data) do
|
|
if type(row) == "table" and string.lower(tostring(row.subtype or "")) == wanted then
|
|
local vendorCount = 0
|
|
if type(row.vendors) == "table" then
|
|
for _ in pairs(row.vendors) do
|
|
vendorCount = vendorCount + 1
|
|
break
|
|
end
|
|
end
|
|
if vendorCount > 0 then
|
|
table.insert(result, {
|
|
itemId = tonumber(itemId) or itemId,
|
|
name = tostring(row.name or ("Item " .. tostring(itemId))),
|
|
level = tonumber(row.level) or 0,
|
|
reqlevel = tonumber(row.reqlevel) or 0,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(result, function(a, b)
|
|
if (a.level or 0) ~= (b.level or 0) then
|
|
return (a.level or 0) < (b.level or 0)
|
|
end
|
|
if (a.reqlevel or 0) ~= (b.reqlevel or 0) then
|
|
return (a.reqlevel or 0) < (b.reqlevel or 0)
|
|
end
|
|
if tostring(a.name or "") ~= tostring(b.name or "") then
|
|
return tostring(a.name or "") < tostring(b.name or "")
|
|
end
|
|
return tonumber(a.itemId or 0) < tonumber(b.itemId or 0)
|
|
end)
|
|
|
|
return result
|
|
end
|
|
|
|
function Engine:IsEnabled()
|
|
local store = self:EnsureDefaults()
|
|
return store.enabled and true or false
|
|
end
|
|
|
|
function Engine:Init()
|
|
self:EnsureDefaults()
|
|
end
|
|
|
|
function Engine:ScanMerchant()
|
|
if type(GetMerchantNumItems) ~= "function" then
|
|
return { count = 0, items = {} }
|
|
end
|
|
|
|
local result = {
|
|
count = GetMerchantNumItems() or 0,
|
|
items = {},
|
|
}
|
|
|
|
for index = 1, result.count do
|
|
local name, _, price, stackCount, numAvailable = GetMerchantItemInfo(index)
|
|
local link = GetMerchantItemLink(index)
|
|
local itemId = AB_ParseItemIdFromLink(link)
|
|
local subtype = AB_GetAmmoSubtypeByItemId(itemId)
|
|
if (not itemId or not subtype) and type(name) == "string" and name ~= "" then
|
|
local byNameItemId, byNameSubtype = AB_ResolveAmmoByName(name)
|
|
if byNameItemId and byNameSubtype then
|
|
itemId = byNameItemId
|
|
subtype = byNameSubtype
|
|
end
|
|
end
|
|
result.items[index] = {
|
|
index = index,
|
|
name = name,
|
|
link = link,
|
|
itemId = itemId,
|
|
subtype = subtype,
|
|
price = price,
|
|
stackCount = stackCount,
|
|
numAvailable = numAvailable,
|
|
}
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function Engine:BuildFoodPlan(merchantSnapshot)
|
|
local store = self:EnsureDefaults()
|
|
local plan = {
|
|
enabled = store.petFood and store.petFood.enabled and true or false,
|
|
actions = {},
|
|
reason = "",
|
|
debugLines = {},
|
|
}
|
|
|
|
local function dbg(line)
|
|
table.insert(plan.debugLines, tostring(line or ""))
|
|
end
|
|
|
|
if not plan.enabled then
|
|
plan.reason = "petfood-disabled"
|
|
dbg("petFood option disabled")
|
|
return plan
|
|
end
|
|
|
|
local targetStacks = math.floor(tonumber(store.petFood.stacks) or 0)
|
|
if targetStacks <= 0 then
|
|
plan.reason = "petfood-quantity-zero"
|
|
dbg("petFood target stacks <= 0")
|
|
return plan
|
|
end
|
|
|
|
local merchantFoods = AB_GetMerchantFoodsSnapshot(merchantSnapshot)
|
|
if type(merchantFoods) ~= "table" or type(merchantFoods.byDiet) ~= "table" then
|
|
plan.reason = "merchant-foods-missing"
|
|
dbg("merchant food scan missing")
|
|
return plan
|
|
end
|
|
|
|
local scope = store.petFood.scope == "all" and "all" or "current"
|
|
local petRows = AB_CollectPetRows(scope)
|
|
if table.getn(petRows) == 0 then
|
|
plan.reason = "no-pets-in-scope"
|
|
dbg("no pets in scope=" .. tostring(scope))
|
|
return plan
|
|
end
|
|
|
|
local families = {}
|
|
for i = 1, table.getn(petRows) do
|
|
local pet = petRows[i]
|
|
local dsRow, familyKey = AB_FindFamilyDietRow(pet.family)
|
|
if type(dsRow) == "table" and type(dsRow.food) == "table" and table.getn(dsRow.food) > 0 and familyKey then
|
|
if type(families[familyKey]) ~= "table" then
|
|
families[familyKey] = {
|
|
key = familyKey,
|
|
petCount = 0,
|
|
levelCap = tonumber(pet.level) or 0,
|
|
foodList = dsRow.food,
|
|
petNames = {},
|
|
candidates = {},
|
|
}
|
|
end
|
|
local family = families[familyKey]
|
|
family.petCount = family.petCount + 1
|
|
local petName = tostring(pet.name or "")
|
|
if petName ~= "" then
|
|
table.insert(family.petNames, petName)
|
|
end
|
|
local petLevel = tonumber(pet.level) or 0
|
|
if family.levelCap <= 0 then
|
|
family.levelCap = petLevel
|
|
elseif petLevel > family.levelCap then
|
|
family.levelCap = petLevel
|
|
end
|
|
else
|
|
dbg("skip pet with unknown family diet: " .. tostring(pet.name or "?") .. " family='" .. tostring(pet.family or "") .. "'")
|
|
end
|
|
end
|
|
|
|
local familyList = {}
|
|
local dietCoverage = {}
|
|
for _, family in pairs(families) do
|
|
for order = 1, table.getn(family.foodList or {}) do
|
|
local diet = AB_NormalizeDietToken(family.foodList[order])
|
|
if diet ~= "" then
|
|
local bestRow, bestLevel = AB_SelectBestFoodRowForDiet(merchantFoods.byDiet[diet], family.levelCap)
|
|
if bestRow then
|
|
table.insert(family.candidates, {
|
|
diet = diet,
|
|
order = order,
|
|
bestLevel = bestLevel,
|
|
})
|
|
dietCoverage[diet] = (dietCoverage[diet] or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
if table.getn(family.candidates) > 0 then
|
|
table.insert(familyList, family)
|
|
else
|
|
dbg("family has no vendor diet options: " .. tostring(family.key))
|
|
end
|
|
end
|
|
|
|
if table.getn(familyList) == 0 then
|
|
plan.reason = "no-family-food-match"
|
|
dbg("no families have compatible vendor foods")
|
|
return plan
|
|
end
|
|
|
|
table.sort(familyList, function(a, b)
|
|
if (a.petCount or 0) ~= (b.petCount or 0) then
|
|
return (a.petCount or 0) > (b.petCount or 0)
|
|
end
|
|
if table.getn(a.candidates or {}) ~= table.getn(b.candidates or {}) then
|
|
return table.getn(a.candidates or {}) < table.getn(b.candidates or {})
|
|
end
|
|
return tostring(a.key or "") < tostring(b.key or "")
|
|
end)
|
|
|
|
local assignedPetCountByDiet = {}
|
|
local chosenByFamily = {}
|
|
for i = 1, table.getn(familyList) do
|
|
local family = familyList[i]
|
|
local bestChoice = nil
|
|
for j = 1, table.getn(family.candidates) do
|
|
local candidate = family.candidates[j]
|
|
local scoreA = (assignedPetCountByDiet[candidate.diet] or 0) + (family.petCount or 0)
|
|
local scoreB = dietCoverage[candidate.diet] or 0
|
|
local scoreC = candidate.bestLevel or 0
|
|
local scoreD = -(candidate.order or 99)
|
|
if not bestChoice then
|
|
bestChoice = {
|
|
candidate = candidate,
|
|
scoreA = scoreA,
|
|
scoreB = scoreB,
|
|
scoreC = scoreC,
|
|
scoreD = scoreD,
|
|
}
|
|
else
|
|
local better = false
|
|
if scoreA > bestChoice.scoreA then
|
|
better = true
|
|
elseif scoreA == bestChoice.scoreA and scoreB > bestChoice.scoreB then
|
|
better = true
|
|
elseif scoreA == bestChoice.scoreA and scoreB == bestChoice.scoreB and scoreC > bestChoice.scoreC then
|
|
better = true
|
|
elseif scoreA == bestChoice.scoreA and scoreB == bestChoice.scoreB and scoreC == bestChoice.scoreC and scoreD > bestChoice.scoreD then
|
|
better = true
|
|
end
|
|
if better then
|
|
bestChoice = {
|
|
candidate = candidate,
|
|
scoreA = scoreA,
|
|
scoreB = scoreB,
|
|
scoreC = scoreC,
|
|
scoreD = scoreD,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
if bestChoice and bestChoice.candidate then
|
|
chosenByFamily[family.key] = {
|
|
diet = bestChoice.candidate.diet,
|
|
levelCap = family.levelCap,
|
|
petCount = family.petCount,
|
|
petNames = family.petNames,
|
|
}
|
|
assignedPetCountByDiet[bestChoice.candidate.diet] = (assignedPetCountByDiet[bestChoice.candidate.diet] or 0) + (family.petCount or 0)
|
|
dbg("family " .. tostring(family.key)
|
|
.. " -> diet " .. tostring(bestChoice.candidate.diet)
|
|
.. " (pets=" .. tostring(family.petCount)
|
|
.. ", levelCap=" .. tostring(family.levelCap)
|
|
.. ")")
|
|
end
|
|
end
|
|
|
|
local buckets = {}
|
|
for familyKey, chosen in pairs(chosenByFamily) do
|
|
local diet = chosen.diet
|
|
if type(buckets[diet]) ~= "table" then
|
|
buckets[diet] = {
|
|
diet = diet,
|
|
petCount = 0,
|
|
familyCount = 0,
|
|
levelCap = chosen.levelCap,
|
|
petNames = {},
|
|
}
|
|
end
|
|
local bucket = buckets[diet]
|
|
bucket.petCount = bucket.petCount + (chosen.petCount or 0)
|
|
bucket.familyCount = bucket.familyCount + 1
|
|
if type(chosen.petNames) == "table" then
|
|
for n = 1, table.getn(chosen.petNames) do
|
|
local candidateName = tostring(chosen.petNames[n] or "")
|
|
if candidateName ~= "" then
|
|
local exists = false
|
|
for e = 1, table.getn(bucket.petNames) do
|
|
if bucket.petNames[e] == candidateName then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
table.insert(bucket.petNames, candidateName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if tonumber(chosen.levelCap) and tonumber(chosen.levelCap) > 0 then
|
|
if (tonumber(bucket.levelCap) or 0) <= 0 or tonumber(chosen.levelCap) > tonumber(bucket.levelCap) then
|
|
bucket.levelCap = tonumber(chosen.levelCap)
|
|
end
|
|
end
|
|
dbg("bucket diet=" .. tostring(diet) .. " add family=" .. tostring(familyKey))
|
|
end
|
|
|
|
for dietName, bucket in pairs(buckets) do
|
|
local bestRow, bestLevel = AB_SelectBestFoodRowForDiet(merchantFoods.byDiet[dietName], bucket.levelCap)
|
|
if bestRow and bestRow.index then
|
|
local stackCount = math.floor(tonumber(bestRow.stack) or 1)
|
|
if stackCount < 1 then stackCount = 1 end
|
|
local bagStackSize = AB_GetItemMaxStack(bestRow.id, stackCount)
|
|
local targetItems = targetStacks * bagStackSize
|
|
local ownedItems = AB_CountDietItemsInBags(dietName)
|
|
local deficitItems = targetItems - ownedItems
|
|
local deficit = 0
|
|
if deficitItems > 0 then
|
|
deficit = math.ceil(deficitItems / stackCount)
|
|
end
|
|
dbg("diet=" .. tostring(dietName)
|
|
.. " targetStacks=" .. tostring(targetStacks)
|
|
.. " bagStackSize=" .. tostring(bagStackSize)
|
|
.. " vendorStackSize=" .. tostring(stackCount)
|
|
.. " targetItems=" .. tostring(targetItems)
|
|
.. " ownedItems=" .. tostring(ownedItems)
|
|
.. " deficitItems=" .. tostring(deficitItems)
|
|
.. " deficit=" .. tostring(deficit)
|
|
.. " selectedLevel=" .. tostring(bestLevel)
|
|
.. " levelCap=" .. tostring(bucket.levelCap)
|
|
.. " pets=" .. tostring(bucket.petCount)
|
|
.. " families=" .. tostring(bucket.familyCount))
|
|
if deficit > 0 then
|
|
table.insert(plan.actions, {
|
|
index = bestRow.index,
|
|
quantityStacks = deficit,
|
|
quantityItems = deficit * stackCount,
|
|
unitPrice = tonumber(bestRow.price) or 0,
|
|
itemId = tonumber(bestRow.id) or bestRow.id,
|
|
itemLink = bestRow.link,
|
|
itemName = bestRow.name,
|
|
diet = dietName,
|
|
targetStacks = targetStacks,
|
|
bagStackSize = bagStackSize,
|
|
vendorStackSize = stackCount,
|
|
targetItems = targetItems,
|
|
ownedItems = ownedItems,
|
|
deficitItems = deficitItems,
|
|
deficit = deficit,
|
|
stackCount = stackCount,
|
|
petCount = bucket.petCount,
|
|
familyCount = bucket.familyCount,
|
|
petNames = bucket.petNames,
|
|
})
|
|
end
|
|
else
|
|
dbg("diet " .. tostring(dietName) .. " has no compatible row at levelCap=" .. tostring(bucket.levelCap))
|
|
end
|
|
end
|
|
|
|
if table.getn(plan.actions) == 0 then
|
|
plan.reason = "no-deficit"
|
|
else
|
|
plan.reason = "ok"
|
|
end
|
|
|
|
return plan
|
|
end
|
|
|
|
function Engine:BuildAmmoPlan(merchantSnapshot)
|
|
local store = self:EnsureDefaults()
|
|
local plan = {
|
|
enabled = store.projectiles and store.projectiles.enabled and true or false,
|
|
actions = {},
|
|
reason = "",
|
|
debugLines = {},
|
|
}
|
|
|
|
local function dbg(line)
|
|
table.insert(plan.debugLines, tostring(line or ""))
|
|
end
|
|
|
|
if not plan.enabled then
|
|
plan.reason = "projectiles-disabled"
|
|
dbg("projectiles option disabled")
|
|
return plan
|
|
end
|
|
|
|
if type(merchantSnapshot) ~= "table" or type(merchantSnapshot.items) ~= "table" then
|
|
plan.reason = "merchant-snapshot-missing"
|
|
dbg("merchant snapshot missing")
|
|
return plan
|
|
end
|
|
|
|
local merchantByItemId = {}
|
|
local hasAmmoVendor = false
|
|
for i = 1, table.getn(merchantSnapshot.items) do
|
|
local row = merchantSnapshot.items[i]
|
|
if type(row) == "table" and row.itemId and row.subtype then
|
|
dbg("merchant row index=" .. tostring(row.index) .. " itemId=" .. tostring(row.itemId) .. " subtype=" .. tostring(row.subtype) .. " stack=" .. tostring(row.stackCount))
|
|
merchantByItemId[tonumber(row.itemId) or row.itemId] = row
|
|
hasAmmoVendor = true
|
|
end
|
|
end
|
|
|
|
if not hasAmmoVendor then
|
|
plan.reason = "not-ammo-vendor"
|
|
dbg("merchant has no recognized arrow/bullet rows")
|
|
return plan
|
|
end
|
|
|
|
local function countStacksInBags(itemId)
|
|
if not itemId then
|
|
return 0
|
|
end
|
|
local count = 0
|
|
local getSlots = _G and _G["GetContainerNumSlots"] or nil
|
|
local getLink = _G and _G["GetContainerItemLink"] or nil
|
|
if type(getSlots) ~= "function" or type(getLink) ~= "function" then
|
|
return 0
|
|
end
|
|
for bag = 0, 4 do
|
|
local slots = tonumber(getSlots(bag)) or 0
|
|
for slot = 1, slots do
|
|
local link = getLink(bag, slot)
|
|
local bagItemId = AB_ParseItemIdFromLink(link)
|
|
if bagItemId and tonumber(bagItemId) == tonumber(itemId) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
local function appendRuleActions(bucket, subtype)
|
|
if type(bucket) ~= "table" or type(bucket.rules) ~= "table" then
|
|
dbg(subtype .. " rules bucket missing")
|
|
return
|
|
end
|
|
for i = 1, table.getn(bucket.rules) do
|
|
local rule = bucket.rules[i]
|
|
if type(rule) == "table" and rule.enabled and rule.itemId then
|
|
local desiredStacks = math.floor(tonumber(rule.stacks) or 0)
|
|
if desiredStacks > 0 then
|
|
local merchantRow = merchantByItemId[tonumber(rule.itemId) or rule.itemId]
|
|
if merchantRow and merchantRow.subtype == subtype then
|
|
local ownedStacks = countStacksInBags(rule.itemId)
|
|
local deficit = desiredStacks - ownedStacks
|
|
dbg(subtype .. "#" .. tostring(i) .. " item=" .. tostring(rule.itemId)
|
|
.. " desired=" .. tostring(desiredStacks)
|
|
.. " owned=" .. tostring(ownedStacks)
|
|
.. " deficit=" .. tostring(deficit))
|
|
if deficit > 0 then
|
|
local merchantStack = math.floor(tonumber(merchantRow.stackCount) or 1)
|
|
if merchantStack < 1 then merchantStack = 1 end
|
|
local quantityStacks = deficit
|
|
local quantityItems = quantityStacks * merchantStack
|
|
table.insert(plan.actions, {
|
|
index = merchantRow.index,
|
|
quantityStacks = quantityStacks,
|
|
quantityItems = quantityItems,
|
|
unitPrice = tonumber(merchantRow.price) or 0,
|
|
itemId = rule.itemId,
|
|
itemLink = merchantRow.link,
|
|
itemName = merchantRow.name,
|
|
subtype = subtype,
|
|
desiredStacks = desiredStacks,
|
|
ownedStacks = ownedStacks,
|
|
deficit = deficit,
|
|
stackCount = merchantStack,
|
|
})
|
|
end
|
|
else
|
|
dbg(subtype .. "#" .. tostring(i) .. " item=" .. tostring(rule.itemId) .. " not sold by this vendor")
|
|
end
|
|
else
|
|
dbg(subtype .. "#" .. tostring(i) .. " desired stacks <= 0")
|
|
end
|
|
else
|
|
dbg(subtype .. "#" .. tostring(i) .. " disabled or no item")
|
|
end
|
|
end
|
|
end
|
|
|
|
appendRuleActions(store.projectiles.arrows, "arrow")
|
|
appendRuleActions(store.projectiles.bullets, "bullet")
|
|
|
|
if table.getn(plan.actions) == 0 then
|
|
plan.reason = "no-deficit"
|
|
else
|
|
plan.reason = "ok"
|
|
end
|
|
|
|
return plan
|
|
end
|
|
|
|
function Engine:ExecutePlan(plan)
|
|
if type(plan) ~= "table" or type(plan.actions) ~= "table" then
|
|
return 0, 0
|
|
end
|
|
local executedActions = 0
|
|
local requestedStacks = 0
|
|
for _, action in ipairs(plan.actions) do
|
|
if type(action) == "table" and action.index then
|
|
local stacksToBuy = math.floor(tonumber(action.quantityStacks) or 0)
|
|
if stacksToBuy > 0 then
|
|
for i = 1, stacksToBuy do
|
|
BuyMerchantItem(action.index)
|
|
end
|
|
executedActions = executedActions + 1
|
|
requestedStacks = requestedStacks + stacksToBuy
|
|
end
|
|
end
|
|
end
|
|
return executedActions, requestedStacks
|
|
end
|
|
|
|
local function AB_AnnouncePurchases(actions)
|
|
if type(actions) ~= "table" or table.getn(actions) == 0 then
|
|
return
|
|
end
|
|
|
|
local function formatMoney(money)
|
|
if MTH and MTH.FormatMoney then
|
|
return MTH:FormatMoney(money)
|
|
end
|
|
if type(MTH_FormatMoney) == "function" then
|
|
return MTH_FormatMoney(money)
|
|
end
|
|
return tostring(tonumber(money) or 0)
|
|
end
|
|
|
|
for i = 1, table.getn(actions) do
|
|
local action = actions[i]
|
|
if type(action) == "table" then
|
|
local stacks = math.floor(tonumber(action.quantityStacks) or 0)
|
|
if stacks > 0 then
|
|
local linkText = tostring(action.itemLink or "")
|
|
if linkText == "" then
|
|
linkText = tostring(action.itemName or ("item:" .. tostring(action.itemId or "?")))
|
|
end
|
|
local totalPrice = (tonumber(action.unitPrice) or 0) * stacks
|
|
local petsSuffix = ""
|
|
if type(action.petNames) == "table" and table.getn(action.petNames) > 0 then
|
|
local names = {}
|
|
for n = 1, table.getn(action.petNames) do
|
|
local petName = tostring(action.petNames[n] or "")
|
|
if petName ~= "" then
|
|
table.insert(names, petName)
|
|
end
|
|
end
|
|
if table.getn(names) > 0 then
|
|
table.sort(names)
|
|
if table.getn(names) == 1 then
|
|
petsSuffix = " " .. tostring(names[1]) .. " likes this !"
|
|
else
|
|
petsSuffix = " " .. table.concat(names, ", ") .. " like this !"
|
|
end
|
|
end
|
|
end
|
|
local line = "[AutoBuy] Bought " .. tostring(stacks)
|
|
.. " stack" .. (stacks > 1 and "s" or "")
|
|
.. " of " .. linkText
|
|
.. " for " .. formatMoney(totalPrice) .. "."
|
|
.. petsSuffix
|
|
if MTH and MTH.Print then
|
|
MTH:Print(line)
|
|
elseif type(MTH_Log) == "function" then
|
|
MTH_Log(line)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Engine:OnMerchantEvent(eventName)
|
|
|
|
if eventName == "MERCHANT_SHOW" then
|
|
self._merchantSessionExecuted = false
|
|
elseif eventName == "MERCHANT_UPDATE" and self._merchantSessionExecuted then
|
|
return
|
|
end
|
|
|
|
if not self:IsEnabled() then
|
|
return
|
|
end
|
|
|
|
local snapshot = self:ScanMerchant()
|
|
local foodPlan = self:BuildFoodPlan(snapshot)
|
|
local ammoPlan = self:BuildAmmoPlan(snapshot)
|
|
|
|
for i = 1, table.getn(foodPlan.debugLines or {}) do
|
|
end
|
|
for i = 1, table.getn(ammoPlan.debugLines or {}) do
|
|
end
|
|
|
|
local totalRequestedStacks = 0
|
|
if table.getn(foodPlan.actions or {}) > 0 then
|
|
local _, requestedStacks = self:ExecutePlan(foodPlan)
|
|
totalRequestedStacks = totalRequestedStacks + requestedStacks
|
|
if requestedStacks > 0 then
|
|
AB_AnnouncePurchases(foodPlan.actions)
|
|
end
|
|
end
|
|
|
|
if table.getn(ammoPlan.actions or {}) > 0 then
|
|
local _, requestedStacks = self:ExecutePlan(ammoPlan)
|
|
totalRequestedStacks = totalRequestedStacks + requestedStacks
|
|
if requestedStacks > 0 then
|
|
local moved = AB_MoveAmmoStacksToSpecialBags(300)
|
|
AB_AnnouncePurchases(ammoPlan.actions)
|
|
end
|
|
end
|
|
|
|
if totalRequestedStacks > 0 then
|
|
self._merchantSessionExecuted = true
|
|
else
|
|
end
|
|
end
|
|
|
|
function Engine:OnMerchantClosed()
|
|
self._merchantSessionExecuted = false
|
|
end
|
|
|
|
function MTH_AutoBuy_GetMerchantSnapshot()
|
|
return Engine:ScanMerchant()
|
|
end
|
|
|
|
function MTH_AutoBuy_GetProjectileItems(subtype)
|
|
return Engine:GetBuyableProjectileItems(subtype)
|
|
end
|