feat(xml): support mixin= and method= on XML elements

mixin="A, B" on frames, textures, font strings and animation groups
copies each global table onto the element's Lua table, as Mixin() does.
The outermost load of an object applies the whole inherits chain once,
templates first, before the engine loads any node, so KeyValues and
scripts build on the mixins instead of being overwritten by them. A
missing mixin logs a load warning.

Script elements take method="Name", binding self[Name] as the handler,
on frames, animation groups and animations.
This commit is contained in:
Brues
2026-10-20 07:43:04 -05:00
parent a2e3eed0de
commit 2cab0dcb12
7 changed files with 322 additions and 21 deletions
+45 -2
View File
@@ -885,8 +885,10 @@ build instructions.
- [`C_XMLUtil.DoesTemplateExist(name)`](#c_xmlutildoestemplateexistname)
- [`inherits` with more than one template](#inherits-with-more-than-one-template)
- [`parentKey` in XML](#parentkey-in-xml)
- [`KeyValues` in XML](#keyvalues-in-xml)
- [`mixin` in XML](#mixin-in-xml)
- [Masks in XML](#masks-in-xml)
- [`function` on XML script handlers](#function-on-xml-script-handlers)
- [`function` and `method` on XML script handlers](#function-and-method-on-xml-script-handlers)
- [`style` on button font elements](#style-on-button-font-elements)
- [`C_TTSSettings` — getters & setters](#c_ttssettings--getters--setters)
- [TTS events](#tts-events)
@@ -20851,6 +20853,35 @@ the global must exist by then.
If an entry has no `key`, has no `value`, or has an unknown type,
`FrameXML.log` gets a warning and the entry is skipped.
### `mixin` in XML
A `mixin` attribute names one or more global tables. Commas or spaces
divide the names. Each table's fields are copied onto the element's Lua
table, the same as `Mixin(object, ...)`:
```xml
<Frame name="MyFrame" mixin="MyFrameMixin">
<Scripts>
<OnLoad method="OnLoad"/>
<OnEvent method="OnEvent"/>
</Scripts>
</Frame>
```
Frames, textures, font strings and `<AnimationGroup>` elements take a
`mixin` attribute. The tables must exist when the XML loads, so define them
in a Lua file that loads before the XML file.
The mixins of the templates in `inherits` apply first, in template order.
Then the element's own mixins apply, so a later mixin replaces fields of an
earlier one. All mixins apply before the element's
[`<KeyValues>`](#keyvalues-in-xml) and scripts load, so a `<KeyValue>`
replaces a default that a mixin gives. Frames from `CreateFrame` with a
template get the template's mixins too.
If a name is not a global table, `FrameXML.log` gets the warning
`Couldn't find mixin` and the other names still apply.
### Masks in XML
A `<Texture>` element takes a `mask` attribute. It is the same as
@@ -20896,7 +20927,7 @@ A `<MaskedTexture>` can name a texture from any layer of the same frame. If
it names no texture, the load log gets a warning and the mask is not
attached.
### `function` on XML script handlers
### `function` and `method` on XML script handlers
A script element with `function="Name"` uses the global function `Name` as
that script's handler. It is the same as
@@ -20909,6 +20940,18 @@ that script's handler. It is the same as
</Scripts>
```
A script element with `method="Name"` uses the frame's own field
`self.Name` as the handler. The field usually comes from a
[mixin](#mixin-in-xml):
```xml
<Scripts>
<OnLoad method="OnLoad"/>
</Scripts>
```
Animation groups and animations take `function` and `method` the same way.
The function is looked up when the element loads, so it must be defined by then. A bound `OnLoad` runs at the
usual point, once the frame is built, and with
[positional script arguments](#setmodernscriptargsenable--getmodernscriptargs)
+45 -5
View File
@@ -26,6 +26,7 @@
#include "baselib/Ascii.h"
#include "xml/FrameLoad.h"
#include "xml/KeyValues.h"
#include "xml/Mixin.h"
#include <cstdio>
#include <cstdlib>
@@ -70,7 +71,8 @@ void BindParentKey(void *object, Object *child, const char *key) {
Game::Lua::SetTop(L, top);
}
// <Scripts>: each element's body, or its `function` global.
// <Scripts>: each element's body, its `function` global, or its `method`
// field on the object.
void LoadScripts(Object *object, const void *node) {
const void *scripts = FindChild(node, "Scripts");
if (scripts == nullptr)
@@ -80,13 +82,22 @@ void LoadScripts(Object *object, const void *node) {
if (script == nullptr)
continue;
const char *function = ::Xml::Attr(s, "function");
if (function != nullptr && *function != '\0') {
const char *method = ::Xml::Attr(s, "method");
const bool byFunction = function != nullptr && *function != '\0';
if (byFunction || (method != nullptr && *method != '\0')) {
uint32_t *slot = object->ScriptSlot(script);
void *L = Game::Lua::State();
if (slot == nullptr || L == nullptr)
continue;
Game::Lua::PushString(L, function);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
if (byFunction) {
Game::Lua::PushString(L, function);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
} else {
Push(L, object);
Game::Lua::PushString(L, method);
Game::Lua::GetTable(L, -2);
Game::Lua::Remove(L, -2);
}
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
Game::Lua::SetTop(L, -2);
continue;
@@ -158,14 +169,41 @@ void LoadAnimChildren(Group *group, const void *node, void *status) {
const void *Template(const char *name) { return ::Xml::Template(name); }
namespace {
// LoadGroup re-enters itself for each inherited template; the mixins of the
// whole chain go in once, from the outermost call, before any node's
// KeyValues or scripts (see Xml::Mixin).
int g_groupDepth = 0;
void LoadGroupNode(Group *group, const void *node, void *status);
} // namespace
void LoadGroup(Group *group, const void *node, void *status) {
if (g_groupDepth == 0) {
if (void *L = Game::Lua::State()) {
const int top = Game::Lua::GetTop(L);
Push(L, group);
::Xml::Mixin::ApplyChain(L, top + 1, node, status, group->DisplayName(), false);
Game::Lua::SetTop(L, top);
}
}
++g_groupDepth;
LoadGroupNode(group, node, status);
--g_groupDepth;
}
namespace {
void LoadGroupNode(Group *group, const void *node, void *status) {
group->flags |= GROUP_LOADING;
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
const void *base = ::Xml::Template(inherits);
if (base == nullptr)
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
else
LoadGroup(group, base, status);
LoadGroupNode(group, base, status);
}
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
@@ -192,6 +230,8 @@ void LoadGroup(Group *group, const void *node, void *status) {
LoadAnimChildren(group, node, status);
}
} // namespace
void LoadAnim(Anim *anim, const void *node, void *status) {
// The subclass reads its values, then CSimpleAnim::LoadXML (inherits
// first, then the shared attributes) - the order 4.3.4's overrides use.
+7 -5
View File
@@ -24,15 +24,15 @@
// CreateFromMixins(...) -- Mixin({}, ...); returns the new table.
// Non-table extra arguments are skipped (the Lua form guards nil the same way).
#include "baselib/Mixin.h"
#include "Game.h"
namespace BaseLib::Mixin {
namespace {
// Shallow-copy every pair from the table at `srcIdx` onto the table at
// `dstIdx` (both absolute, positive stack indices). Stack-balanced.
void MixinInto(void *L, int dstIdx, int srcIdx) {
void Into(void *L, int dstIdx, int srcIdx) {
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, srcIdx) != 0) {
// stack top: ..., key (-2), value (-1). Copy both so the originals
@@ -44,6 +44,8 @@ void MixinInto(void *L, int dstIdx, int srcIdx) {
}
}
namespace {
int __fastcall Script_Mixin(void *L) {
const int top = Game::Lua::GetTop(L);
if (top < 1 || Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
@@ -52,7 +54,7 @@ int __fastcall Script_Mixin(void *L) {
}
for (int i = 2; i <= top; ++i)
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
MixinInto(L, 1, i);
Into(L, 1, i);
Game::Lua::PushValue(L, 1); // return object
return 1;
}
@@ -63,7 +65,7 @@ int __fastcall Script_CreateFromMixins(void *L) {
const int objIdx = top + 1;
for (int i = 1; i <= top; ++i)
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
MixinInto(L, objIdx, i);
Into(L, objIdx, i);
Game::Lua::SetTop(L, objIdx); // the new table is the sole result
return 1;
}
+22
View File
@@ -0,0 +1,22 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
namespace BaseLib::Mixin {
// dst[k] = v for every field of the table at `srcIdx` — Mixin()'s copy.
// Both indices must be absolute.
void Into(void *L, int dstIdx, int srcIdx);
} // namespace BaseLib::Mixin
+144
View File
@@ -0,0 +1,144 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `mixin="A, B"` on frames, textures, font strings and animation groups, the
// later-client attribute: each name is a global table whose fields are
// copied onto the element's Lua table, as `Mixin(object, A, B)` does.
//
// ORDER. A mixin carries methods and default fields; <KeyValues> and the
// element's scripts build on them, so every mixin must be in place before
// any of those load. The engine's loaders load a template by re-entering the
// same loader for the template node (FUN_XML_FRAME_LOAD through
// Xml::Inherits, the texture and font string loaders through
// `(*self)->vtable[+8]` at their top), and the KeyValues for a template node
// are set when that inner load returns. So mixins cannot be applied per
// node: an element's own mixin would land after its template's KeyValues and
// overwrite their defaults. Instead the OUTERMOST load of an object walks
// the whole inherits chain once, before the engine runs, and mixes in each
// template's mixins (templates first, in inherits order) and then the
// element's own. Inner loads of the same object apply nothing.
//
// A name that is not a global table is skipped with a load warning.
#include "xml/Mixin.h"
#include "Game.h"
#include "baselib/Mixin.h"
#include "xml/FrameLoad.h"
#include "xml/Inherits.h"
#include <vector>
namespace Xml::Mixin {
namespace {
// Objects whose load is in progress, outermost first. Nesting is the
// inherits chain plus anything a Before subscriber loads, so this stays a
// handful of entries deep.
std::vector<void *> g_loading;
// Deeper than any real chain; stops a template that inherits itself.
constexpr int kMaxChainDepth = 32;
void ApplyNode(void *L, int index, const void *node, void *status, const char *owner,
bool listInherits, int depth) {
if (depth > kMaxChainDepth)
return;
const char *inherits = Attr(node, "inherits");
if (inherits != nullptr && *inherits != '\0') {
const auto applyTemplate = [&](const char *name) {
if (const void *t = Template(name))
ApplyNode(L, index, t, status, owner, listInherits, depth + 1);
};
if (listInherits)
Inherits::ForEach(inherits, applyTemplate);
else
applyTemplate(inherits);
}
const char *mixins = Attr(node, "mixin");
if (mixins == nullptr || *mixins == '\0')
return;
Inherits::ForEach(mixins, [&](const char *name) {
Game::Lua::PushString(L, name);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); // [mixin]
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
BaseLib::Mixin::Into(L, index, Game::Lua::GetTop(L));
else
Warn(status, "%s: Couldn't find mixin %s", owner, name);
Game::Lua::SetTop(L, -2);
});
}
bool HasMixinInChain(const void *node, bool listInherits, int depth) {
if (depth > kMaxChainDepth)
return false;
if (const char *m = Attr(node, "mixin"); m != nullptr && *m != '\0')
return true;
const char *inherits = Attr(node, "inherits");
if (inherits == nullptr || *inherits == '\0')
return false;
bool found = false;
const auto check = [&](const char *name) {
if (!found)
if (const void *t = Template(name))
found = HasMixinInChain(t, listInherits, depth + 1);
};
if (listInherits)
Inherits::ForEach(inherits, check);
else
check(inherits);
return found;
}
} // namespace
void ApplyChain(void *L, int index, const void *node, void *status, const char *owner,
bool listInherits) {
ApplyNode(L, index, node, status, owner, listInherits, 0);
}
void Begin(void *object, const void *node, void *status, bool listInherits) {
bool outer = true;
for (void *o : g_loading)
if (o == object)
outer = false;
g_loading.push_back(object);
if (!outer || !HasMixinInChain(node, listInherits, 0))
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
if (PushWrapper(L, object))
ApplyChain(L, top + 1, node, status, Game::Lua::ObjectDisplayName(object), listInherits);
Game::Lua::SetTop(L, top);
}
void End(void *object) {
if (!g_loading.empty() && g_loading.back() == object)
g_loading.pop_back();
}
namespace {
void BeginFrame(void *object, const void *node, void *status) { Begin(object, node, status, true); }
void EndFrame(void *object, const void *, void *) { End(object); }
const FrameLoad::AutoSubscribeBefore _frameBegin{&BeginFrame};
const FrameLoad::AutoSubscribe _frameEnd{&EndFrame};
} // namespace
} // namespace Xml::Mixin
+35
View File
@@ -0,0 +1,35 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// `mixin="A, B"` on XML elements — see Mixin.cpp.
namespace Xml::Mixin {
// Mixes the mixins of `node`'s whole inherits chain into the Lua table at
// stack index `index`: each template's (recursively) in inherits order, then
// the node's own. `listInherits` reads `inherits` as a list, as frames do;
// regions and animation groups take it as one name. `owner` names the
// element in load warnings.
void ApplyChain(void *L, int index, const void *node, void *status, const char *owner,
bool listInherits);
// For a loader hook that the engine re-enters for each inherited template:
// Begin before the engine loads `node`, End after. Only the object's
// outermost load applies the chain, so every mixin is in place before any
// node's KeyValues and scripts.
void Begin(void *object, const void *node, void *status, bool listInherits);
void End(void *object);
} // namespace Xml::Mixin
+24 -9
View File
@@ -60,7 +60,9 @@
// binds its whole OnLoad this way). After the engine has loaded the node, each
// script element carrying `function` becomes `frame:SetScript(<element>,
// _G[function])`. The loader runs before the builder's finalize, so a bound
// OnLoad still fires at the usual point.
// OnLoad still fires at the usual point. `method="Name"` does the same with
// `frame[Name]`, the frame's own field - usually a function from its mixin
// (Xml::Mixin), which is in place before any node of the frame loads.
//
// A button's font elements take their font from `style` on later clients
// (`<NormalFont style="GameFontNormal"/>`) where vanilla reads `inherits`; the
@@ -86,6 +88,7 @@
#include "animation/Xml.h"
#include "texture/Mask.h"
#include "xml/KeyValues.h"
#include "xml/Mixin.h"
#include <cstdint>
@@ -126,11 +129,12 @@ void BindParentKey(void *object, const void *node) {
Game::Lua::SetTop(L, top);
}
// ---- <Scripts> function= ---------------------------------------------------
// ---- <Scripts> function= / method= -----------------------------------------
// frame:SetScript(<element>, _G[function]) for every <Scripts> child of `node`
// that carries a `function` attribute. A name that is not a global function,
// or a script the frame type does not have, is skipped.
// that carries a `function` attribute, or frame:SetScript(<element>,
// frame[method]) for one that carries `method`. A name that is not a
// function, or a script the frame type does not have, is skipped.
void BindScriptFunctions(void *object, const void *node) {
const void *scripts = nullptr;
for (const void *c = FirstChild(node); c != nullptr; c = NextSibling(c)) {
@@ -149,8 +153,10 @@ void BindScriptFunctions(void *object, const void *node) {
bool havePushed = false;
for (const void *e = FirstChild(scripts); e != nullptr; e = NextSibling(e)) {
const char *fn = Xml::Attr(e, "function");
const char *method = Xml::Attr(e, "method");
const char *script = Xml::Tag(e);
if (fn == nullptr || *fn == '\0' || script == nullptr)
const bool byFunction = fn != nullptr && *fn != '\0';
if (script == nullptr || (!byFunction && (method == nullptr || *method == '\0')))
continue;
if (!havePushed) {
if (!PushWrapper(L, object)) // [frame]
@@ -163,8 +169,9 @@ void BindScriptFunctions(void *object, const void *node) {
break;
Game::Lua::PushValue(L, top + 1); // [frame, SetScript, frame]
Game::Lua::PushString(L, script); // [.., frame, script]
Game::Lua::PushString(L, fn);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); // [.., frame, script, _G[fn]]
Game::Lua::PushString(L, byFunction ? fn : method);
Game::Lua::GetTable(L, byFunction ? Game::Lua::GLOBALS_INDEX
: top + 1); // [.., frame, script, handler]
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION)
Game::Lua::PCall(L, 3, 0, 0);
Game::Lua::SetTop(L, top + 1); // [frame]
@@ -251,8 +258,12 @@ void *RegionObject(void *self) {
}
uintptr_t __fastcall TextureLoad_h(void *self, void *edx, const void *node, void *status) {
if (self == nullptr || node == nullptr)
return g_origTextureLoad(self, edx, node, status);
Xml::Mixin::Begin(RegionObject(self), node, status, false);
const uintptr_t r = g_origTextureLoad(self, edx, node, status);
if (self != nullptr && node != nullptr) {
Xml::Mixin::End(RegionObject(self));
{
BindParentKey(RegionObject(self), node);
Xml::KeyValues::ApplyToRegion(RegionObject(self), node, status);
Animation::Xml::LoadAnimations(RegionObject(self), node, status);
@@ -263,8 +274,12 @@ uintptr_t __fastcall TextureLoad_h(void *self, void *edx, const void *node, void
}
uintptr_t __fastcall FontStringLoad_h(void *self, void *edx, const void *node, void *status) {
if (self == nullptr || node == nullptr)
return g_origFontStringLoad(self, edx, node, status);
Xml::Mixin::Begin(RegionObject(self), node, status, false);
const uintptr_t r = g_origFontStringLoad(self, edx, node, status);
if (self != nullptr && node != nullptr) {
Xml::Mixin::End(RegionObject(self));
{
BindParentKey(RegionObject(self), node);
Xml::KeyValues::ApplyToRegion(RegionObject(self), node, status);
Animation::Xml::LoadAnimations(RegionObject(self), node, status);