Thursday, April 19, 2012

MobXP Estimation

I'm having some trouble saving tables.

The project i'm working on saves a database of mob xp, in conjunction

with determining a how many times a mob must be killed before you will

level up. I'm not sure if i'm coding this properly, however. The database

usually ends up "nil", or with incomplete/corrupt information.

Also, I have set this project up to catch what a player's last known

target was, and when his/her xp is updated, it assumes the last target

is where the xp comes from, and the database is updated accordingly.

However, this is unfortunately not always the case within the game.

For example, AoE spells don't need a target. Any better solutions?


Code:
-- TYPE DECLARATIONS
local GrindXP_Options = {
type='group',
args = {}
}

-- VARIABLE DECLARATIONS
local GrindXP_DBVersion = {
["Author"] = "MikeTV",
["Month"] = "01",
["Year"] = "2008",
["Major"] = 1,
["Minor"] = 0,
["Revision"] = 0,
}
local GrindXP_FirstLoad = false
local GrindXP_Unit = {}
local GrindXP_UnitName = "nil"
local GrindXP_OldXP = 0
local GrindXP_NewXP = 0
local GrindXP_OldXPMax = 0
local GrindXP_NewXPMax = 0
local GrindXP_OldLevel = 0
local GrindXP_NewLevel = 0
local me = nil;

-- LIBRARY INITIALIZATIONS
GrindXP = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", "AceDB-2.0")
GrindXP:RegisterChatCommand("/GrindXP", "/xp", GrindXP_Options)
GrindXP:RegisterDB("GrindXP_DB")

-- FUNCTIONS
function GrindXP:OnInitialize()
me = self
GrindXP:Print("[DEBUG]: AddOn initialized...")
end

function GrindXP:OnEnable()
me:RegisterEvent("VARIABLES_LOADED")
me:RegisterEvent("QUEST_PROGRESS")
me:RegisterEvent("QUEST_FINISHED")
me:RegisterEvent("PLAYER_TARGET_CHANGED")
me:RegisterEvent("PLAYER_XP_UPDATE")
GrindXP:Print("[DEBUG]: AddOn enabled...")
end

function GrindXP:OnDisable()
me:UnregisterEvent("PLAYER_XP_UPDATE")
me:UnregisterEvent("PLAYER_TARGET_CHANGED")
me:UnregisterEvent("QUEST_FINISHED")
me:UnregisterEvent("QUEST_PROGRESS")
me:UnregisterEvent("VARIABLES_LOADED")
GrindXP:Print("[DEBUG]: AddOn disabled...")
end

function GrindXP:VARIABLES_LOADED()
if ((not GrindXP_DB) or (GrindXP_DB == nil)) then
GrindXP_DB = {
["DBVersion"] = GrindXP_DBVersion
}
GrindXP:Print("[DEBUG]: First-time user detected...")
GrindXP_FirstLoad = true
end
if (not GrindXP_DB["DBVersion"]) then
GrindXP:Print("[ERROR]: Database contains no version information!")
return false
end
GrindXP:Print("[DEBUG]: AddOn variables loaded...")
end

function GrindXP:QUEST_PROGRESS()
me:UnregisterEvent("PLAYER_TARGET_CHANGED")
GrindXP:Print("[DEBUG]: Unit tracking disabled <QUEST_PROGRESS>")
end

function GrindXP:QUEST_FINISHED()
me:RegisterEvent("PLAYER_TARGET_CHANGED")
GrindXP:Print("[DEBUG]: Unit tracking enabled <QUEST_FINISHED>")
end

function GrindXP:PLAYER_TARGET_CHANGED()
GrindXP_UpdateUnit()
GrindXP:Print("[DEBUG]: Unit updated...")
end

function GrindXP:PLAYER_XP_UPDATE()
GrindXP:Print("[DEBUG]: Experience update detected...")
GrindXP:Print(" [Name]: "..ToString(GrindXP_Unit["Name"]))
GrindXP:Print(" [Level]: "..ToString(GrindXP_Unit["Level"]))
GrindXP:Print(" [Race]: "..ToString(GrindXP_Unit["Race"]))
GrindXP:Print(" [Class]: "..ToString(GrindXP_Unit["Class"]))
GrindXP:Print(" [Rank]: "..ToString(GrindXP_Unit["Rank"]))
GrindXP_Unit["Player"]["PostKill"] = {
["Level"] = UnitLevel("player"),
["XP"] = UnitXP("player"),
["XPMax"] = UnitXPMax("player"),
["XPRest"] = GetXPExhaustion(),
["Zone"] = GetZoneText(),
["SubZone"] = GetSubZoneText(),
}
GrindXP_UpdateDatabase()
end

function ToString(value)
if (value == nil) then
return "nil"
elseif (value == true) then
return "true"
elseif (value == false) then
return "false"
else
return ""..value..""
end
end

function GrindXP_UpdateUnit()
-- Update the target unit information
GrindXP_UnitName = UnitName("target")
GrindXP_Unit = {
["Level"] = UnitLevel("target"),
["Race"] = UnitRace("target"),
["Class"] = UnitClass("target"),
["Rank"] = UnitClassification("target"),
["Player"] = {
["Name"] = UnitName("player"),
["Race"] = UnitRace("player"),
["Class"] = UnitClass("player"),
["Rank"] = UnitClassification("player"),
["PreKill"] = {
["Level"] = UnitLevel("player"),
["XP"] = UnitXP("player"),
["XPMax"] = UnitXPMax("player"),
["XPRest"] = GetXPExhaustion(),
["Zone"] = GetZoneText(),
["SubZone"] = GetSubZoneText(),
},
["PostKill"] = {
["Level"] = nil,
["XP"] = nil,
["XPMax"] = nil,
["XPRest"] = nil,
["Zone"] = nil,
["SubZone"] = nil,
}
}
}
end

function GrindXP_UpdateDatabase()
-- Make sure the target unit is valid for database storage
if (not GrindXP_UnitName) then
me:Print("[DEBUG]: No unit specified...")
return false
end
if (GrindXP_DB[GrindXP_UnitName]) then
me:Print("[DEBUG]: Unit data already exists...")
return false
end
if (not GrindXP_Unit) then
me:Print("[ERROR]: Unit is invalid...")
return false
end

-- Add target unit to the database
GrindXP_DB[GrindXP_UnitName] = GrindXP_Unit
GrindXP:Print("[DEBUG]: Database updated...")
return true
end
|||This sounds like it could get quite complicated, but assuming your only worrying about SOLO-grinding, then here are some thoughts...

You could try to discard XP gained from AoE / Pet deaths, etc. and concentrate only on estimating XP gained from your target's death, by checking that when you gain XP, your current target is at 0% health or less(?)

I'm not certain that this would help in cases where you are fighting multiple Mobs, as I think you will auto-target the next live Mob who is aggroed...and you would need to do some testing to figure out whether this would still work or not.

i.e. its possible you would auto-change target to a live mob BEFORE the XP gain is reported...

Also you might want some checks on what type of Target your new target is. It can be quite easy to accidentally target your Pet, or another Mob, or a neutral Mob/NPC or even yourself.

Also, I believe the event will fire when you lose a target...

In any of these cases you shouldn't update GrindXP_UnitName or GrindXP_Unit|||I have an idea... It's complex, but it could work:

Perhaps I could implement a unit buffer which keeps track of units which you, your pet, and/or a party members successfully damage, and attempt to link a unit's death accordingly? This could be done by monitoring combat messages...

Something like:


Code:
local buffer = {} -- Unit Buffer
local bufferLen = 3 -- Buffer length
local bufferPos = 0 -- Buffer position

-- Fires whenever you, your pet, or a party member damages a unit
function UpdateUnitBuffer(DamagedUnit)
buffer[bufferPos] = DamagedUnit -- Set the buffer position to be the most recent damaged unit
bufferPos = bufferPos + 1 -- Increment the buffer position
if (bufferPos > bufferLen) then -- Once we reach the maximum buffer length, then...
bufferPos = 0 -- We could overwrite units from the beginning
end
end

The unit table would have to be well organized and include various circumstances whenever a damage event fires:
  • Were you in a group?

  • If yes, what were the names in your group?

  • Who hit what? (You, your pet, or a party member)

  • Is the unit dead yet? (Parse table via chat messages)

  • Has the operation timed out yet? (If a unit never dies, no reason to keep it buffered!)

And of course, for performance issues, bufferLen could always be configurable via the settings dialog. Pretty sweet, eh?

But i'm still having trouble properly saving nested tables... ='(|||Obviously I'm not spending as much time thinking about this as you, but here are some more thoughts ;)

WoWWiki doesn't mention any arguments for the PLAYER_XP_UPDATE event, but have you tried testing the values of arg1, arg2, etc. ?

I'd be surprised if it doesn't pass the amount of XP gained, which would save you some trouble and would be worth a quick test.

What happens when you are fighting 3 mobs with the same name but that are all different Levels of Mob that give different XP. When one of them dies from AoE while not targetted by anyone - the only record of death is a message in the combat log and the XP Event, but you can't work out from the combat log which of the 3 mobs has died...

Unless you are going to key your buffer by Name and Level...

I guess if 2 of the mobs have the same name & level, then you can assume that they will give the same xp, and it doesn't really matter which you remove from your buffer...

(I guess you'll need to do a complete buffer reset should the Player actually gain a Level in the middle of combat)

Unfortunately, if you're going to build your buffer from the Combat log, I don't think you'll be able to get the Level for the Mob that was damaged...

None of my AddOns are combat mods, so I'm never sure about what level of targetting is allowed from AddOns while in combat - I know you can't target an explicit Mob, but can you still cycle through available targets ?

If you can, then you could try to build/refresh your buffer this way, although I'm not sure how you would know for sure when you had reached the first Mob again, as they don't have unique identifiers(?), and what happens when they move in and out of targetting range...

In fact now, I'm really not sure how you would ever reliably build a buffer of aggroed Mobs, and you would only know how many you fought by counting "Mob has died..." messages in the combat log between combat starting and finishing.

Perhaps you could Focus on the first Mob you Target, and once you've cycled through all targets and reached the Mob with Focus again, then you know you've got all targettable Mobs within Range in a buffer....?

From a soloing point of view (and assuming that you target each mob you kill at least once), then you might be able to do something like the following :


Code:

local MobBuffer;
local LogBuffer;

-- when entering combat with clean buffers (i.e. arrays)
-- store the Name/Level/etc. of each Mob that you Target in MobBuffer
-- store each "You have slain [Mob Name]" combat log message in a LogBuffer
-- when you receive an _XP_UPDATE
-- try to associate it with the last "You have slain [Mob Name]" message
-- and store it in the LogBuffer against a Mob that has no XP yet

-- when exiting combat
-- go through the LogBuffer sorted by Mob Name and amount of XP gained
-- try to associate the smallest amount of XP gained for a Mob with the lowest
Level in MobBuffer and
-- for LogBuffer entries for the same Mob with the same XP look for
MobBuffer entries with the same name and Level
-- for LogBuffer entries for the same Mob with more XP look for
MobBuffer entries with the same name and a higher level
-- Repeat for each differently named Mob.....
-- driving the loop from the LogBuffer allows for an unreliable MobBuffer.....

-- Clean down the arrays before the next combat


-- this code asssumes the _XP_ Event is received AFTER the Combat Log message...

Whether this will work, and how reliably it could be implemented when in a Group, I can't be bothered thinking about atm ;)

....these are all just my vague meandering thoughts....wibble.|||Thanks for the replies. I've been working on this post since just before your last reply, Telic, but you beat me to the punch

Several problems solved, and things are becoming rather intuitive now...

The unit buffer is pushed and popped via mouseovers, and destroyed through timeout timers. So a player could, for example, set the buffer length to 25 and the timeout to 120 seconds and start waving the mouse around like a magic wand, and the buffer will fill up with unit data, creating unique IDs for each unit, up to a maximum of 25 units (or whatever the buffer length setting is). If a unit dies, it is linked with the respective data in the unit buffer, and actions are carried out to update the database and remove the unit from the buffer. If no matching units die, then the units are respectively removed from the buffer after exactly 2 minutes (120 seconds, or whatever the unit timeout settings are) of inactivity -- thus clearing up memory, as well as space for another unit query.

The REAL Problem:

Just as you pretty much put it, I can't seem to disambiguate units. Is there a way to uniquely identify a unit? Kind of like how items can be uniquely identified ("Item-Links")? The trouble here is that a unit's properties tend to change dynamically -- especially during combat -- whereas items are usually just static objects. The best thing I can think of is to store it's name, level, and base stats, although I'm not sure if a unit's base stats can change. Currently, I'm simply formatting it's base stats as "%d:%d:%d:%d:%d" and using the resulted string as it's unique ID, but i'm not sure how effective this is. Not to mention, it isn't entirely unique.

Anyhow, here's what i've come up with so far:


Code:
-- VARIABLE DECLARATIONS
local Grind_buffer = {} -- Unit Buffer
local Grind_bufferLen = 10 -- Buffer length (Set lower to improve performance)
local Grind_bufferPos = 0 -- Buffer position
local Grind_unitTimeout = 60 -- Amount of time (in seconds) to preserve unit within the buffer
local Grind_currentTime = 0 -- Stores a snapshot of the current time (in seconds) each frame
local me = nil;

-- LIBRARY INITIALIZATIONS
Grind = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", "AceDB-2.0")
Grind:RegisterDB("Grind_UnitDB", "Grind_UnitBuffer")

-- FUNCTIONS
function Grind:OnInitialize()
me = self
Grind:Print("Initialized...")
end

function Grind:OnEnable()
me:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH")
me:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
me:RegisterEvent("PLAYER_LEAVING_WORLD")
me:ScheduleRepeatingEvent("Grind:OnUpdate", me.OnUpdate, 1, me)
Grind:Print("Enabled...")
end

function Grind:OnUpdate()
Grind_currentTime = time()
Grind_CheckUnitBuffer()
end

function Grind:OnDisable()
me:CancelScheduledEvent("Grind:OnUpdate")
Grind:Print("Disabled...")
end

function Grind:CHAT_MSG_COMBAT_HOSTILE_DEATH()
Grind:Print("CHAT_MSG_COMBAT_HOSTILE_DEATH ("..tostring(arg1)..")")
end

function Grind:UPDATE_MOUSEOVER_UNIT()
Grind_UpdateUnitBuffer(Grind_GetUnit("mouseover", "UPDATE_MOUSEOVER_UNIT"))
Grind:Print("UPDATE_MOUSEOVER_UNIT")
end

function Grind:PLAYER_LEAVING_WORLD()
Grind_UnitBuffer = Grind_buffer -- Saves unit buffer before logging off
Grind:Print("PLAYER_LEAVING_WORLD")
end

-- Checks the unit buffer for timeouts, redundancies, etc, and removes them accordingly
function Grind_CheckUnitBuffer()
local l = #Grind_buffer
local mUnit = {}
local n = 0
for i=0,l do
mUnit = Grind_buffer[i]
if (mUnit) then
n = (Grind_currentTime - mUnit["Time"])
if (n > Grind_unitTimeout) then
if (l > 1) then
table.remove(Grind_buffer, i)
Grind:Print("Unit buffer #"..tostring(i).." removed")
else
Grind_buffer = {}
Grind:Print("Unit buffer destroyed")
end
Grind_bufferPos = Grind_bufferPos - 1 -- Decrement the buffer position
if (Grind_bufferPos < 0) then Grind_bufferPos = Grind_bufferLen end
end
end
end
end

-- Checks the unit buffer to see if a unit already exists within it
function Grind_UnitExistsInBuffer(QueriedUnit)
local l = #Grind_buffer
for i=0,l do
if (QueriedUnit == Grind_buffer[i]) then return true end
end
return false
end

-- Updates the unit buffer, adding new entries wherever neccesary
function Grind_UpdateUnitBuffer(QueriedUnit)
if (Grind_UnitExistsInBuffer(QueriedUnit)) then return false end -- For unit disambiguation
Grind_buffer[Grind_bufferPos] = QueriedUnit -- Set the buffer position to be the most recently damaged unit
Grind_bufferPos = Grind_bufferPos + 1 -- Increment the buffer position
if (Grind_bufferPos > Grind_bufferLen) then -- Once we reach the maximum buffer length, then...
Grind_bufferPos = 0 -- We can overwrite units from the top of the list
end
Grind:Print("Unit buffer #"..tostring(Grind_bufferPos).." updated")
end

-- Checks if a unit is alive
function UnitIsAlive(mUnitID)
return (not UnitIsDead(mUnitID))
end

-- Return the unit's base stats in a formatted string
function UnitStats(mUnitID)
return format("%d:%d:%d:%d:%d", UnitStat(mUnitID, 1), UnitStat(mUnitID, 2), UnitStat(mUnitID, 3), UnitStat(mUnitID, 4), UnitStat(mUnitID, 5))
end

-- Creates a unit table based upon the passed UnitID
function Grind_GetUnit(mUnitID, sourceEvent)
if UnitExists(mUnitID) and UnitIsAlive(mUnitID) then
if ((tostring(sourceEvent) == "nil") or (tostring(sourceEvent) == "")) then sourceEvent = "UNKNOWN" end
return {
["Link"] = mUnitID,
["Name"] = UnitName(mUnitID),
["Level"] = UnitLevel(mUnitID),
["Stats"] = UnitStats(mUnitID),
["HP"] = UnitHealth(mUnitID),
["HPMax"] = UnitHealthMax(mUnitID),
["Rank"] = UnitClassification(mUnitID),
["Event"] = sourceEvent,
["Time"] = time()
}
end
return nil
end

BUGS:

For some reason, Grind_UnitExistsInBuffer() has trouble comparing QueriedUnit with the unit buffer, ultimately allowing duplicate entries in the buffer. This could cause several problems in future development.|||The title of this thread has become slightly off course from the current topic.

I have created this thread to continue discussion about the unique identification of a unit.|||If you have an array called A1, and declare :

local A2 = A1;

Then you have not created a second array with the same entries and values as A1.

You made both variables A1 and A2 point to the same array, and testing if ( A1 == A2 ) will be true.

In order to make a second, different array called A2 that is identical to A1, you declare A2 as an array, and then iterate through A1 and create equivalent entries in A2.

After which, testing if ( A1 == A2 ) will be false; Which is essentially the problem you have encountered.

I can't remember if there is a shortcut isEqual() style function for testing array equivalence of two separate arrays...but you could always check the equivalence of the Key entries manually.

No comments:

Post a Comment