diff --git a/Heimdall.toc b/Heimdall.toc
index 7affc63..e587394 100644
--- a/Heimdall.toc
+++ b/Heimdall.toc
@@ -7,6 +7,7 @@
#core
Modules/CLEUParser.lua
+Modules/ReactiveValue.lua
Modules/DumpTable.lua
Modules/Spotter.lua
Modules/Whoer.lua
diff --git a/Modules/ReactiveValue.lua b/Modules/ReactiveValue.lua
new file mode 100644
index 0000000..40ca42b
--- /dev/null
+++ b/Modules/ReactiveValue.lua
@@ -0,0 +1,667 @@
+local function Init()
+ local metadata = {
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __add = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value + other._value
+ end
+ if otherType == "string" and self._type == otherType then
+ return self._value .. other
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value + other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __mul = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value * other._value
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value * other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __sub = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value - other._value
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value - other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __div = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value / other._value
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value / other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __mod = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value % other._value
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value % other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return ReactiveValue|nil
+ __pow = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value ^ other._value
+ end
+ if otherType == "number" and self._type == otherType then
+ return self._value ^ other
+ end
+ return nil
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return boolean
+ __eq = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value == other._value
+ end
+ return self._value == other
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return boolean
+ __lt = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value < other._value
+ end
+ return self._value < other
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return boolean
+ __le = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value <= other._value
+ end
+ return self._value <= other
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return boolean
+ __gt = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value > other._value
+ end
+ return self._value > other
+ end,
+ ---@param self ReactiveValue
+ ---@param other ReactiveValue
+ ---@return boolean
+ __ge = function(self, other)
+ local otherType = type(other)
+ if otherType == "table" and other._type and other._type == self._type and other._value then
+ return self._value >= other._value
+ end
+ return self._value >= other
+ end,
+ ---@param self ReactiveValue
+ ---@return number
+ __len = function(self)
+ if self._type == "table" then
+ return #self._value
+ end
+ if self._type == "string" then
+ return string.len(self._value)
+ end
+ return 0
+ end,
+ ---@param self ReactiveValue
+ ---@return string
+ __tostring = function(self)
+ return tostring(self._value)
+ end,
+ ---@param self ReactiveValue
+ ---@param key string
+ ---@param value any
+ ---@return nil
+ __newindex = function(self, key, value)
+ local setupComplete = rawget(self, "_setupComplete")
+ if setupComplete == nil or setupComplete == false then
+ rawset(self, key, value)
+ return
+ end
+ if self._type ~= "table" then
+ rawset(self, key, value)
+ return
+ end
+
+ self._value[key] = value
+ local ChangedKey = { key }
+
+ -- If the value being assigned is a ReactiveValue
+ -- Then listen to changes on it as well
+ -- And propagate those changes upwards
+ if self._recursive and getmetatable(value) == getmetatable(self) then
+ self:_setupListeners(key, value)
+ end
+
+ self:_notify()
+ self:_notifyFieldChanged(ChangedKey)
+ self:_notifyAnyFieldChanged(ChangedKey)
+ end,
+ ---@param self ReactiveValue
+ ---@param key string
+ ---@return any|nil
+ __index = function(self, key)
+ local value = rawget(self, key)
+ if value ~= nil then
+ return value
+ end
+ if rawget(self, "_type") ~= "table" then
+ return nil
+ end
+ local innerTable = rawget(self, "_value")
+ if innerTable ~= nil then
+ return rawget(innerTable, key)
+ end
+ return nil
+ end
+ -- __index = ReactiveValue
+ }
+
+ --- Sadly I could not get @generic to play nice with this class
+ --- I think it's not ready yet, there are issues on github describing similar problems and it is marked as WIP...
+ --- Guess I'll have to live without it for now and specify type of a RV in #type
+
+ ---## A type safe value that can be listened to for changes
+ ---### **Always use RV:set() for setting primitive values**
+ --- Supports primitive values and tables
+ --- Tables can be listened to for changes on any field or a specific field
+ ---### Example usage (value):
+ --- ```lua
+ --- local test = ReactiveValue.new(1)
+ --- test:onChange(function(value)
+ --- print("test changed to " .. value)
+ --- end)
+ --- test:set(2)
+ --- test:set(test + 3)
+ --- ```
+ ---### Example usage (table):
+ --- ```lua
+ --- local test = ReactiveValue.new({1, 2, 3})
+ --- test:onAnyFieldChange(function(field, value)
+ --- print(string.format("test.%s changed to %s", table.concat(field, "."), value))
+ --- end)
+ --- test[1] = 4 -- test.1 changed to 4
+ --- test[4] = {1, 2, 3} -- test.4 changed to
+ --- test[4][1] = 14 -- No log(!!) because test[4] is a table and not a ReactiveValue
+ --- ```
+ ---### To trigger a callback for `test[4][1]` in the previous example do:
+ --- ```lua
+ --- local test = ReactiveValue.new({1, 2, 3}, true)
+ --- test:onAnyFieldChange(function(field, value)
+ --- print(string.format("test.%s changed to %s", table.concat(field, "."), value))
+ --- end)
+ --- test[1] = 4 -- test.1 changed to 4
+ --- test[4] = {1, 2, 3} -- test.4 changed to
+ --- test[4][1] = 14 -- test.4.1 changed to 14
+ --- ```
+ ---### To listen to a specific field of a table do:
+ --- ```lua
+ --- local test = ReactiveValue.new({1, 2, 3}, true)
+ --- test:onFieldChange("1", function(value)
+ --- print("test.1 changed to " .. value)
+ --- end)
+ --- test[1] = 4 -- test.1 changed to 4
+ --- test[4] = {1, 2, 3} -- Does not trigger callback
+ -- ```
+ ---@class ReactiveValue
+ ---@field _listeners table
+ ---@field _fieldListeners table>
+ ---@field _anyFieldListeners table>
+ ---@field _oneTimeListeners table
+ ---@field _value any
+ ---@field _type string
+ ---@field _recursive boolean?
+ ReactiveValue = {
+ ---#### Get the underlying value of a ReactiveValue
+ ---@param self ReactiveValue
+ ---@return any
+ get = function(self) end,
+
+ ---### Set the underlying value of a ReactiveValue triggering listener callbacks
+ ---@param self ReactiveValue
+ ---@param newValue any
+ set = function(self, newValue) end,
+
+ ---## EVENT
+ ---### Register a listener that is triggered whenever the underlying value changes
+ --- Returns a function that can be called to undo the callback
+ ---@param self ReactiveValue
+ ---@param callback fun(value: any, type: string)
+ ---@return fun(): nil
+ onChange = function(self, callback) end,
+
+ ---## EVENT
+ ---### Register a listener that is triggered whenever a specific field of a table changes
+ --- Returns a function that can be called to undo the callback
+ ---@param self ReactiveValue
+ ---@param field string
+ ---@param callback fun(field: string[], value: any, type: string)
+ ---@return fun(): nil
+ onFieldChange = function(self, field, callback) end,
+
+ ---## EVENT
+ ---### Register a listener that is triggered whenever any field of a table changes
+ --- Returns a function that can be called to undo the callback
+ ---@param self ReactiveValue
+ ---@param callback fun(field: string[], value: any, type: string)
+ ---@param depth number? How deep to listen for changes
+ ---@return fun(): nil
+ onAnyFieldChange = function(self, callback, depth) end,
+
+ ---## EVENT
+ ---### Register a listener that is triggered ONCE whenever the underlying value changes
+ --- Returns a function that can be called to undo the callback
+ ---@param self ReactiveValue
+ ---@param callback fun(value: any, type: string)
+ ---@return fun(): nil
+ once = function(self, callback) end,
+ ---### Setup listeners for all fields of a table recursively
+ --- This is used to ensure that listeners are notified recursively
+
+ ---@param self ReactiveValue
+ _setupAllListenersRecursively = function(self) end,
+ ---### Setup listeners for a specific field of a table recursively
+ --- This is used to ensure that listeners are notified recursively
+
+ ---@param self ReactiveValue
+ _setupListeners = function(self, key, value, recursive) end,
+
+ ---### Notify listeners that the underlying value has changed
+ ---@param self ReactiveValue
+ ---@return nil
+ ---#### Event contains:
+ --- 2. value: any - The new value of the changed field
+ --- 3. type: string - The type of the new value of the changed field
+ _notify = function(self) end,
+
+ ---### Notify listeners that a specific field of the underlying value has changed
+ ---#### Event contains:
+ --- 1. field: table - A list of keys that lead to the changed field
+ --- 2. value: any - The new value of the changed field
+ --- 3. type: string - The type of the new value of the changed field
+ ---@param self ReactiveValue
+ _notifyFieldChanged = function(self, field) end,
+
+ ---### Notify listeners that any field of the underlying value has changed
+ ---#### Event contains:
+ --- 1. field: table - A list of keys that lead to the changed field
+ --- 2. value: any - The new value of the changed field
+ --- 3. type: string - The type of the new value of the changed field
+ _notifyAnyFieldChanged = function(self, field) end,
+ }
+ ---### Constructor
+ ---@param initialValue any
+ ---@param recursive boolean?
+ ---@return ReactiveValue
+ ReactiveValue.new = function(initialValue, recursive)
+ local self = setmetatable({}, metadata)
+ self._listeners = {}
+ self._fieldListeners = {}
+ self._anyFieldListeners = {}
+ self._oneTimeListeners = {}
+ self._value = initialValue
+ self._type = type(initialValue)
+ self._recursive = recursive or false
+
+ ---@return any
+ self.get = function(self)
+ return self._value
+ end
+ ---@param newValue any
+ self.set = function(self, newValue)
+ if self._value == newValue then
+ return
+ end
+ if type(newValue) ~= self._type then
+ error("Expected " .. self._type .. ", got " .. type(newValue))
+ return
+ end
+ self._value = newValue
+ self:_notify()
+ end
+ self.onChange = function(self, callback)
+ if type(callback) ~= "function" then
+ error("Expected function, got " .. type(callback))
+ return function() end
+ end
+ self._listeners[callback] = true
+ return function()
+ self._listeners[callback] = nil
+ end
+ end
+ self.onFieldChange = function(self, field, callback)
+ if type(callback) ~= "function" then
+ error("Expected function, got " .. type(callback))
+ return function() end
+ end
+ if self._fieldListeners[field] == nil then
+ self._fieldListeners[field] = {}
+ end
+ self._fieldListeners[field][callback] = true
+ return function()
+ self._fieldListeners[field][callback] = nil
+ end
+ end
+ self.onAnyFieldChange = function(self, callback, depth)
+ depth = depth or 99999
+ if type(callback) ~= "function" then
+ error("Expected function, got " .. type(callback))
+ return function() end
+ end
+ if self._anyFieldListeners[depth] == nil then
+ self._anyFieldListeners[depth] = {}
+ end
+ self._anyFieldListeners[depth][callback] = true
+ return function()
+ self._anyFieldListeners[depth][callback] = nil
+ end
+ end
+ self.once = function(self, callback)
+ if type(callback) ~= "function" then
+ error("Expected function, got " .. type(callback))
+ return function() end
+ end
+ self._oneTimeListeners[callback] = true
+ return function()
+ self._oneTimeListeners[callback] = nil
+ end
+ end
+
+ self._setupAllListenersRecursively = function(self)
+ if self._type ~= "table" then
+ return
+ end
+ for key, value in pairs(self._value) do
+ self:_setupListeners(key, value, true)
+ end
+ end
+ ---@param key string
+ ---@param value any
+ ---@param recursive boolean?
+ self._setupListeners = function(self, key, value, recursive)
+ recursive = recursive or false
+ if self._type ~= "table" then
+ return
+ end
+ if getmetatable(value) ~= getmetatable(self) then
+ return
+ end
+ value._recursive = true
+ if value._type == "table" then
+ value:onAnyFieldChange(function(key2)
+ ChangedKey = { key, table.unpack(key2) }
+ self:_notifyFieldChanged(ChangedKey)
+ self:_notifyAnyFieldChanged(ChangedKey)
+ end)
+ else
+ value:onChange(function(newVal)
+ ChangedKey = { key }
+ self:_notifyFieldChanged(ChangedKey)
+ self:_notifyAnyFieldChanged(ChangedKey)
+ end)
+ end
+
+ if recursive then
+ value:_setupAllListenersRecursively()
+ end
+ end
+
+ if recursive then
+ self:_setupAllListenersRecursively()
+ end
+
+ self._notify = function(self)
+ for listener, _ in pairs(self._oneTimeListeners) do
+ -- task.spawn(listener, self._value, self._type)
+ listener(self._value, self._type)
+ self._oneTimeListeners[listener] = nil
+ end
+ for listener, _ in pairs(self._listeners) do
+ -- task.spawn(listener, self._value, self._type)
+ listener(self._value, self._type)
+ end
+ end
+ -- TODO: Maybe implement some sort of regex here or something...
+ -- Such as listening to *.field1 or something
+ -- But this (having to loop over listeners and evaluate some condition) would tank performance
+ -- Compared to a simple lookup
+ -- So I'm not going to do anything about it for now, until I figure out a better way
+ ---@param field table A list of keys that lead to the changed field
+ ---@return nil
+ self._notifyFieldChanged = function(self, field)
+ local value = self._value
+ for _, key in ipairs(field) do
+ value = value[key]
+ end
+
+ local strfield = table.concat(field, ".")
+ if self._fieldListeners[strfield] == nil then
+ return
+ end
+ for listener, _ in pairs(self._fieldListeners[strfield]) do
+ -- task.spawn(listener, value, type(value))
+ listener(value, type(value))
+ end
+ end
+ ---@param self ReactiveValue
+ ---@param field table A list of keys that lead to the changed field
+ ---@return nil
+ self._notifyAnyFieldChanged = function(self, field)
+ local value = self._value
+ for _, key in ipairs(field) do
+ value = value[key]
+ end
+ local keyDepth = #field
+ for listenerDepth, listeners in pairs(self._anyFieldListeners) do
+ if listenerDepth >= keyDepth then
+ for listener, _ in pairs(listeners) do
+ -- The reason this also returns type(value) is so that clients don't have to compute type(value)
+ -- I assume some of them might want to do it so computing it once is probably better than having every client compute it for themselves
+ -- task.spawn(listener, field, value, type(value))
+ listener(field, value, type(value))
+ end
+ end
+ end
+ end
+
+ self._setupComplete = true
+ return self
+ end
+
+ -- S -- begintest
+ -- S local invocations = 0
+ -- S -- Integer example
+ -- S local test = ReactiveValue.new(1)
+ -- S test:onChange(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test changed to " .. value)
+ -- S end)
+ -- S test:set(2)
+ -- S assert(invocations == 1)
+ -- S
+ -- S invocations = 0
+ -- String example
+ -- S test = ReactiveValue.new("test")
+ -- S test:onChange(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test changed to " .. value)
+ -- S end)
+ -- S test:set("test2")
+ -- S assert(invocations == 1)
+ -- S
+ -- S -- Type safety example
+ -- S local res, err = pcall(test.set, test, 1)
+ -- S assert(res == false)
+ -- S assert(err:find("Expected string, got number"))
+ -- S
+ -- S -- Table example
+ -- S invocations = 0
+ -- S test = ReactiveValue.new({1, 2, 3})
+ -- S local clbk = test:onChange(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test changed to")
+ -- S dumpTable(value, 0)
+ -- S end)
+ -- S test:set({1, 2, 3, 4})
+ -- S assert(invocations == 1)
+ -- S
+ -- S -- Callback removal example
+ -- S clbk()
+ -- S
+ -- S invocations = 0
+ -- S -- Any field change example
+ -- S clbk = test:onAnyFieldChange(function(field, value)
+ -- S invocations = invocations + 1
+ -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
+ -- S end)
+ -- S test.Pero = 1
+ -- S test.Pero = nil
+ -- S assert(invocations == 2)
+ -- S clbk()
+ -- S
+ -- S invocations = 0
+ -- S -- Field change example
+ -- S test:onFieldChange("Pero", function(value)
+ -- S invocations = invocations + 1
+ -- S print("test.Pero changed to " .. value)
+ -- S end)
+ -- S test.Pero = 2
+ -- S assert(invocations == 1)
+ -- S
+ -- S invocations = 0
+ -- S -- One time listener example
+ -- S test:once(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test changed to")
+ -- S dumpTable(value, 0)
+ -- S end)
+ -- S test:set({3, 2, 1})
+ -- S assert(invocations == 1)
+ -- S
+ -- S invocations = 0
+ -- S -- Table push example
+ -- S test = ReactiveValue.new({})
+ -- S test:onChange(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test changed to")
+ -- S dumpTable(value, 0)
+ -- S end)
+ -- S test:onAnyFieldChange(function(field, value)
+ -- S invocations = invocations + 1
+ -- S print("test." .. table.concat(field, ".") .. " changed to " .. value)
+ -- S end)
+ -- S test[#test + 1] = 4
+ -- S assert(invocations == 2)
+ -- S
+ -- S invocations = 0
+ -- S test = ReactiveValue.new({
+ -- S name = "pero",
+ -- S coins = ReactiveValue.new(1)
+ -- S })
+ -- S test.coins:onChange(function(value)
+ -- S invocations = invocations + 1
+ -- S print("test.coins changed to " .. value)
+ -- S end)
+ -- S test.coins:set(2)
+ -- S assert(invocations == 1)
+ -- S
+ -- S invocations = 0
+ -- S test = ReactiveValue.new({
+ -- S name = "pero",
+ -- S coins = ReactiveValue.new(1)
+ -- S }, true)
+ -- S test:onAnyFieldChange(function(field, value)
+ -- S invocations = invocations + 1
+ -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
+ -- S end)
+ -- S test.coins:set(2)
+ -- S test.pero2 = ReactiveValue.new({})
+ -- S test.pero2.coins = ReactiveValue.new(1)
+ -- S test.pero2.coins:set(2)
+ -- S assert(invocations == 4)
+ -- S
+ -- S invocations = 0
+ -- S test = ReactiveValue.new({
+ -- S name = "pero",
+ -- S coins = ReactiveValue.new({
+ -- S value = ReactiveValue.new(1)
+ -- S })
+ -- S }, true)
+ -- S test:onAnyFieldChange(function(field, value)
+ -- S invocations = invocations + 1
+ -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
+ -- S end)
+ -- S test.coins.value:set(2)
+ -- S assert(invocations == 1)
+ -- S
+ -- S invocations = 0
+ -- S test = ReactiveValue.new({}, true)
+ -- S test.coins = ReactiveValue.new({})
+ -- S test.coins.value = ReactiveValue.new(1)
+ -- S test:onAnyFieldChange(function(field, value)
+ -- S invocations = invocations + 1
+ -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
+ -- S end)
+ -- S test.coins.value:set(3)
+ -- S assert(invocations == 1)
+ --S
+ --S invocations = 0
+ --S test = ReactiveValue.new({}, true)
+ --S test:onAnyFieldChange(function(field, value)
+ --S invocations = invocations + 1
+ --S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
+ --S end, 1)
+ --S test.test2 = ReactiveValue.new({}, true)
+ --S test.test2.test3 = ReactiveValue.new(1)
+ --S assert(invocations == 1)
+ --S
+ -- S -- endtest
+end
+
+local frame = CreateFrame("Frame")
+frame:RegisterEvent("PLAYER_LOGIN")
+frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+frame:RegisterEvent("GUILD_ROSTER_UPDATE")
+frame:SetScript("OnEvent", function(self, event, ...)
+ Init()
+end)
+Init()