Skip to content

Commit

Permalink
Simplify object prototype system (#850)
Browse files Browse the repository at this point in the history
This commit simplifies some of the implementation details of object
prototypes, as well as renames the (internal) functions for actually
using prototypes.

The main renames are as follows:

  - CreateFromPrototype -> AllocateObject
  - CreateAndInitFromPrototype -> CreateObject
  - ApplyPrototypeToObject -> SetObjectPrototype

Additional functions have been added for initializing an allocated
object (ConstructObject) and querying the prototype of an object
(GetObjectPrototype). Additionally, the '__init' method is now fully
optional and may be omitted.

In terms of implementation details, the creation of metatables for
prototypes no longer takes a shallow copy of the prototype, and instead
just connects objects to their prototypes directly via '__index'. This
means that changes to any fields in prototypes are made visible to
objects that inherit the prototype, and don't become stuck.

For metamethods, we no longer support the '__index', '__newindex', or
'__le' fields being defined. These are unused and are weird enough to
generally just be worth avoiding for objects anyway.

Finally, allocation of objects can now be customized. If a prototype
defines an '__allocate' method then it will be invoked (with the
prototype passed as 'self') and is expected to return a table for the
object to live inside. Like '__init' this method is fully optional; if
not defined then the default is to just create an empty table.
  • Loading branch information
Meorawr authored Mar 18, 2024
1 parent 32314ef commit d77345c
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 81 deletions.
4 changes: 2 additions & 2 deletions totalRP3/Core/Color.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion totalRP3/Core/Objects/Callback.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion totalRP3/Core/Objects/CallbackGroup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ function CallbackGroup:Unregister()
end

function TRP3_API.CreateCallbackGroup()
return TRP3_API.CreateAndInitFromPrototype(CallbackGroup);
return TRP3_API.CreateObject(CallbackGroup);
end
2 changes: 1 addition & 1 deletion totalRP3/Core/Objects/CallbackGroupCollection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,5 @@ function CallbackGroupCollection:UnregisterGroup(key)
end

function TRP3_API.CreateCallbackGroupCollection()
return TRP3_API.CreateAndInitFromPrototype(CallbackGroupCollection);
return TRP3_API.CreateObject(CallbackGroupCollection);
end
2 changes: 1 addition & 1 deletion totalRP3/Core/Objects/CallbackRegistry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
185 changes: 133 additions & 52 deletions totalRP3/Core/Prototype.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 17 additions & 16 deletions totalRP3/Modules/Analytics/Analytics.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions totalRP3/Modules/Automation/Automation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion totalRP3/Modules/Register/Main/RegisterTooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

--*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
Expand Down
Loading

0 comments on commit d77345c

Please sign in to comment.