Quantcast

Move state kept in OvaleState into the relevant modules.

Johnny C. Lam [11-10-13 - 00:31]
Move state kept in OvaleState into the relevant modules.

Each module now manages part of the state for the simulator.  A module
needs to define the following (possibly empty) methods:

    ResetState(state)
    ApplySpellStart(state, ...)
    ApplySpellToPlayer(state, ...)
    ApplySpellToTarget(state, ...)

The module also needs to define a state prototype with additional methods
that should be mixed into the state object maintained by OvaleState.

Modules register/unregister their state for the simulator using:

    OvaleState:RegisterState(...)
    OvaleState:UnregisterState(...)

This de-couples OvaleState from every module that tracks some piece of
information in the game.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1149 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
Ovale.toc
OvaleAura.lua
OvaleBestAction.lua
OvaleComboPoints.lua
OvaleFuture.lua
OvaleOptions.lua
OvalePower.lua
OvaleState.lua
compiler.pl
diff --git a/Ovale.toc b/Ovale.toc
index 1d5196d..3e075e7 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -29,7 +29,6 @@ OvaleLatency.lua
 OvalePool.lua
 OvalePoolGC.lua
 OvalePoolRefCount.lua
-OvalePower.lua
 OvaleQueue.lua
 OvaleSpellBook.lua
 OvaleStance.lua
@@ -39,8 +38,11 @@ OvaleDamageTaken.lua
 OvalePaperDoll.lua
 OvaleScore.lua
 #
+OvaleState.lua
+#
 OvaleAura.lua
 OvaleComboPoints.lua
+OvalePower.lua
 OvaleRecount.lua
 OvaleScripts.lua
 scripts\scripts.xml
@@ -51,13 +53,11 @@ OvaleSwing.lua
 OvaleOptions.lua
 OvaleFuture.lua
 #
-OvaleState.lua
 conditions\conditions.xml
-#
+OvaleCompile.lua
 OvaleIcone.lua
 OvaleIcone.xml
 #
 OvaleBestAction.lua
-OvaleCompile.lua
 #
 OvaleFrame.lua
diff --git a/OvaleAura.lua b/OvaleAura.lua
index d62f90f..3877199 100644
--- a/OvaleAura.lua
+++ b/OvaleAura.lua
@@ -20,6 +20,7 @@ local OvaleData = Ovale.OvaleData
 local OvaleGUID = Ovale.OvaleGUID
 local OvalePaperDoll = Ovale.OvalePaperDoll
 local OvalePool = Ovale.OvalePool
+local OvaleState = Ovale.OvaleState

 local ipairs = ipairs
 local pairs = pairs
@@ -327,9 +328,11 @@ function OvaleAura:OnEnable()
 	self:RegisterEvent("UNIT_AURA")
 	self:RegisterMessage("Ovale_GroupChanged", RemoveAurasForMissingUnits)
 	self:RegisterMessage("Ovale_InactiveUnit")
+	OvaleState:RegisterState(self, self.statePrototype)
 end

 function OvaleAura:OnDisable()
+	OvaleState:UnregisterState(self)
 	self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
 	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
 	self:UnregisterEvent("UNIT_AURA")
@@ -615,3 +618,338 @@ function OvaleAura:DebugListAura(unitId, filter)
 	end
 end
 --</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleAura.statePrototype = {
+	aura = nil,
+	serial = nil,
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleAura:InitializeState(state)
+	state.aura = {}
+	state.serial = 0
+end
+
+-- Reset the state to the current conditions.
+function OvaleAura:ResetState(state)
+	state.serial = state.serial + 1
+end
+
+-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
+function OvaleAura:ApplySpellOnPlayer(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	-- If the spellcast has already ended, then the effects on the player have already occurred.
+	if endCast <= OvaleState.now then
+		return
+	end
+
+	-- Apply the auras on the player.
+	local si = OvaleData.spellInfo[spellId]
+	if si and si.aura and si.aura.player then
+		state:ApplySpellAuras(spellId, startCast, endCast, OvaleGUID:GetGUID("player"), si.aura.player, spellcast)
+	end
+end
+
+-- Apply the effects of the spell on the target's state when it lands on the target.
+function OvaleAura:ApplySpellOnTarget(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	local si = OvaleData.spellInfo[spellId]
+	if si and si.aura and si.aura.target then
+		-- Apply the auras on the target.
+		state:ApplySpellAuras(spellId, startCast, endCast, targetGUID, si.aura.target, spellcast)
+	end
+end
+--</public-static-methods>
+
+-- Mix-in methods for simulator state.
+do
+	local statePrototype = OvaleAura.statePrototype
+
+	-- Apply the auras caused by the given spell in the simulator.
+	function statePrototype:ApplySpellAuras(spellId, startCast, endCast, guid, auraList, spellcast)
+		local state = self
+		for filter, filterInfo in pairs(auraList) do
+			for auraId, spellData in pairs(filterInfo) do
+				local si = OvaleData.spellInfo[auraId]
+				-- An aura is treated as a periodic aura if it sets "tick" explicitly in SpellInfo.
+				local isDoT = (si and si.tick)
+				local duration = spellData
+				local stacks = spellData
+
+				-- If aura is specified with a duration, then assume stacks == 1.
+				if type(duration) == "number" and duration > 0 then
+					stacks = 1
+				end
+				-- Set the duration to the proper length if it's a DoT.
+				if si and si.duration then
+					duration = state:GetDuration(auraId)
+				end
+
+				local start, ending, currentStacks, tick = state:GetAuraByGUID(guid, auraId, filter, true, target)
+				local newAura = state:NewAura(guid, auraId, filter)
+				newAura.mine = true
+
+				--[[
+					auraId=N, N > 0		N is duration, auraID is applied, add one stack
+					auraId=0			aura is removed
+					auraId=N, N < 0		N is number of stacks of aura removed
+					auraId=refresh		auraId is refreshed, no change to stacks
+				--]]
+				if type(stacks) == "number" and stacks == 0 then
+					Ovale:Logf("Aura %d is completely removed", auraId)
+					newAura.stacks = 0
+					newAura.start = start
+					newAura.ending = endCast
+				elseif ending and endCast <= ending then
+					-- Spellcast ends before the aura expires.
+					if stacks == "refresh" or stacks > 0 then
+						if stacks == "refresh" then
+							Ovale:Logf("Aura %d is refreshed", auraId)
+							newAura.stacks = currentStacks
+						else -- if stacks > 0 then
+							newAura.stacks = currentStacks + stacks
+							Ovale:Logf("Aura %d gains a stack to %d because of spell %d (ending was %s)", auraId, newAura.stacks, spellId, ending)
+						end
+						newAura.start = start
+						if isDoT and ending > newAura.start and tick and tick > 0 then
+							-- Add new duration after the next tick is complete.
+							local remainingTicks = floor((ending - endCast) / tick)
+							newAura.ending = (ending - tick * remainingTicks) + duration
+							newAura.tick = OvaleAura:GetTickLength(auraId)
+							-- Re-snapshot stats for the DoT.
+							-- XXX This is not quite right because it uses the current player stats instead of the simulator's state.
+							OvalePaperDoll:SnapshotStats(newAura, spellcast)
+							newAura.damageMultiplier = state:GetDamageMultiplier(auraId)
+						else
+							newAura.ending = endCast + duration
+						end
+						Ovale:Logf("Aura %d ending is now %f", auraId, newAura.ending)
+					elseif stacks < 0 then
+						newAura.stacks = currentStacks + stacks
+						newAura.start = start
+						newAura.ending = ending
+						Ovale:Logf("Aura %d loses %d stack(s) to %d because of spell %d", auraId, -1 * stacks, newAura.stacks, spellId)
+						if newAura.stacks <= 0 then
+							Ovale:Logf("Aura %d is completely removed", auraId)
+							newAura.stacks = 0
+							newAura.ending = endCast
+						end
+					end
+				elseif type(stacks) == "number" and type(duration) == "number" and stacks > 0 and duration > 0 then
+					Ovale:Logf("New aura %d at %f on %s", auraId, endCast, guid)
+					newAura.stacks = stacks
+					newAura.start = endCast
+					newAura.ending = endCast + duration
+					if isDoT then
+						newAura.tick = OvaleAura:GetTickLength(auraId)
+						-- Snapshot stats for the DoT.
+						-- XXX This is not quite right because it uses the current player stats instead of the simulator's state.
+						OvalePaperDoll:SnapshotStats(newAura, spellcast)
+						newAura.damageMultiplier = state:GetDamageMultiplier(auraId)
+					end
+				end
+			end
+		end
+	end
+
+	function statePrototype:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
+		local state = self
+		local aura
+		if mine then
+			local auraTable = state.aura[guid]
+			if auraTable then
+				if filter then
+					local auraList = auraTable[filter]
+					if auraList then
+						if auraList[spellId] and auraList[spellId].serial == state.serial then
+							aura = auraList[spellId]
+						end
+					end
+				else
+					for auraFilter, auraList in pairs(auraTable) do
+						if auraList[spellId] and auraList[spellId].serial == state.serial then
+							aura = auraList[spellId]
+							filter = auraFilter
+							break
+						end
+					end
+				end
+			end
+		end
+		if aura then
+			if aura.stacks > 0 then
+				Ovale:Logf("Found %s aura %s on %s", filter, spellId, guid)
+			else
+				Ovale:Logf("Found %s aura %s on %s (removed)", filter, spellId, guid)
+			end
+			if auraFound then
+				for k, v in pairs(aura) do
+					auraFound[k] = v
+				end
+			end
+			return aura.start, aura.ending, aura.stacks, aura.gain
+		else
+			Ovale:Logf("Aura %s not found in state for %s", spellId, guid)
+			return OvaleAura:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
+		end
+	end
+
+	do
+		local aura = {}
+		local newAura = {}
+
+		function statePrototype:GetAura(unitId, spellId, filter, mine, auraFound)
+			local state = self
+			local guid = OvaleGUID:GetGUID(unitId)
+			if OvaleData.buffSpellList[spellId] then
+				if auraFound then wipe(newAura) end
+				local newStart, newEnding, newStacks, newGain
+				for auraId in pairs(OvaleData.buffSpellList[spellId]) do
+					if auraFound then wipe(aura) end
+					local start, ending, stacks, gain = state:GetAuraByGUID(guid, auraId, filter, mine, unitId, aura)
+					if start and (not newStart or stacks > newStacks) then
+						newStart = start
+						newEnding = ending
+						newStacks = stacks
+						newGain = gain
+						if auraFound then
+							wipe(newAura)
+							for k, v in pairs(aura) do
+								newAura[k] = v
+							end
+						end
+					end
+				end
+				if auraFound then
+					for k, v in pairs(newAura) do
+						auraFound[k] = v
+					end
+				end
+				return newStart, newEnding, newStacks, newGain
+			else
+				return state:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
+			end
+		end
+	end
+
+	-- Look for an aura on any target, excluding the given GUID.
+	-- Returns the earliest start time, the latest ending time, and the number of auras seen.
+	function statePrototype:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
+		local state = self
+		local start, ending, count = OvaleAura:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
+		-- TODO: This is broken because it doesn't properly account for removed auras in the current frame.
+		for guid, auraTable in pairs(state.aura) do
+			if guid ~= excludingGUID then
+				for auraFilter, auraList in pairs(auraTable) do
+					if not filter or auraFilter == filter then
+						local aura = auraList[spellId]
+						if aura and aura.serial == state.serial then
+							if aura.start and (not start or aura.start < start) then
+								start = aura.start
+							end
+							if aura.ending and (not ending or aura.ending > ending) then
+								ending = aura.ending
+							end
+							count = count + 1
+						end
+					end
+				end
+			end
+		end
+		return start, ending, count
+	end
+
+	function statePrototype:NewAura(guid, spellId, filter)
+		local state = self
+		if not state.aura[guid] then
+			state.aura[guid] = {}
+		end
+		if not state.aura[guid][filter] then
+			state.aura[guid][filter] = {}
+		end
+		if not state.aura[guid][filter][spellId] then
+			state.aura[guid][filter][spellId] = {}
+		end
+		local aura = state.aura[guid][filter][spellId]
+		aura.serial = state.serial
+		aura.mine = true
+		aura.gain = OvaleState.currentTime
+		return aura
+	end
+
+	function statePrototype:GetDamageMultiplier(spellId)
+		local state = self
+		local damageMultiplier = 1
+		if spellId then
+			local si = OvaleData.spellInfo[spellId]
+			if si and si.damageAura then
+				local playerGUID = OvaleGUID:GetGUID("player")
+				for filter, auraList in pairs(si.damageAura) do
+					for auraSpellId, multiplier in pairs(auraList) do
+						local count = select(3, state:GetAuraByGUID(playerGUID, auraSpellId, filter, nil, "player"))
+						if count and count > 0 then
+							local auraSpellInfo = OvaleData.spellInfo[auraSpellId]
+							if auraSpellInfo.stacking and auraSpellInfo.stacking > 0 then
+								multiplier = 1 + (multiplier - 1) * count
+							end
+							damageMultiplier = damageMultiplier * multiplier
+						end
+					end
+				end
+			end
+		end
+		return damageMultiplier
+	end
+
+	-- Returns the duration, tick length, and number of ticks of an aura.
+	function statePrototype:GetDuration(auraSpellId)
+		local state = self
+		local si
+		if type(auraSpellId) == "number" then
+			si = OvaleData.spellInfo[auraSpellId]
+		elseif OvaleData.buffSpellList[auraSpellId] then
+			for spellId in pairs(OvaleData.buffSpellList[auraSpellId]) do
+				si = OvaleData.spellInfo[spellId]
+				if si then
+					auraSpellId = spellId
+					break
+				end
+			end
+		end
+		if si and si.duration then
+			local OvaleComboPoints = Ovale.OvaleComboPoints
+			local OvalePower = Ovale.OvalePower
+			local duration = si.duration
+			local combo = state.combo or 0
+			local holy = state.holy or 1
+			if si.adddurationcp then
+				duration = duration + si.adddurationcp * combo
+			end
+			if si.adddurationholy then
+				duration = duration + si.adddurationholy * (holy - 1)
+			end
+			if si.tick then	-- DoT
+				--DoT duration is tick * numTicks.
+				local tick = OvaleAura:GetTickLength(auraSpellId)
+				local numTicks = floor(duration / tick + 0.5)
+				duration = tick * numTicks
+				return duration, tick, numTicks
+			end
+			return duration
+		end
+	end
+
+	-- Track a new Eclipse buff that starts at timestamp.
+	function statePrototype:AddEclipse(timestamp, spellId)
+		local state = self
+		local aura = state:NewAura(self_player_guid, spellId, "HELPFUL")
+		aura.start = timestamp
+		aura.ending = nil
+		aura.stacks = 1
+	end
+end
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index f5f2cde..b12ba04 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -17,6 +17,7 @@ local OvaleActionBar = Ovale.OvaleActionBar
 local OvaleCondition = Ovale.OvaleCondition
 local OvaleData = Ovale.OvaleData
 local OvaleEquipement = Ovale.OvaleEquipement
+local OvaleFuture = Ovale.OvaleFuture
 local OvalePaperDoll = Ovale.OvalePaperDoll
 local OvalePool = Ovale.OvalePool
 local OvalePower = Ovale.OvalePower
@@ -699,7 +700,7 @@ local OVALE_COMPUTE_VISITOR =
 --<public-static-methods>
 function OvaleBestAction:StartNewAction()
 	OvaleState:Reset()
-	OvaleState:ApplyActiveSpells()
+	OvaleFuture:ApplyInFlightSpells()
 	self_serial = self_serial + 1
 end

diff --git a/OvaleComboPoints.lua b/OvaleComboPoints.lua
index cabc92f..78e5b77 100644
--- a/OvaleComboPoints.lua
+++ b/OvaleComboPoints.lua
@@ -17,6 +17,7 @@ Ovale.OvaleComboPoints = OvaleComboPoints
 local OvaleData = Ovale.OvaleData
 local OvaleGUID = Ovale.OvaleGUID
 local OvalePaperDoll = Ovale.OvalePaperDoll
+local OvaleState = Ovale.OvaleState

 local API_GetComboPoints = GetComboPoints
 local MAX_COMBO_POINTS = MAX_COMBO_POINTS
@@ -35,11 +36,13 @@ function OvaleComboPoints:OnEnable()
 		self:RegisterEvent("PLAYER_TARGET_CHANGED", "Refresh")
 		self:RegisterEvent("UNIT_COMBO_POINTS")
 		self:RegisterEvent("UNIT_TARGET", "UNIT_COMBO_POINTS")
+		OvaleState:RegisterState(self, self.statePrototype)
 	end
 end

 function OvaleComboPoints:OnDisable()
 	if OvalePaperDoll.class == "ROGUE" or OvalePaperDoll.class == "DRUID" then
+		OvaleState:UnregisterState(self)
 		self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
 		self:UnregisterEvent("PLAYER_ENTERING_WORLD")
 		self:UnregisterEvent("PLAYER_LOGIN")
@@ -92,3 +95,67 @@ function OvaleComboPoints:Debug()
 	Ovale:FormatPrint("Player has %d combo points on target %s.", self.combo, OvaleGUID:GetGUID("target"))
 end
 --</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleComboPoints.statePrototype = {
+	combo = nil,
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleComboPoints:InitializeState(state)
+	state.combo = 0
+end
+
+-- Reset the state to the current conditions.
+function OvaleComboPoints:ResetState(state)
+	state.combo = self.combo or 0
+end
+
+-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
+function OvaleComboPoints:ApplySpellOnPlayer(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	-- If the spellcast has already ended, then the effects on the player have already occurred.
+	if endCast <= OvaleState.now then
+		return
+	end
+
+	local si = OvaleData.spellInfo[spellId]
+	if si and si.combo then
+		local cost = si.combo
+		local power = state.combo or 0
+		--[[
+			cost > 0 means that the spell generates combo points.
+			cost < 0 means that the spell costs combo points.
+			cost == 0 means that the spell uses all of the combo points (finisher).
+		--]]
+		if cost == 0 then
+			power = 0
+		else
+			power = power + cost
+		end
+		--[[
+			Add extra combo points generated by presence of a buff.
+			"buff_combo" is the spell ID of the buff that causes extra points to be generated or used.
+			"buff_combo_amount" is the number of extra points generated or used, defaulting to 1
+				(one extra point generated).
+		--]]
+		if si.buff_combo and state:GetAura("player", si.buff_combo, nil, true) then
+			local buffAmount = si.buff_combo_amount or 1
+			power = power + buffAmount
+		end
+		-- Clamp combo points to lower and upper limits.
+		if power < 0 then
+			power = 0
+		end
+		if power > MAX_COMBO_POINTS then
+			power = MAX_COMBO_POINTS
+		end
+		state.combo = power
+	end
+end
+--</public-static-methods>
diff --git a/OvaleFuture.lua b/OvaleFuture.lua
index d5c9e2c..490c059 100644
--- a/OvaleFuture.lua
+++ b/OvaleFuture.lua
@@ -23,6 +23,7 @@ local OvalePaperDoll = Ovale.OvalePaperDoll
 local OvalePool = Ovale.OvalePool
 local OvaleScore = Ovale.OvaleScore
 local OvaleSpellBook = Ovale.OvaleSpellBook
+local OvaleState = Ovale.OvaleState

 local ipairs = ipairs
 local pairs = pairs
@@ -249,9 +250,11 @@ function OvaleFuture:OnEnable()
 	self:RegisterEvent("UNIT_SPELLCAST_START")
 	self:RegisterMessage("Ovale_AuraAdded")
 	self:RegisterMessage("Ovale_InactiveUnit")
+	OvaleState:RegisterState(self, self.statePrototype)
 end

 function OvaleFuture:OnDisable()
+	OvaleState:UnregisterState(self)
 	self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
 	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
 	self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
@@ -446,8 +449,10 @@ function OvaleFuture:COMBAT_LOG_EVENT_UNFILTERED(event, ...)
 	end
 end

--- Apply spells that are being cast or are in flight.
-function OvaleFuture:ApplyInFlightSpells(now, ApplySpell)
+-- Apply the effects of spells that are being cast or are in flight, allowing us to
+-- ignore lag or missile travel time.
+function OvaleFuture:ApplyInFlightSpells()
+	local now = OvaleState.now
 	local index = 0
 	local spellcast, si
 	while true do
@@ -460,7 +465,7 @@ function OvaleFuture:ApplyInFlightSpells(now, ApplySpell)
 		if not (si and si.toggle) then
 			Ovale:Logf("now = %f, spellId = %d, endCast = %f", now, spellcast.spellId, spellcast.stop)
 			if now - spellcast.stop < 5 then
-				ApplySpell(spellcast.spellId, spellcast.start, spellcast.stop, spellcast.stop, spellcast.nocd, spellcast.target, spellcast)
+				OvaleState:ApplySpell(spellcast.spellId, spellcast.start, spellcast.stop, spellcast.stop, spellcast.nocd, spellcast.target, spellcast)
 			else
 				tremove(self_activeSpellcast, index)
 				self_pool:Release(spellcast)
@@ -497,3 +502,59 @@ function OvaleFuture:Debug()
 	end
 end
 --</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleFuture.statePrototype = {
+	counter = nil,
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleFuture:InitializeState(state)
+	state.counter = {}
+end
+
+-- Reset the state to the current conditions.
+function OvaleFuture:ResetState(state)
+	for k, v in pairs(self.counter) do
+		state.counter[k] = self.counter[k]
+	end
+end
+
+-- Apply the effects of the spell at the start of the spellcast.
+function OvaleFuture:ApplySpellStart(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	-- If the spellcast has already started, then the effects have already occurred.
+	if startCast < OvaleState.now then
+		return
+	end
+
+	local si = OvaleData.spellInfo[spellId]
+	if si then
+		-- Increment and reset spell counters.
+		if si.inccounter then
+			local id = si.inccounter
+			local value = state.counter[id] and state.counter[id] or 0
+			state.counter[id] = value + 1
+		end
+		if si.resetcounter then
+			local id = si.resetcounter
+			state.counter[id] = 0
+		end
+	end
+end
+--</public-static-methods>
+
+-- Mix-in methods for simulator state.
+do
+	local statePrototype = OvaleFuture.statePrototype
+
+	function statePrototype:GetCounterValue(id)
+		local state = self
+		return state.counter[id] and state.counter[id] or 0
+	end
+end
\ No newline at end of file
diff --git a/OvaleOptions.lua b/OvaleOptions.lua
index 00fe530..35abca0 100644
--- a/OvaleOptions.lua
+++ b/OvaleOptions.lua
@@ -19,6 +19,7 @@ local L = Ovale.L
 local OvalePaperDoll = Ovale.OvalePaperDoll
 local OvaleScripts = Ovale.OvaleScripts
 local OvaleSpellBook = Ovale.OvaleSpellBook
+local OvaleState = Ovale.OvaleState

 local strgmatch = string.gmatch
 local strgsub = string.gsub
@@ -600,7 +601,7 @@ local self_options =
 					name = "Power",
 					type = "execute",
 					func = function()
-						if Ovale.OvaleState then Ovale.OvaleState:DebugPower() end
+						OvaleState.state:DebugPower()
 					end
 				},
 				talent =
diff --git a/OvalePower.lua b/OvalePower.lua
index 5f4fba9..1b993cc 100644
--- a/OvalePower.lua
+++ b/OvalePower.lua
@@ -13,8 +13,13 @@ local OvalePower = Ovale:NewModule("OvalePower", "AceEvent-3.0")
 Ovale.OvalePower = OvalePower

 --<private-static-properties>
+local OvaleData = Ovale.OvaleData
+local OvaleState = Ovale.OvaleState
+
 local pairs = pairs
+local select = select
 local API_GetPowerRegen = GetPowerRegen
+local API_GetSpellInfo = GetSpellInfo
 local API_UnitPower = UnitPower
 local API_UnitPowerMax = UnitPowerMax
 local API_UnitPowerType = UnitPowerType
@@ -94,9 +99,11 @@ function OvalePower:OnEnable()
 	self:RegisterEvent("UNIT_RANGEDDAMAGE")
 	self:RegisterEvent("UNIT_SPELL_HASTE", "UNIT_RANGEDDAMAGE")
 	self:RegisterMessage("Ovale_StanceChanged", "EventHandler")
+	OvaleState:RegisterState(self, self.statePrototype)
 end

 function OvalePower:OnDisable()
+	OvaleState:UnregisterState(self)
 	self:UnregisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
 	self:UnregisterEvent("PLAYER_ALIVE")
 	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
@@ -188,4 +195,117 @@ function OvalePower:Debug()
 	Ovale:FormatPrint("Active regen: %f", self.activeRegen)
 	Ovale:FormatPrint("Inactive regen: %f", self.inactiveRegen)
 end
---</public-static-methods>
\ No newline at end of file
+--</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvalePower.statePrototype = {
+	powerRate = nil,
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvalePower:InitializeState(state)
+	state.powerRate = {}
+end
+
+-- Reset the state to the current conditions.
+function OvalePower:ResetState(state)
+	-- Power levels for each resource.
+	for powerType in pairs(self.POWER_INFO) do
+		state[powerType] = self.power[powerType] or 0
+	end
+	-- Clear power regeneration rates for each resource.
+	for powerType in pairs(self.POWER_INFO) do
+		state.powerRate[powerType] = 0
+	end
+	-- Set power regeneration for current resource.
+	if Ovale.enCombat then
+		state.powerRate[self.powerType] = self.activeRegen
+	else
+		state.powerRate[self.powerType] = self.inactiveRegen
+	end
+end
+
+-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
+function OvalePower:ApplySpellOnPlayer(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	-- If the spellcast has already ended, then the effects on the player have already occurred.
+	if endCast <= OvaleState.now then
+		return
+	end
+
+	local si = OvaleData.spellInfo[spellId]
+
+	-- Update power using information from GetSpellInfo() if there is no SpellInfo() for the spell's cost.
+	do
+		local cost, _, powerType = select(4, API_GetSpellInfo(spellId))
+		if cost and powerType then
+			powerType = self.POWER_TYPE[powerType]
+			if not si or not si[powerType] then
+				state[powerType] = state[powerType] - cost
+			end
+		end
+	end
+
+	if si then
+		-- Update power state except for eclipse energy.
+		for powerType, powerInfo in pairs(self.POWER_INFO) do
+			if powerType ~= "eclipse" then
+				local cost = si[powerType]
+				local power = state[powerType] or 0
+				if cost then
+					--[[
+						cost > 0 means that the spell costs resources.
+						cost < 0 means that the spell generates resources.
+						cost == 0 means that the spell uses all of the resources (zeroes it out).
+					--]]
+					if cost == 0 then
+						power = 0
+					else
+						power = power - cost
+					end
+					--[[
+						Add extra resource generated by presence of a buff.
+						"buff_<powerType>" is the spell ID of the buff that causes extra resources to be generated or used.
+						"buff_<powerType>_amount" is the amount of extra resources generated or used, defaulting to -1
+							(one extra resource generated).
+					--]]
+					local buffParam = "buff_" .. tostring(powerType)
+					local buffAmoumtParam = buffParam .. "_amount"
+					if si[buffParam] and state:GetAura("player", si[buffParam], nil, true) then
+						local buffAmount = si[buffAmountParam] or -1
+						power = power - buffAmount
+					end
+					-- Clamp power to lower and upper limits.
+					local mini = powerInfo.mini or 0
+					local maxi = powerInfo.maxi or self.maxPower[powerType]
+					if mini and power < mini then
+						power = mini
+					end
+					if maxi and power > maxi then
+						power = maxi
+					end
+					state[powerType] = power
+				end
+			end
+		end
+	end
+end
+--</public-static-methods>
+
+-- Mix-in methods for simulator state.
+do
+	local statePrototype = OvalePower.statePrototype
+
+	-- Print out the levels of each power type in the current state.
+	function statePrototype:DebugPower()
+		local state = self
+		for powerType in pairs(OvalePower.POWER_INFO) do
+			Ovale:FormatPrint("%s = %d", powerType, state[powerType])
+		end
+	end
+end
diff --git a/OvaleState.lua b/OvaleState.lua
index a6857d7..a52b56e 100644
--- a/OvaleState.lua
+++ b/OvaleState.lua
@@ -15,34 +15,37 @@ local OvaleState = Ovale:NewModule("OvaleState")
 Ovale.OvaleState = OvaleState

 --<private-static-properties>
-local OvaleAura = Ovale.OvaleAura
-local OvaleComboPoints = Ovale.OvaleComboPoints
 local OvaleData = Ovale.OvaleData
-local OvaleFuture = Ovale.OvaleFuture
 local OvaleGUID = Ovale.OvaleGUID
 local OvalePaperDoll = Ovale.OvalePaperDoll
-local OvalePower = Ovale.OvalePower
+local OvaleQueue = Ovale.OvaleQueue
 local OvaleSpellBook = Ovale.OvaleSpellBook
 local OvaleStance = Ovale.OvaleStance

 local floor = math.floor
 local pairs = pairs
 local select = select
+local tinsert = table.insert
+local tremove = table.remove
 local tostring = tostring
 local type = type
 local wipe = table.wipe
 local API_GetEclipseDirection = GetEclipseDirection
 local API_GetRuneCooldown = GetRuneCooldown
 local API_GetRuneType = GetRuneType
-local API_GetSpellInfo = GetSpellInfo
 local API_GetTime = GetTime
 local API_UnitHealth = UnitHealth
 local API_UnitHealthMax = UnitHealthMax
-local MAX_COMBO_POINTS = MAX_COMBO_POINTS
+
+local self_statePrototype = {}
+local self_stateModules = OvaleQueue:NewQueue("OvaleState_stateModules")

 local self_runes = {}
 local self_runesCD = {}

+-- Whether the state of the simulator has been initialized.
+local self_stateIsInitialized = false
+
 -- Aura IDs for Eclipse buffs.
 local LUNAR_ECLIPSE = 48518
 local SOLAR_ECLIPSE = 48517
@@ -52,73 +55,101 @@ local STARFALL = 48505

 --<public-static-properties>
 --the state in the current frame
-OvaleState.state = {rune={}, cd = {}, counter={}}
-OvaleState.aura = {}
-OvaleState.serial = 0
-for i=1,6 do
-	OvaleState.state.rune[i] = {}
-end
+OvaleState.state = {}
 --The spell being cast
 OvaleState.currentSpellId = nil
+OvaleState.now = nil
 OvaleState.maintenant = nil
 OvaleState.currentTime = nil
 OvaleState.attenteFinCast = nil
 OvaleState.startCast = nil
 OvaleState.endCast = nil
 OvaleState.gcd = 1.5
-OvaleState.powerRate = {}
 OvaleState.lastSpellId = nil
 --</public-static-properties>

 --<private-static-methods>
-local function ApplySpell(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	local self = OvaleState
-	self:ApplySpell(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-end
-
--- Track a new Eclipse buff that starts at timestamp.
-local function AddEclipse(timestamp, spellId)
+-- XXX The way this function updates the rune state looks completely wrong.
+local function AddRune(atTime, runeType, value)
 	local self = OvaleState
-	local newAura = self:NewAura(OvaleGUID:GetGUID("player"), spellId, "HELPFUL")
-	newAura.start = timestamp
-	newAura.ending = nil
-	newAura.stacks = 1
+	for i = 1, 6 do
+		local rune = self.state.rune[i]
+		if (rune.type == runeType or rune.type == 4) and rune.cd <= atTime then
+			rune.cd = atTIme + 10
+		end
+	end
 end
 --</private-static-methods>

 --<public-static-methods>
+function OvaleState:RegisterState(addon, statePrototype)
+	self_stateModules:Insert(addon)
+	self_statePrototype[addon] = statePrototype
+
+	-- Mix-in addon's state prototype into OvaleState.state.
+	for k, v in pairs(statePrototype) do
+		self.state[k] = v
+	end
+end
+
+function OvaleState:UnregisterState(addon)
+	stateModules = OvaleQueue:NewQueue("OvaleState_stateModules")
+	while self_stateModules:Size() > 0 do
+		local stateAddon = self_stateModules:Remove()
+		if stateAddon ~= addon then
+			stateModules:Insert(addon)
+		end
+	end
+	self_stateModules = stateModules
+
+	-- Remove mix-in methods from addon's state prototype.
+	local statePrototype = self_statePrototype[addon]
+	for k in pairs(statePrototype) do
+		self.state[k] = nil
+	end
+	self_stateModules[addon] = nil
+end
+
+function OvaleState:InvokeMethod(methodName, ...)
+	for _, addon in self_stateModules:Iterator() do
+		if addon[methodName] then
+			addon[methodName](addon, self.state, ...)
+		end
+	end
+end
+
 function OvaleState:StartNewFrame()
-	self.maintenant = API_GetTime()
+	if not self_stateIsInitialized then
+		self:InitializeState()
+	end
+	self.now = API_GetTime()
+	self.maintenant = self.now
 	self.gcd = self:GetGCD()
 end

-function OvaleState:UpdatePowerRates()
-	for powerType in pairs(OvalePower.POWER_INFO) do
-		self.powerRate[powerType] = 0
-	end
-	-- Power regeneration for current power type.
-	if Ovale.enCombat then
-		self.powerRate[OvalePower.powerType] = OvalePower.activeRegen
-	else
-		self.powerRate[OvalePower.powerType] = OvalePower.inactiveRegen
+function OvaleState:InitializeState()
+	self:InvokeMethod("InitializeState")
+
+	self.state.rune = {}
+	for i = 1, 6 do
+		self.state.rune[i] = {}
 	end
+
+	-- Legacy fields
+	self.powerRate = self.state.powerRate
+
+	self_stateIsInitialized = true
 end

 function OvaleState:Reset()
 	self.lastSpellId = Ovale.lastSpellcast and Ovale.lastSpellcast.spellId
-	self.serial = self.serial + 1
 	self.currentTime = self.maintenant
 	Ovale:Logf("Reset state with current time = %f", self.currentTime)
 	self.currentSpellId = nil
 	self.attenteFinCast = self.maintenant

-	-- Snapshot the current power and regeneration rates.
-	self.state.combo = OvaleComboPoints.combo
-	for powerType in pairs(OvalePower.POWER_INFO) do
-		self.state[powerType] = OvalePower.power[powerType]
-	end
-	self:UpdatePowerRates()
-
+	self:InvokeMethod("ResetState")
+
 	if OvalePaperDoll.class == "DEATHKNIGHT" then
 		for i=1,6 do
 			self.state.rune[i].type = API_GetRuneType(i)
@@ -140,16 +171,6 @@ function OvaleState:Reset()
 		v.enable = 0
 		v.toggled = nil
 	end
-
-	for k,v in pairs(self.state.counter) do
-		self.state.counter[k] = OvaleFuture.counter[k]
-	end
-end
-
--- Apply the effects of spells that are being cast or are in flight, allowing us to
--- ignore lag or missile travel time.
-function OvaleState:ApplyActiveSpells()
-	OvaleFuture:ApplyInFlightSpells(self.maintenant, ApplySpell)
 end

 --[[
@@ -200,30 +221,12 @@ end

 -- Apply the effects of the spell at the start of the spellcast.
 function OvaleState:ApplySpellStart(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	local si = OvaleData.spellInfo[spellId]
-	--[[
-		If the spellcast has already started, then the effects have already occurred,
-		so only consider spells that are cast in the future in the simulator.
-	--]]
-	if startCast >= self.maintenant then
-		if si then
-			-- Increment and reset spell counters.
-			if si.inccounter then
-				local id = si.inccounter
-				local value = self.state.counter[id] and self.state.counter[id] or 0
-				self.state.counter[id] = value + 1
-			end
-			if si.resetcounter then
-				local id = si.resetcounter
-				self.state.counter[id] = 0
-			end
-		end
-	end
+	self:InvokeMethod("ApplySpellStart", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
 function OvaleState:ApplySpellOnPlayer(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	local si = OvaleData.spellInfo[spellId]
+	self:InvokeMethod("ApplySpellOnPlayer", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 	--[[
 		If the spellcast has already ended, then the effects have already occurred,
 		so only consider spells that have not yet finished casting in the simulator.
@@ -234,21 +237,12 @@ function OvaleState:ApplySpellOnPlayer(spellId, startCast, endCast, nextCast, no

 		-- Adjust the player's resources.
 		self:ApplySpellCost(spellId, startCast, endCast)
-
-		-- Apply the auras on the player.
-		if si and si.aura and si.aura.player then
-			self:ApplySpellAuras(spellId, startCast, endCast, OvaleGUID:GetGUID("player"), si.aura.player, spellcast)
-		end
 	end
 end

 -- Apply the effects of the spell on the target's state when it lands on the target.
 function OvaleState:ApplySpellOnTarget(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	local si = OvaleData.spellInfo[spellId]
-	if si and si.aura and si.aura.target then
-		-- Apply the auras on the target.
-		self:ApplySpellAuras(spellId, startCast, endCast, targetGUID, si.aura.target, spellcast)
-	end
+	self:InvokeMethod("ApplySpellOnTarget", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 end

 -- Adjust a spell cooldown in the simulator.
@@ -308,95 +302,8 @@ end
 -- Adjust the player's resources in the simulator from casting the given spell.
 function OvaleState:ApplySpellCost(spellId, startCast, endCast)
 	local si = OvaleData.spellInfo[spellId]
-	local _, _, _, cost, _, powerType = API_GetSpellInfo(spellId)
-
-	-- Update power using information from GetSpellInfo() if there is no user-defined SpellInfo() for the spell's cost.
-	if cost and powerType then
-		powerType = OvalePower.POWER_TYPE[powerType]
-		if not si or not si[powerType] then
-			self.state[powerType] = self.state[powerType] - cost
-		end
-	end

 	if si then
-		-- Update power state, except for combo points, eclipse energy, and runes.
-		for powerType, powerInfo in pairs(OvalePower.POWER_INFO) do
-			if powerType ~= "eclipse" then
-				local cost = si[powerType]
-				if cost then
-					--[[
-						cost > 0 means that the spell costs resources.
-						cost < 0 means that the spell generates resources.
-						cost == 0 means that the spell uses all of the resources (zeroes it out).
-					--]]
-					if cost == 0 then
-						self.state[powerType] = 0
-					else
-						self.state[powerType] = self.state[powerType] - cost
-					end
-					--[[
-						Add extra resource generated by presence of a buff.
-						"buff_<powerType>" is the spell ID of the buff that causes extra resources to be generated or used.
-						"buff_<powerType>_amount" is the amount of extra resources generated or used, defaulting to -1
-							(one extra resource generated).
-					--]]
-					local buffParam = "buff_" .. tostring(powerType)
-					local buffAmoumtParam = buffParam .. "_amount"
-					if si[buffParam] and self:GetAura("player", si[buffParam], nil, true) then
-						local buffAmount = si[buffAmountParam] or -1
-						self.state[powerType] = self.state[powerType] - buffAmount
-					end
-					-- Clamp self.state[powerType] to lower and upper limits.
-					local mini = powerInfo.mini or 0
-					local maxi = powerInfo.maxi or OvalePower.maxPower[powerType]
-					if mini and self.state[powerType] < mini then
-						self.state[powerType] = mini
-					end
-					if maxi and self.state[powerType] > maxi then
-						self.state[powerType] = maxi
-					end
-				end
-			end
-		end
-
-		--[[
-			Combo points: This resource is handled specially because it has different semantics
-			from other resources. In particular, it's a resource that's attached to the target
-			and not to the player.
-		--]]
-		if si.combo then
-			local combo = si.combo
-			--[[
-				Combo points have the opposite meaning from other resources:
-
-				combo > 0 means that the spell generates resources.
-				combo < 0 means that the spell costs resources.
-				combo == 0 means that the spell uses all of the combo points.
-			--]]
-			if combo == 0 then
-				self.state.combo = 0
-			else
-				self.state.combo = self.state.combo + combo
-			end
-			--[[
-				Add extra combo points generated by presence of a buff.
-				"buff_combo" is the spell ID of the buff that causes extra points to be generated or used.
-				"buff_combo_amount" is the number of extra points generated or used, defaulting to 1
-					(one extra resource generated).
-			--]]
-			if si.buff_combo and self:GetAura("player", si.buff_combo, nil, true) then
-				local buffAmount = si.buff_combo_amount or 1
-				self.state.combo = self.state.combo + buffAmount
-			end
-			-- Clamp self.state.combo to lower and upper limits.
-			if self.state.combo < 0 then
-				self.state.combo = 0
-			end
-			if self.state.combo > MAX_COMBO_POINTS then
-				self.state.combo = MAX_COMBO_POINTS
-			end
-		end
-
 		-- Eclipse
 		if si.eclipse then
 			local energy = si.eclipse
@@ -417,7 +324,7 @@ function OvaleState:ApplySpellCost(spellId, startCast, endCast)
 			-- Clamp Eclipse energy to min/max values and note that an Eclipse state will be reached after the spellcast.
 			if self.state.eclipse <= -100 then
 				self.state.eclipse = -100
-				AddEclipse(endCast, LUNAR_ECLIPSE)
+				self.state:AddEclipse(endCast, LUNAR_ECLIPSE)
 				-- Reaching Lunar Eclipse resets the cooldown of Starfall.
 				local cd = self:GetCD(STARFALL)
 				if cd then
@@ -427,118 +334,22 @@ function OvaleState:ApplySpellCost(spellId, startCast, endCast)
 				end
 			elseif self.state.eclipse >= 100 then
 				self.state.eclipse = 100
-				AddEclipse(endCast, SOLAR_ECLIPSE)
+				self.state:AddEclipse(endCast, SOLAR_ECLIPSE)
 			end
 		end

 		-- Runes
 		if si.blood and si.blood < 0 then
-			self:AddRune(startCast, 1, si.blood)
+			AddRune(startCast, 1, si.blood)
 		end
 		if si.unholy and si.unholy < 0 then
-			self:AddRune(startCast, 2, si.unholy)
+			AddRune(startCast, 2, si.unholy)
 		end
 		if si.frost and si.frost < 0 then
-			self:AddRune(startCast, 3, si.frost)
+			AddRune(startCast, 3, si.frost)
 		end
 		if si.death and si.death < 0 then
-			self:AddRune(startCast, 4, si.death)
-		end
-	end
-end
-
--- XXX The way this function updates the rune state looks completely wrong.
-function OvaleState:AddRune(atTime, runeType, value)
-	for i = 1, 6 do
-		local rune = self.state.rune[i]
-		if (rune.type == runeType or rune.type == 4) and rune.cd <= atTime then
-			rune.cd = atTime + 10
-		end
-	end
-end
-
--- Apply the auras caused by the given spell in the simulator.
-function OvaleState:ApplySpellAuras(spellId, startCast, endCast, guid, auraList, spellcast)
-	for filter, filterInfo in pairs(auraList) do
-		for auraId, spellData in pairs(filterInfo) do
-			local si = OvaleData.spellInfo[auraId]
-			-- An aura is treated as a periodic aura if it sets "tick" explicitly in SpellInfo.
-			local isDoT = (si and si.tick)
-			local duration = spellData
-			local stacks = spellData
-
-			-- If aura is specified with a duration, then assume stacks == 1.
-			if type(duration) == "number" and duration > 0 then
-				stacks = 1
-			end
-			-- Set the duration to the proper length if it's a DoT.
-			if si and si.duration then
-				duration = self:GetDuration(auraId)
-			end
-
-			local start, ending, currentStacks, tick = self:GetAuraByGUID(guid, auraId, filter, true, target)
-			local newAura = self:NewAura(guid, auraId, filter)
-			newAura.mine = true
-
-			--[[
-				auraId=N, N > 0		N is duration, auraID is applied, add one stack
-				auraId=0			aura is removed
-				auraId=N, N < 0		N is number of stacks of aura removed
-				auraId=refresh		auraId is refreshed, no change to stacks
-			--]]
-			if type(stacks) == "number" and stacks == 0 then
-				Ovale:Logf("Aura %d is completely removed", auraId)
-				newAura.stacks = 0
-				newAura.start = start
-				newAura.ending = endCast
-			elseif ending and endCast <= ending then
-				-- Spellcast ends before the aura expires.
-				if stacks == "refresh" or stacks > 0 then
-					if stacks == "refresh" then
-						Ovale:Logf("Aura %d is refreshed", auraId)
-						newAura.stacks = currentStacks
-					else -- if stacks > 0 then
-						newAura.stacks = currentStacks + stacks
-						Ovale:Logf("Aura %d gains a stack to %d because of spell %d (ending was %s)", auraId, newAura.stacks, spellId, ending)
-					end
-					newAura.start = start
-					if isDoT and ending > newAura.start and tick and tick > 0 then
-						-- Add new duration after the next tick is complete.
-						local remainingTicks = floor((ending - endCast) / tick)
-						newAura.ending = (ending - tick * remainingTicks) + duration
-						newAura.tick = OvaleAura:GetTickLength(auraId)
-						-- Re-snapshot stats for the DoT.
-						-- XXX This is not quite right because it uses the current player stats instead of the simulator's state.
-						OvalePaperDoll:SnapshotStats(newAura, spellcast)
-						newAura.damageMultiplier = self:GetDamageMultiplier(auraId)
-					else
-						newAura.ending = endCast + duration
-					end
-					Ovale:Logf("Aura %d ending is now %f", auraId, newAura.ending)
-				elseif stacks < 0 then
-					newAura.stacks = currentStacks + stacks
-					newAura.start = start
-					newAura.ending = ending
-					Ovale:Logf("Aura %d loses %d stack(s) to %d because of spell %d", auraId, -1 * stacks, newAura.stacks, spellId)
-					if newAura.stacks <= 0 then
-						Ovale:Logf("Aura %d is completely removed", auraId)
-						newAura.stacks = 0
-						newAura.ending = endCast
-					end
-				end
-			elseif type(stacks) == "number" and type(duration) == "number" and stacks > 0 and duration > 0 then
-				Ovale:Logf("New aura %d at %f on %s", auraId, endCast, guid)
-				newAura.stacks = stacks
-				newAura.start = endCast
-				newAura.ending = endCast + duration
-				if isDoT then
-					newAura.tick = OvaleAura:GetTickLength(auraId)
-					-- Snapshot stats for the DoT.
-					-- XXX This is not quite right because it uses the current player stats instead of the simulator's state.
-					OvalePaperDoll:SnapshotStats(newAura, spellcast)
-					newAura.damageMultiplier = self:GetDamageMultiplier(auraId)
-				end
-			end
+			AddRune(startCast, 4, si.death)
 		end
 	end
 end
@@ -634,147 +445,27 @@ function OvaleState:GetComputedSpellCD(spellId)
 end

 function OvaleState:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
-	local aura
-	if mine then
-		local auraTable = self.aura[guid]
-		if auraTable then
-			if filter then
-				local auraList = auraTable[filter]
-				if auraList then
-					if auraList[spellId] and auraList[spellId].serial == self.serial then
-						aura = auraList[spellId]
-					end
-				end
-			else
-				for auraFilter, auraList in pairs(auraTable) do
-					if auraList[spellId] and auraList[spellId].serial == self.serial then
-						aura = auraList[spellId]
-						filter = auraFilter
-						break
-					end
-				end
-			end
-		end
-	end
-	if aura then
-		if aura.stacks > 0 then
-			Ovale:Logf("Found %s aura %s on %s", filter, spellId, guid)
-		else
-			Ovale:Logf("Found %s aura %s on %s (removed)", filter, spellId, guid)
-		end
-		if auraFound then
-			for k, v in pairs(aura) do
-				auraFound[k] = v
-			end
-		end
-		return aura.start, aura.ending, aura.stacks, aura.gain
-	else
-		Ovale:Logf("Aura %s not found in state for %s", spellId, guid)
-		return OvaleAura:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
-	end
+	return self.state:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
 end

-do
-	local aura = {}
-	local newAura = {}
-
-	function OvaleState:GetAura(unitId, spellId, filter, mine, auraFound)
-		local guid = OvaleGUID:GetGUID(unitId)
-		if OvaleData.buffSpellList[spellId] then
-			if auraFound then wipe(newAura) end
-			local newStart, newEnding, newStacks, newGain
-			for auraId in pairs(OvaleData.buffSpellList[spellId]) do
-				if auraFound then wipe(aura) end
-				local start, ending, stacks, gain = self:GetAuraByGUID(guid, auraId, filter, mine, unitId, aura)
-				if start and (not newStart or stacks > newStacks) then
-					newStart = start
-					newEnding = ending
-					newStacks = stacks
-					newGain = gain
-					if auraFound then
-						wipe(newAura)
-						for k, v in pairs(aura) do
-							newAura[k] = v
-						end
-					end
-				end
-			end
-			if auraFound then
-				for k, v in pairs(newAura) do
-					auraFound[k] = v
-				end
-			end
-			return newStart, newEnding, newStacks, newGain
-		else
-			return self:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
-		end
-	end
+function OvaleState:GetAura(unitId, spellId, filter, mine, auraFound)
+	return self.state:GetAura(unitId, spellId, filter, mine, auraFound)
 end

--- Look for an aura on any target, excluding the given GUID.
--- Returns the earliest start time, the latest ending time, and the number of auras seen.
 function OvaleState:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
-	local start, ending, count = OvaleAura:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
-	-- TODO: This is broken because it doesn't properly account for removed auras in the current frame.
-	for guid, auraTable in pairs(self.aura) do
-		if guid ~= excludingGUID then
-			for auraFilter, auraList in pairs(auraTable) do
-				if not filter or auraFilter == filter then
-					local aura = auraList[spellId]
-					if aura and aura.serial == self.serial then
-						if aura.start and (not start or aura.start < start) then
-							start = aura.start
-						end
-						if aura.ending and (not ending or aura.ending > ending) then
-							ending = aura.ending
-						end
-						count = count + 1
-					end
-				end
-			end
-		end
-	end
-	return start, ending, count
+	return self.state:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
 end

 function OvaleState:NewAura(guid, spellId, filter)
-	if not self.aura[guid] then
-		self.aura[guid] = {}
-	end
-	if not self.aura[guid][filter] then
-		self.aura[guid][filter] = {}
-	end
-	if not self.aura[guid][filter][spellId] then
-		self.aura[guid][filter][spellId] = {}
-	end
-	local aura = self.aura[guid][filter][spellId]
-	aura.serial = self.serial
-	aura.mine = true
-	aura.gain = self.currentTime
-	return aura
+	return self.state:NewAura(guid, spellId, filter)
 end

 function OvaleState:GetDamageMultiplier(spellId)
-	local damageMultiplier = 1
-	if spellId then
-		local si = OvaleData.spellInfo[spellId]
-		if si and si.damageAura then
-			local playerGUID = OvaleGUID:GetGUID("player")
-			for filter, auraList in pairs(si.damageAura) do
-				for auraSpellId, multiplier in pairs(auraList) do
-					local count = select(3, self:GetAuraByGUID(playerGUID, auraSpellId, filter, nil, "player"))
-					if count and count > 0 then
-						local auraSpellInfo = OvaleData.spellInfo[auraSpellId]
-						if auraSpellInfo.stacking and auraSpellInfo.stacking > 0 then
-							multiplier = 1 + (multiplier - 1) * count
-						end
-						damageMultiplier = damageMultiplier * multiplier
-					end
-				end
-			end
-		end
-	end
-	return damageMultiplier
+	return self.state:GetDamageMultiplier(spellId)
+end
+
+function OvaleState:GetDuration(auraSpellId)
+	return self.state:GetDuration(auraSpellId)
 end

 -- Returns 1 if moving toward Solar or -1 if moving toward Lunar.
@@ -861,45 +552,42 @@ function OvaleState:GetRunesCooldown(blood, frost, unholy, death, nodeath)
 	return maxCD
 end

--- Returns the duration, tick length, and number of ticks of an aura.
-function OvaleState:GetDuration(auraSpellId)
-	local si
-	if type(auraSpellId) == "number" then
-		si = OvaleData.spellInfo[auraSpellId]
-	elseif OvaleData.buffSpellList[auraSpellId] then
-		for spellId in pairs(OvaleData.buffSpellList[auraSpellId]) do
-			si = OvaleData.spellInfo[spellId]
-			if si then
-				auraSpellId = spellId
-				break
-			end
-		end
-	end
-	if si and si.duration then
-		local duration = si.duration
-		local combo = self.state.combo or 0
-		local holy = self.state.holy or 1
-		if si.adddurationcp then
-			duration = duration + si.adddurationcp * combo
-		end
-		if si.adddurationholy then
-			duration = duration + si.adddurationholy * (holy - 1)
-		end
-		if si.tick then	-- DoT
-			--DoT duration is tick * numTicks.
-			local tick = OvaleAura:GetTickLength(auraSpellId)
-			local numTicks = floor(duration / tick + 0.5)
-			duration = tick * numTicks
-			return duration, tick, numTicks
-		end
-		return duration
-	end
+--[[------------------------------
+	Legacy methods for transition.
+--]]------------------------------
+function OvaleState:GetCounterValue(id)
+	return self.state:GetCounterValue(id)
 end

--- Print out the levels of each power type in the current state.
-function OvaleState:DebugPower()
-	for powerType in pairs(OvalePower.POWER_INFO) do
-		Ovale:FormatPrint("%s = %d", powerType, self.state[powerType])
-	end
+function OvaleState:GetCD(spellId)
+	return self.state:GetCD(spellId)
+end
+
+function OvaleState:GetComputedSpellCD(spellId)
+	return self.state:GetSpellCooldown(spellId)
+end
+
+function OvaleState:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
+	return self.state:GetAuraByGUID(guid, spellId, filter, mine, unitId, auraFound)
+end
+
+function OvaleState:GetAura(unitId, spellId, filter, mine, auraFound)
+	return self.state:GetAura(unitId, spellId, filter, mine, auraFound)
+end
+
+function OvaleState:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
+	return self.state:GetAuraOnAnyTarget(spellId, filter, mine, excludingGUID)
+end
+
+function OvaleState:NewAura(guid, spellId, filter)
+	return self.state:NewAura(guid, spellId, filter)
+end
+
+function OvaleState:GetDamageMultiplier(spellId)
+	return self.state:GetDamageMultiplier(spellId)
+end
+
+function OvaleState:GetDuration(auraSpellId)
+	return self.state:GetDuration(auraSpellId)
 end
 --</public-static-methods>
diff --git a/compiler.pl b/compiler.pl
index 2399aee..6eb1c42 100644
--- a/compiler.pl
+++ b/compiler.pl
@@ -101,6 +101,7 @@ $sp{OvaleCondition}{ParseCondition} = true;
 $sp{OvaleCondition}{ParseRuneCondition} = true;
 $sp{OvaleCondition}{TestValue} = true;

+$m{OvaleQueue}{NewQueue} = true;
 $sp{OvaleQueue}{Front} = true;
 $sp{OvaleQueue}{FrontToBackIterator} = true;
 $sp{OvaleQueue}{InsertBack} = true;