diff --git a/totalRP3/Core/Color.lua b/totalRP3/Core/Color.lua index 22f45ed5c..7bc085ef2 100644 --- a/totalRP3/Core/Color.lua +++ b/totalRP3/Core/Color.lua @@ -153,7 +153,7 @@ function ColorCache:Acquire(r, g, b, a) assert(b >= 0 and b <= 1, "invalid color component: b"); assert(a >= 0 and a <= 1, "invalid color component: a"); - color = TRP3_API.ApplyPrototypeToObject({ r = r, g = g, b = b, a = a }, Color); + color = TRP3_API.SetObjectPrototype({ r = r, g = g, b = b, a = a }, Color); self.cache[key] = color; end @@ -166,7 +166,7 @@ end -- inherited from our prototype. function TRP3_API.ApplyColorPrototype(color) - return TRP3_API.ApplyPrototypeToObject(color, Color); + return TRP3_API.SetObjectPrototype(color, Color); end function TRP3_API.CreateColor(r, g, b, a) diff --git a/totalRP3/Core/Objects/Callback.lua b/totalRP3/Core/Objects/Callback.lua index 3735dd5c1..fd9080130 100644 --- a/totalRP3/Core/Objects/Callback.lua +++ b/totalRP3/Core/Objects/Callback.lua @@ -45,7 +45,7 @@ function Callback:Unregister() end function TRP3_API.CreateCallback(registry, event, callback, owner, ...) - return TRP3_API.CreateAndInitFromPrototype(Callback, registry, event, callback, owner, ...); + return TRP3_API.CreateObject(Callback, registry, event, callback, owner, ...); end function TRP3_API.IsEventValid(registry, event) diff --git a/totalRP3/Core/Objects/CallbackGroup.lua b/totalRP3/Core/Objects/CallbackGroup.lua index 105ec72d5..dfaeeae8d 100644 --- a/totalRP3/Core/Objects/CallbackGroup.lua +++ b/totalRP3/Core/Objects/CallbackGroup.lua @@ -48,5 +48,5 @@ function CallbackGroup:Unregister() end function TRP3_API.CreateCallbackGroup() - return TRP3_API.CreateAndInitFromPrototype(CallbackGroup); + return TRP3_API.CreateObject(CallbackGroup); end diff --git a/totalRP3/Core/Objects/CallbackGroupCollection.lua b/totalRP3/Core/Objects/CallbackGroupCollection.lua index f6861ed46..593d633b3 100644 --- a/totalRP3/Core/Objects/CallbackGroupCollection.lua +++ b/totalRP3/Core/Objects/CallbackGroupCollection.lua @@ -86,5 +86,5 @@ function CallbackGroupCollection:UnregisterGroup(key) end function TRP3_API.CreateCallbackGroupCollection() - return TRP3_API.CreateAndInitFromPrototype(CallbackGroupCollection); + return TRP3_API.CreateObject(CallbackGroupCollection); end diff --git a/totalRP3/Core/Objects/CallbackRegistry.lua b/totalRP3/Core/Objects/CallbackRegistry.lua index fa2b6db5f..61bcc5de8 100644 --- a/totalRP3/Core/Objects/CallbackRegistry.lua +++ b/totalRP3/Core/Objects/CallbackRegistry.lua @@ -109,7 +109,7 @@ function CallbackRegistry:TriggerEvent(event, ...) end function TRP3_API.CreateCallbackRegistry() - return TRP3_API.CreateAndInitFromPrototype(CallbackRegistry); + return TRP3_API.CreateObject(CallbackRegistry); end function TRP3_API.CreateCallbackRegistryWithEvents(events) diff --git a/totalRP3/Core/Prototype.lua b/totalRP3/Core/Prototype.lua index e262c9a63..916366b00 100644 --- a/totalRP3/Core/Prototype.lua +++ b/totalRP3/Core/Prototype.lua @@ -4,84 +4,165 @@ ---@class TRP3_API local TRP3_API = select(2, ...); --- Prototype factory --- --- This implements a basic wrapper around Lua's prototypical metatable system --- by providing a slightly more convenient way of defining metatables for --- object-like tables. --- --- A "prototype" is a table that has arbitrary key/value pairs. When a prototype --- is fed to the CreateFromPrototype function those key/value pairs will be --- shallow-copied to a metatable. For standard key/value pairs they will be --- placed in an '__index' table for lookups, and for magic '__foo' style keys --- they will be placed at the root of the metatable. --- --- The metatables created from prototypes are cached - so creation from a --- single prototype should yield the same metatable on all instantiated objects. - -local PrototypeMetatableFactory = { cache = setmetatable({}, { __mode = "kv" }) }; - -function PrototypeMetatableFactory:Create(prototype) - local metatable = { __index = {} }; - local index = metatable.__index; - - for k, v in pairs(prototype) do - if type(k) == "string" and string.find(k, "^__") then +--[[ + This file defines a few convenience functions for instantiating objects + with metatable-based inheritance from prototypes. + + Calling the CreateObject function will create a new object that inherits + from an optional prototype. Any accesses for fields that don't exist + on the object will instead consult the prototype, as if via '__index'. + + local Person = {}; + + function Person:__init(name) self.name = name; end + function Person:Greet() print("Hello", self.name); end + + local Bob = TRP3_API.CreateObject(Person); + Bob:Greet(); -- prints "Hello Bob" + + This system does not enforce a strict model of inheritance, but either + catenative or chained models are supported. The below example uses + chained inheritance to say that cats are pets, and catenative inheritance + to make cats feedable. + + local Pet = {}; + function Pet:__init(name) self.name = name; end + function Pet:GetName() return self.name; end + + local Feedable = {}; + function Feedable:Feed(food) print("You feed", self:GetName(), food, "."); end; + + local Cat = TRP3_API.CreateObject(Pet); + Mixin(Cat, Feedable); + + local Mittens = TRP3_API.CreateObject(Cat, "Mittens"); + Mittens:Feed("bananas"); -- prints "You feed Mittens bananas." + + Creation and initialization of objects can be customized by defining two + the '__allocate' and '__init' methods on a prototype respectively. These + methods are both optional, and will not cause errors if omitted. + + Prototypes may define fields that match the names of a restricted subset + of metamethods. These metamethods will be made available to all objects + that derive from the prototype. +]]-- + +local ProxyMethods = {}; + +function ProxyMethods.__add(lhs, rhs) return lhs:__add(rhs); end +function ProxyMethods.__call(lhs, rhs) return lhs:__call(rhs); end +function ProxyMethods.__concat(lhs, rhs) return lhs:__concat(rhs); end +function ProxyMethods.__div(lhs, rhs) return lhs:__div(rhs); end +function ProxyMethods.__eq(lhs, rhs) return lhs:__eq(rhs); end +function ProxyMethods.__lt(lhs, rhs) return lhs:__lt(rhs); end +function ProxyMethods.__mod(lhs, rhs) return lhs:__mod(rhs); end +function ProxyMethods.__mul(lhs, rhs) return lhs:__mul(rhs); end +function ProxyMethods.__pow(lhs, rhs) return lhs:__pow(rhs); end +function ProxyMethods.__sub(lhs, rhs) return lhs:__sub(rhs); end +function ProxyMethods.__tostring(op) return op:__tostring(); end +function ProxyMethods.__unm(op) return op:__unm(); end + +local function MixinProxyMethods(metatable, prototype) + for k, v in pairs(ProxyMethods) do + if prototype[k] then metatable[k] = v; - else - index[k] = v; end end +end + +local MetatableCache = setmetatable({}, { __mode = "kv" }); - -- If the prototype comes with its own '__index' then it will be used - -- for all lookups that don't hit the 'index' table that we just created. +local function GetPrototypeMetatable(prototype) + local metatable = MetatableCache[prototype]; - if metatable.__index ~= index and next(index) ~= nil then - metatable.__index = setmetatable(index, { __index = metatable.__index }); + if prototype and not metatable then + metatable = { __index = prototype, __prototype = prototype }; + MixinProxyMethods(metatable, prototype); + MetatableCache[prototype] = metatable; end return metatable; end -function PrototypeMetatableFactory:GetOrCreate(prototype) - local metatable = self.cache[prototype]; +local function AllocateObject(prototype) + local object; - if not metatable then - metatable = self:Create(prototype); - self.cache[prototype] = metatable; + if prototype and prototype.__allocate then + object = prototype:__allocate(); + else + object = {}; end - return metatable; + return object; end ----@generic T : table ----@param prototype T ----@return T object -function TRP3_API.CreateFromPrototype(prototype) - local metatable = PrototypeMetatableFactory:GetOrCreate(prototype); - return setmetatable({}, metatable); +local function ConstructObject(object, ...) + if object.__init then + object:__init(...); + end end ----@generic T : table & { __init: fun(object: table, ...: any)? } ----@param prototype T +--- Allocates and initializes a new object that optionally inherits all fields +--- from a supplied prototype. +--- +---@generic T +---@param prototype T? ---@param ... any ---@return T object -function TRP3_API.CreateAndInitFromPrototype(prototype, ...) - local object = TRP3_API.CreateFromPrototype(prototype); +function TRP3_API.CreateObject(prototype, ...) + local metatable = GetPrototypeMetatable(prototype); + local object = AllocateObject(prototype); + setmetatable(object, metatable); + ConstructObject(object, ...); + return object; +end - ---@cast prototype table - if prototype.__init then - prototype.__init(object, ...); - end +--- Allocates a new object and optionally associates it with a supplied +--- prototype, but does not initialize the object. +--- +--- If the prototype defines an '__allocate' method, it will be invoked and +--- the resulting object will be returned by this function with a metatable +--- assigned that links the object to the prototype. Otherwise, if no such +--- method exists then an empty table is created instead. +--- +---@generic T +---@param prototype T? +---@return T object +function TRP3_API.AllocateObject(prototype) + local metatable = GetPrototypeMetatable(prototype); + local object = AllocateObject(prototype); + return setmetatable(object, metatable); +end +--- Initializes a previously allocated object, invoking the '__init' method +--- on the object with the supplied parameters if such a method is defined. +--- +---@generic T +---@param object T +---@return table object +function TRP3_API.ConstructObject(object, ...) + ConstructObject(object, ...); return object; end ----@generic T : table +--- Returns the prototype assigned to an object. +--- +---@param object table +---@return table? prototype +function TRP3_API.GetObjectPrototype(object) + local metatable = getmetatable(object); + return metatable and metatable.__prototype or nil; +end + +--- Changes the prototype assigned to an object. +--- +--- This function does not call the '__init' method on the object. +--- +---@generic T ---@param object table ---@param prototype T ---@return T object -function TRP3_API.ApplyPrototypeToObject(object, prototype) - local metatable = PrototypeMetatableFactory:GetOrCreate(prototype); +function TRP3_API.SetObjectPrototype(object, prototype) + local metatable = GetPrototypeMetatable(prototype); return setmetatable(object, metatable); end diff --git a/totalRP3/Modules/Analytics/Analytics.lua b/totalRP3/Modules/Analytics/Analytics.lua index 1eb201dac..7ab2afe1e 100644 --- a/totalRP3/Modules/Analytics/Analytics.lua +++ b/totalRP3/Modules/Analytics/Analytics.lua @@ -23,23 +23,23 @@ local L = TRP3_API.loc; -- through a "/trp3 statistics" command. -- -local SinkPrototype = {}; +---@class TRP3_Analytics.BasicSink +local BasicSink = {}; -function SinkPrototype:WriteBoolean(id, state) -- luacheck: no unused +function BasicSink:WriteBoolean(id, state) -- luacheck: no unused -- Override in a custom sink implementation. end -function SinkPrototype:WriteCounter(id, count) -- luacheck: no unused +function BasicSink:WriteCounter(id, count) -- luacheck: no unused -- Override in a custom sink implementation. end -function SinkPrototype:Flush() +function BasicSink:Flush() -- Override in a custom sink implementation. end --- Chat frame sink - -local ChatFrameSink = CreateFromMixins(SinkPrototype); +---@class TRP3_Analytics.ChatFrameSink : TRP3_Analytics.BasicSink +local ChatFrameSink = CreateFromMixins(BasicSink); function ChatFrameSink:__init() self.results = {}; @@ -83,31 +83,32 @@ function ChatFrameSink:Flush() end local function CreateChatFrameSink() - return TRP3_API.CreateAndInitFromPrototype(ChatFrameSink); + return TRP3_API.CreateObject(ChatFrameSink); end -- Wago analytics sink -local WagoAnalyticsSink = CreateFromMixins(SinkPrototype); +---@class TRP3_Analytics.WagoSink : TRP3_Analytics.BasicSink +local WagoSink = CreateFromMixins(BasicSink); -function WagoAnalyticsSink:__init(wagoAddonID) +function WagoSink:__init(wagoAddonID) self.handle = WagoAnalytics:Register(wagoAddonID); end -function WagoAnalyticsSink:WriteBoolean(id, state) +function WagoSink:WriteBoolean(id, state) self.handle:Switch(id, state); end -function WagoAnalyticsSink:WriteCounter(id, count) +function WagoSink:WriteCounter(id, count) self.handle:SetCounter(id, count); end -function WagoAnalyticsSink:Flush() +function WagoSink:Flush() self.handle:Save(); end -local function CreateWagoAnalyticsSink(wagoAddonID) - return TRP3_API.CreateAndInitFromPrototype(WagoAnalyticsSink, wagoAddonID); +local function CreateWagoSink(wagoAddonID) + return TRP3_API.CreateObject(WagoSink, wagoAddonID); end -- Analytics module @@ -152,7 +153,7 @@ function TRP3_Analytics:OnAddonsUnloading() local wagoAddonID = GetAddOnMetadata("totalRP3", "X-Wago-ID"); if WagoAnalytics and wagoAddonID and self:IsDataCollectionEnabled() then - local sink = CreateWagoAnalyticsSink(wagoAddonID); + local sink = CreateWagoSink(wagoAddonID); self:Collect(sink); end end diff --git a/totalRP3/Modules/Automation/Automation.lua b/totalRP3/Modules/Automation/Automation.lua index d5ca1d168..9dacc689c 100644 --- a/totalRP3/Modules/Automation/Automation.lua +++ b/totalRP3/Modules/Automation/Automation.lua @@ -45,7 +45,7 @@ function ConditionContext:__init(condition, option) end local function CreateConditionContext(condition, option) - return TRP3_API.CreateAndInitFromPrototype(ConditionContext, condition, option); + return TRP3_API.CreateObject(ConditionContext, condition, option); end local ActionContext = CreateFromMixins(BaseContext); @@ -60,7 +60,7 @@ function ActionContext:Apply(...) end local function CreateActionContext(action, option) - return TRP3_API.CreateAndInitFromPrototype(ActionContext, action, option); + return TRP3_API.CreateObject(ActionContext, action, option); end TRP3_Automation = TRP3_Addon:NewModule("Automation", "AceConsole-3.0"); diff --git a/totalRP3/Modules/Register/Main/RegisterTooltip.lua b/totalRP3/Modules/Register/Main/RegisterTooltip.lua index ecc5f1485..e2a094711 100644 --- a/totalRP3/Modules/Register/Main/RegisterTooltip.lua +++ b/totalRP3/Modules/Register/Main/RegisterTooltip.lua @@ -371,7 +371,7 @@ function TooltipBuilder:Build() end function TRP3_API.ui.tooltip.createTooltipBuilder(tooltip) - return TRP3_API.CreateAndInitFromPrototype(TooltipBuilder, tooltip); + return TRP3_API.CreateObject(TooltipBuilder, tooltip); end --*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* diff --git a/totalRP3/UI/Browsers/IconBrowser.lua b/totalRP3/UI/Browsers/IconBrowser.lua index 9755a0fdb..bd5d479a6 100644 --- a/totalRP3/UI/Browsers/IconBrowser.lua +++ b/totalRP3/UI/Browsers/IconBrowser.lua @@ -124,7 +124,7 @@ end ---@param query string ---@param model TRP3.AbstractIconBrowserModel local function CreateIconBrowserSearchTask(query, model) - return TRP3_API.CreateAndInitFromPrototype(IconBrowserSearchTask, query, model); + return TRP3_API.CreateObject(IconBrowserSearchTask, query, model); end -- Icon Browser Data Models @@ -176,7 +176,7 @@ function IconBrowserModel:GetIconIndex(name) end local function CreateIconBrowserModel() - return TRP3_API.CreateAndInitFromPrototype(IconBrowserModel); + return TRP3_API.CreateObject(IconBrowserModel); end --- IconBrowserFilterModel is a proxy model that implements asynchronous @@ -359,7 +359,7 @@ end ---@param source TRP3.AbstractIconBrowserModel local function CreateIconBrowserFilterModel(source) - return TRP3_API.CreateAndInitFromPrototype(IconBrowserFilterModel, source); + return TRP3_API.CreateObject(IconBrowserFilterModel, source); end --- IconBrowserSelectionModel is a proxy model that relocates the currently @@ -475,7 +475,7 @@ end ---@param source TRP3.AbstractIconBrowserModel local function CreateIconBrowserSelectionModel(source) - return TRP3_API.CreateAndInitFromPrototype(IconBrowserSelectionModel, source); + return TRP3_API.CreateObject(IconBrowserSelectionModel, source); end --- Creates a data provider that displays the contents of an icon data model