Quantcast
-- Copyright 2008 James Whitehead II
-- Credits for name 'DressToKill' go to Aska on StormReaver(EU)

DressToKill = {}
local L = DressToKillLocals

local DEBUG = false
local function debug(fmt, ...)
	local msg = "|cffffff78DressToKill:|r "
	if select("#", ...) > 0 then
		local succ,err = pcall(string.format, fmt, ...)
		if not succ then
			error(debugstack())
		else
			msg = msg .. err
		end
	else
		msg = msg .. fmt
	end
	if DEBUG then
		ChatFrame1:AddMessage(msg)
	end
	DressToKillDebugScrollFrame:AddMessage(msg)
end

function DressToKill:SetDebug(value)
	DEBUG = value
end

local function print(fmt, ...)
	local msg = "|cffffff78DressToKill:|r "
	if select("#", ...) > 0 then
		msg = msg .. fmt:format(...)
	else
		msg = msg .. fmt
	end
	ChatFrame1:AddMessage(msg)
end

local slotNames = {
	BackSlot = L["Back"],
	ChestSlot = L["Chest"],
	FeetSlot = L["Feet"],
	Finger0Slot = L["Left Finger"],
	Finger1Slot = L["Right Finger"],
	HeadSlot = L["Head"],
	HandsSlot = L["Hands"],
	LegsSlot = L["Legs"],
	NeckSlot = L["Neck Slot"],
	RangedSlot = L["Ranged Slot"],
	ShoulderSlot = L["Shoulders"],
	Trinket0Slot = L["Trinket 0"],
	Trinket1Slot = L["Trinket 1"],
	WaistSlot = L["Waist"],
	WristSlot = L["Wrist"],
	--MainHandSlot = L["Main Hand"],
	--SecondaryHandSlot = L["Secondary Hand"],
}

local slotIds = {}

function DressToKill:Initialize()
	-- Only run this code once
	if self.initDone then return end
	self.initDone = true

	for slot in pairs(slotNames) do
		local id = GetInventorySlotInfo(slot)
		if id then
			slotIds[slot] = id
		else
			local err = string.format(L["Could not get inventory slot information for %s"], tostring(slot))
			error(err)
			return
		end
	end

	DressToKillDB = DressToKillDB or {}
	self.profile = DressToKillDB
	self.profile.weightFuncs = self.profile.weightFuncs or {
		["Healing/Mana"] = {handler = [[local healing = GetSpellBonusHealing()
local mana = UnitManaMax("player")
return (healing * 1.0) + (mana * 0.7)]]},
		["Health/Armor"] = {handler = [[local health = UnitHealthMax("player")
local base, pos, neg = UnitArmor("player")
local armor = base + pos - neg
return (health * 1.0) + (armor * 0.8)]]},
		["Attack Power"] = {handler = [[local base, pos, neg = UnitAttackPower("player")
local apower = base + pos - neg
return apower]]},
	}
end

function DressToKill:FindEmptySlot()
	for bag=0,4 do
		for slot=1,GetContainerNumSlots(bag) do
			if not GetContainerItemInfo(bag, slot) then
				return bag, slot
			end
		end
	end
end

local function scanFunction(weightFunction)
	debug(L["********** Beginning an inventory scan **********"])
	UIErrorsFrame:AddMessage("Please do not change buffs or forms during scan...", 1, 0.2, 0.2)

	local blacklist = {}
	local slotAvail = {}
	for slotName,slotId in pairs(slotIds) do
		debug(L["Collecting available items for %s"], slotName)
		local avail = GetInventoryItemsForSlot(slotId)
		if next(avail) then
			local c = 0
			for k,v in pairs(avail) do c = c + 1 end
			debug(L["Found %d available items"], c)
			slotAvail[slotId] = avail
		else
			debug(L["Found no available items"])
		end
	end

	-- Let's handle the weapons, since they're somewhat complicated
	local mainslot = GetInventorySlotInfo("MainHandSlot")
	local offslot = GetInventorySlotInfo("SecondaryHandSlot")

	-- Unequip both weapon slots
	local mh_link = GetInventoryItemLink("player", mainslot)
	local oh_link = GetInventoryItemLink("player", offslot)

	debug(L["Mainhand link: %s"], tostring(mh_link))
	debug(L["Offhand link: %s"], tostring(oh_link))

	if mh_link then
		debug(L["Found a mainhand weapon: %s"], mh_link)
		local mh_bag, mh_slot = DressToKill:FindEmptySlot()
		if not mh_bag then
			print(L["Out of inventory space, cannot proceed"])
			return
		end
		debug(L["Unequipping the mainhand weapon"])
		PickupInventoryItem(mainslot)
		PickupContainerItem(mh_bag, mh_slot)
		coroutine.yield()
	end

	if oh_link then
		debug(L["Found an offhand weapon: %s"], oh_link)
		local oh_bag, oh_slot = DressToKill:FindEmptySlot()
		if not oh_bag then
			print(L["Out of inventory space, cannot proceed"])
			return
		end
		debug(L["Unequipping the offhand weapon"])
		PickupInventoryItem(offslot)
		PickupContainerItem(oh_bag, oh_slot)
		coroutine.yield()
	end

	local mh_avail = GetInventoryItemsForSlot(mainslot)
	local oh_avail = GetInventoryItemsForSlot(offslot)

	-- Calculate weapon base score with nothing equipped
	local linen_shirt = "\124cffffffff\124Hitem:2576:0:0:0:0:0:0:0\124h[White Linen Shirt]\124h\124r"
	local weapon_max, weapon_win = (- math.huge), {}

	-- Try on each mainhand weapon
	local score
	for mh_mask,item in pairs(mh_avail) do
		local name, link = GetItemInfo(item)
		debug(L["Equipping mainhand weapon: %s"], link)

		local mh_equipped = DressToKill:EquipItem(mainslot, mh_mask)
		blacklist[mh_mask] = true

		if mh_equipped then
			debug(L["Successfully equipped %s"], link)
			local mh_link = GetInventoryItemLink("player", mainslot)
			local mh_score = weightFunction(mh_link, mainslot)
			debug(L["Mainhand score: %d"], mh_score)

			if not IsEquippedItemType("Two-Hand") then
				debug(L["This is a one hand weapon, try offhand weapons"])
				-- Try to equip each off-hand weapon to compliment this one
				for oh_mask,item in pairs(oh_avail) do
					if not blacklist[oh_mask] then
						score = mh_score
						local name, link = GetItemInfo(item)

						debug(L["Equipping offhand weapon: %s"], link)
						local oh_equipped = DressToKill:EquipItem(offslot, oh_mask)

						-- If we equipped offhand, then score it too
						if oh_equipped then
							local oh_link = GetInventoryItemLink("player", offslot) or linen_shirt
							local oh_score = weightFunction(oh_link, offslot)
							score = oh_score
							debug(L["Got score of %s for %s/%s"], score, mh_link, oh_link)
						else
							debug(L["Failed to equip %s"], link)
						end

						if score >= weapon_max then
							weapon_max = score
							weapon_win.mh = mh_mask
							weapon_win.oh = oh_equipped and oh_mask or nil
						end

						-- Unequip the offhand item
						if oh_equipped then
							debug(L["Unequipping %s"], link)
							DressToKill:UnequipItem(offslot, oh_mask, oh_stash)
						end
					end
				end
			else
				score = mh_score
				debug("Got score of %s for %s", score, mh_link)
				if score >= weapon_max then
					weapon_max = score
					weapon_win.mh = mh_mask
					weapon_win.oh = nil
				end
			end

			-- Unequip this mainhand item
			DressToKill:UnequipItem(mainslot, mh_mask, mh_stash)
			blacklist[mh_mask] = false
		end
	end

	-- Equip the weapon winners (MAINHAND)
	DressToKill:EquipItem(mainslot, weapon_win.mh, mh_stash)
	local link = GetInventoryItemLink("player", mainslot)
	print("Choosing %s", link)
	debug("Choosing %s", link)
	blacklist[weapon_win.mh] = true

	-- Equip the offhand winner, if there was one
	if weapon_win.oh then
		DressToKill:EquipItem(offslot, weapon_win.oh, oh_stash)
		local link = GetInventoryItemLink("player", offslot)
		print("Choosing %s", link)
		blacklist[weapon_win.oh] = true
	end

	local stash = {}
	-- Loop through all of the inventory slots
	for slotId,avail in pairs(slotAvail) do
		local maxScore, winner = (- math.huge), nil
		local link = GetInventoryItemLink("player", slotId)
		if link then
			-- We current have an item equipped, so stash that item elsewhere
			local bag,slot = DressToKill:FindEmptySlot()
			if not bag or not slot then
				print(L["Out of inventory space, cannot proceed"])
				return
			end

			stash.bag = bag
			stash.slot = slot
			PickupInventoryItem(slotId)
			PickupContainerItem(bag, slot)
			coroutine.yield()
		end

		-- Get the base score for this slot, so we can normalize our scores
		local linen_shirt = "\124cffffffff\124Hitem:2576:0:0:0:0:0:0:0\124h[White Linen Shirt]\124h\124r"
		local base = weightFunction(linen_shirt, slotId) or 0

		-- Loop through all of the available items, and try equipping each of them
		for mask,item in pairs(avail) do
			if not blacklist[mask] then
				local name,link = GetItemInfo(item)
				debug(L["Trying on %s"], link)
				local equipped = DressToKill:EquipItem(slotId, mask, stash)
				if equipped then
					local link = GetInventoryItemLink("player", slotId)
					local score = weightFunction(link, slotId) - base
					debug("Got score of %s for %s", score, link)
					if score >= maxScore then
						maxScore = score
						winner = mask
					end

					-- Unequip the item
					DressToKill:UnequipItem(slotId, mask, stash)
				else
					debug(L["Item not equipped... skipping..."])
				end
			else
				debug(L["Item blacklisted, skipping..."])
			end
		end
		-- Now equip the item that won this round and blacklist assuming there was a winner
		if winner then
			DressToKill:EquipItem(slotId, winner, stash)
			local link = GetInventoryItemLink("player", slotId)
			debug(L["Choosing %s for this slot"], link)
			print(L["Choosing %s"], link)
			blacklist[winner] = true
		else
			debug(L["There was no winner from this round, not enough trinkets or rings?"])
		end
	end
	UIErrorsFrame:AddMessage("Scan complete!", 0.2, 1, 0.2)
	print("Evaluation complete!")
	debug(L["Evaluation complete!"])
end

function DressToKill:EquipItem(slotId, mask, stash)
	local bag,slot = DressToKill:ItemInBag(mask)
	local eqslot = DressToKill:ItemEquipped(mask)
	if bag then
		-- Equip the item
		PickupContainerItem(bag, slot)
		EquipCursorItem(slotId)
		local equipped = coroutine.yield()
		return equipped
	elseif eqslot then
		-- The item was equipped, so find the current slot in the stash
		PickupContainerItem(stash.bag, stash.slot)
		EquipCursorItem(slotId)
		local equipped = coroutine.yield()
		return equipped
	end
end

function DressToKill:UnequipItem(slotId, mask, stash)
	local bag,slot = DressToKill:ItemInBag(mask)
	local eqslot = DressToKill:ItemEquipped(mask)
	if bag then
		-- Just put the item back
		PickupInventoryItem(slotId)
		PickupContainerItem(bag, slot)
		coroutine.yield()
	elseif eqslot then
		-- Put it back into the stash slot
		PickupInventoryItem(slotId)
		PickupContainerItem(stash.bag, stash.slot)
		coroutine.yield()
	end
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
eventFrame:RegisterEvent("EQUIP_BIND_CONFIRM")
eventFrame:RegisterEvent("UI_ERROR_MESSAGE")

--local LEVEL_LOW = LEVEL_TOO_LOW:gsub("%%d", "%%d%+"):gsub("%.", "%%.")

local function OnEvent(self, event, arg1)
	self.equipped = true
	local thread = DressToKill.currentThread
	if thread and coroutine.status(thread) ~= "dead" then
		if event == "EQUIP_BIND_CONFIRM" and CursorHasItem() then
			self.equipped = false
			debug(L["Clearing non-soulbound item"])
			ClearCursor()
		elseif event == "UI_ERROR_MESSAGE" and arg1 == ERR_PROFICIENCY_NEEDED then
			self.equipped = false
			debug(L["Clearing item we don't have the proficiency for"])
			ClearCursor()
		elseif event == "UI_ERROR_MESSAGE" and arg1 == ERR_2HSKILLNOTFOUND then
			self.equipped = false
			debug(L["Clearing item since we lack dual wielding skill"])
			ClearCursor()
		elseif event == "UI_ERROR_MESSAGE" and arg1 == ERR_CANT_EQUIP_SKILL then
			self.equipped = false
			debug(L["Clearing item since we lack the skill to equip it"])
			ClearCursor()
		elseif event == "UI_ERROR_MESSAGE" then
			self.equipped = false
			debug(L["Clearing item due to API saying it is valid for an invalid slot"])
			ClearCursor()
		elseif event == "UI_ERROR_MESSAGE" then
			debug(L["Got an UI error message: %s"], arg1)
		end

		self:Show()
	end
end

local function OnUpdate(self, elapsed)
	self:Hide()
	local succ,err = coroutine.resume(DressToKill.currentThread, self.equipped)
	assert(succ, err)
end

eventFrame:SetScript("OnEvent", OnEvent)
eventFrame:SetScript("OnUpdate", OnUpdate)
eventFrame:Hide()

SLASH_DRESSTOKILL1 = "/dtk"
SLASH_DRESSTOKILL2 = "/dress"
SLASH_DRESSTOKILL3 = "/dresstokill"

SlashCmdList["DRESSTOKILL"] = function(msg, editbox)
	DressToKill:Initialize()
	if msg and msg:lower():match("%s*config%s*") then
		InterfaceOptionsFrame_OpenToFrame(DressToKillOptionsFrame)
		return
	end

	if msg and msg:lower():match("%s*debug%s*") then
		InterfaceOptionsFrame_OpenToFrame(DressToKillDebugFrame)
		return
	end

	-- Check to see if a function was specified on the commandline
	local weightName = msg:match("^%s*(.+)$")
	if not weightName then
		if DressToKill.profile.selected then
			weightName = DressToKill.profile.selected
		else
			print(L["You don't have a default weight function selected"])
			return
		end
	end

	-- Make sure the function actually exist
	if not DressToKill.profile.weightFuncs[weightName] then
		print(L["The weight function '%s' doesn't exist"], weightName)
		return
	end

	-- Compile the function
	local source = DressToKill.profile.weightFuncs[weightName].handler
	local newsrc = string.format("return function (link, slot) %s end", source)
	local weightFunction,err = loadstring(newsrc)

	if not weightFunction then
		print("Failed to compile weight function: " .. tostring(err))
		return
	else
		local succ,err = pcall(weightFunction)
		if not succ then
			print("Failed when running weight function: " .. tostring(err))
			return
		else
			weightFunction = err
		end
	end

	print(L["Dressing to kill with %s..."], weightName)
	DressToKill.currentThread = coroutine.create(scanFunction)
	coroutine.resume(DressToKill.currentThread, weightFunction)
end

local ITEM_INVENTORY_PLAYER = 0x00100000
local ITEM_INVENTORY_BACKPACK = 0x00200000
local ITEM_INVENTORY_BAGS = 0x00400000
local ITEM_INVENTORY_BANK = 0x00800000
local MASK_BAG = 0xf00
local MASK_SLOT = 0x3f
local bagMap = {
	[0x100] = 1,
	[0x200] = 2,
	[0x400] = 3,
	[0x800] = 4,
}

function DressToKill:ItemInBank(mask)
	if bit.band(mask, ITEM_INVENTORY_BANK) > 0 then
	end
end

function DressToKill:ItemInBag(mask)
	if bit.band(mask, ITEM_INVENTORY_BAGS) > 0 then
		local bag = bagMap[bit.band(mask, MASK_BAG)]
		local slot = bit.band(mask, MASK_SLOT)
		return bag, slot
	elseif bit.band(mask, ITEM_INVENTORY_BACKPACK) > 0 then
		local slot = bit.band(mask, MASK_SLOT)
		return 0, slot
	end
end

function DressToKill:ItemEquipped(mask)
	if bit.band(mask, ITEM_INVENTORY_PLAYER) > 0 then
		local slot = bit.band(mask, MASK_SLOT)
		return slot
	end
end

--[[
ITEM_INVENTORY_LOCATION_PLAYER          = 0x00100000
ITEM_INVENTORY_LOCATION_BACKPACK        = 0x00200000
ITEM_INVENTORY_LOCATION_BAGS            = 0x00400000
ITEM_INVENTORY_LOCATION_BANK            = 0x00800000

--
-- With four netherweave bags:
-- In backpack, slots 1 -> 16
--
-- 001100000000000000000001
-- 001100000000000000000010
-- ...
-- 001100000000000000010000
--
-- First bag, slots 1 -> 16
--
-- 010000000000000100000001
-- ...
-- 010000000000000100010000
--
-- Second bag, slots 1 -> 16
--
-- 010000000000001000000001
-- ...
-- 010000000000001000010000
--
-- Third bag, slots 1 -> 16
--
-- 010000000000010000000001
-- ...
-- 010000000000010000010000
--
-- Fourth bag, slots 1 -> 16
--
-- 010000000000100000000001
-- ...
-- 010000000000100000010000
--
-- Hands(10)
-- 000100000000000000001010
--
-- Waist(6)
-- 000100000000000000000110
--
-- Finger0(11)
-- 000100000000000000001011
--
-- Finger1(12)
-- 000100000000000000001011
--]]