feat(xml): support <KeyValues> on XML elements

A <KeyValue key= value= type= keyType=/> entry sets object[key] = value
on frames, textures, font strings, animation groups and animations.
Types are nil, boolean, number, string and global, as in the later
clients' schema. Template entries apply first so an element's own
entries win, and all are set before OnLoad. Bad entries log a load
warning and are skipped.

C_XMLUtil.GetTemplateInfo now returns the template's own entries in
keyValues instead of an empty table.
This commit is contained in:
Brues
2026-10-20 07:35:53 -05:00
parent 9b01cca336
commit a2e3eed0de
6 changed files with 282 additions and 4 deletions
+45 -1
View File
@@ -20736,7 +20736,7 @@ local info = C_XMLUtil.GetTemplateInfo("StatFrameTemplate")
|---|---|---|
| `type` | string | The template's frame type — its XML element tag (`"Frame"`, `"Button"`, …). |
| `width` / `height` | number | The size **statically declared on the template itself** via a `<Size>` element, or `0` if it declares none (including when the size comes from an inherited template rather than a direct `<Size>`). |
| `keyValues` | table | Always an **empty table** — the XML schema has no `<KeyValues>` element, so no template can carry key/value pairs. Present for API parity. |
| `keyValues` | table | The template's own [`<KeyValues>`](#keyvalues-in-xml) entries, in file order. Each entry is `{ key, keyType, type, value }`, with the text as written in the file. `keyType` and `type` are `"string"` when the file leaves them out. Entries from inherited templates are not included. The table is empty when the template has no entries. |
| `inherits` | string? | The template's `inherits=` attribute (a comma-delimited list), or `nil` if it inherits nothing. |
Lookup is by name through the engine's own template registry — the same
@@ -20807,6 +20807,50 @@ already sees them, and a `parentKey` inside a virtual template is set on
every frame built from it. A button's `<NormalTexture parentKey="...">`
works the same way.
### `KeyValues` in XML
A `<KeyValues>` element sets fields on the Lua table of the element that
holds it. Each `<KeyValue>` inside it is the same as `object[key] = value`:
```xml
<Frame name="MyFrame">
<KeyValues>
<KeyValue key="layoutIndex" value="3" type="number"/>
<KeyValue key="title" value="Options"/>
<KeyValue key="tooltipFrame" value="GameTooltip" type="global"/>
</KeyValues>
<Scripts>
<OnLoad>print(this.layoutIndex, this.title, this.tooltipFrame:GetName())</OnLoad>
</Scripts>
</Frame>
```
Frames, textures, font strings, `<AnimationGroup>` and animation elements
can hold a `<KeyValues>` element.
`type` sets how `value` is read. `keyType` sets how `key` is read. Both
are `string` if you leave them out:
| Type | Result |
|---|---|
| `string` | The text as written. |
| `number` | The text as a number. |
| `boolean` | `true` if the text starts with `t`, `y` or a digit from `1` to `9` (any case), or is `on` or `enabled`. Otherwise `false`. |
| `nil` | `nil`. This type clears a field that a template set. |
| `global` | The global variable with that name, `_G[value]`. |
Numbers keep about seven significant digits, so integers up to 16777216
are exact.
A template's entries apply first, so an element's own entries replace
entries with the same key. The fields are set before `OnLoad` runs, and a
`<KeyValues>` element inside a virtual template applies to every frame
built from it. A `global` value reads the global when the element loads, so
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.
### Masks in XML
A `<Texture>` element takes a `mask` attribute. It is the same as
+14
View File
@@ -25,6 +25,7 @@
#include "Offsets.h"
#include "baselib/Ascii.h"
#include "xml/FrameLoad.h"
#include "xml/KeyValues.h"
#include <cstdio>
#include <cstdlib>
@@ -102,6 +103,17 @@ void LoadScripts(Object *object, const void *node) {
}
}
// <KeyValues> on a group or an animation, as on later clients' schema.
void LoadKeyValues(Object *object, const void *node, void *status) {
void *L = Game::Lua::State();
if (L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
Push(L, object);
::Xml::KeyValues::Apply(L, top + 1, node, status, object->DisplayName());
Game::Lua::SetTop(L, top);
}
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
void LoadOrigin(Anim *anim, const void *node) {
const void *origin = FindChild(node, "Origin");
@@ -176,6 +188,7 @@ void LoadGroup(Group *group, const void *node, void *status) {
group->initialOffsetY = y;
}
LoadScripts(group, node);
LoadKeyValues(group, node, status);
LoadAnimChildren(group, node, status);
}
@@ -255,6 +268,7 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
anim->order = value < 0 ? 0 : (value > 99 ? 99 : value);
}
LoadScripts(anim, node);
LoadKeyValues(anim, node, status);
// The subclasses' reads after the base.
// CSimpleAlphaAnim::LoadXML sets the change, 0 when it is missing.
+182
View File
@@ -0,0 +1,182 @@
// 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/>.
// `<KeyValues>` on XML elements, the later-client way to set fields on an
// element's Lua table at load time:
//
// <Frame name="Foo">
// <KeyValues>
// <KeyValue key="layoutIndex" value="3" type="number"/>
// <KeyValue key="tooltip" value="GameTooltip" type="global"/>
// </KeyValues>
// </Frame>
//
// Each entry is `object[key] = value`. `type` and `keyType` (default
// "string") are one of nil / boolean / number / string / global, the later
// clients' KEYVALUETYPE; "global" is `_G[text]`. Booleans and numbers go
// through the same loader helpers as `<Attributes>` (FUN_XML_ATTR_TO_BOOL,
// FUN_SSTR_TO_FLOAT).
//
// The schema allows it on every layout element and on animation groups and
// animations. Frames are read here, through the shared Xml::FrameLoad
// observer; textures and font strings from their loader co-hooks in
// Xml::ParentKey; groups and animations from Animation::Xml. Every one of
// those runs once per inherited template node before the element's own node,
// so a template's values are set first and the element's own win, and all of
// them run before the builder's finalize, so OnLoad sees the fields.
#include "xml/KeyValues.h"
#include "Game.h"
#include "Offsets.h"
#include "baselib/Ascii.h"
#include "xml/FrameLoad.h"
namespace Xml::KeyValues {
namespace {
enum class Kind { Nil, Boolean, Number, String, Global, Invalid };
// A missing type is the schema's default, "string".
Kind KindOf(const char *type) {
if (type == nullptr || Ascii::EqualCI(type, "string"))
return Kind::String;
if (Ascii::EqualCI(type, "nil"))
return Kind::Nil;
if (Ascii::EqualCI(type, "boolean"))
return Kind::Boolean;
if (Ascii::EqualCI(type, "number"))
return Kind::Number;
if (Ascii::EqualCI(type, "global"))
return Kind::Global;
return Kind::Invalid;
}
void Push(void *L, Kind kind, const char *text) {
using ToBool_t = int(__fastcall *)(const char *);
using ToFloat_t = float(__stdcall *)(const char *);
switch (kind) {
case Kind::Boolean:
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0);
break;
case Kind::Number:
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(text));
break;
case Kind::String:
Game::Lua::PushString(L, text);
break;
case Kind::Global:
Game::Lua::PushString(L, text);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
break;
default:
Game::Lua::PushNil(L);
break;
}
}
const void *FindKeyValues(const void *node) {
for (const void *c = FirstChild(node); c != nullptr; c = NextSibling(c))
if (TagIs(c, "KeyValues"))
return c;
return nullptr;
}
} // namespace
void Apply(void *L, int index, const void *node, void *status, const char *owner) {
const void *keyValues = FindKeyValues(node);
if (keyValues == nullptr)
return;
const int top = Game::Lua::GetTop(L);
for (const void *e = FirstChild(keyValues); e != nullptr; e = NextSibling(e)) {
if (!TagIs(e, "KeyValue")) {
Warn(status, "%s: Unknown KeyValues element %s", owner, Tag(e));
continue;
}
const char *key = Attr(e, "key");
if (key == nullptr) {
Warn(status, "%s: KeyValue element missing key", owner);
continue;
}
const char *keyTypeName = Attr(e, "keyType");
const char *typeName = Attr(e, "type");
const Kind keyKind = KindOf(keyTypeName);
const Kind kind = KindOf(typeName);
if (keyKind == Kind::Invalid || kind == Kind::Invalid) {
Warn(status, "%s: KeyValue %s has unknown type %s", owner, key,
keyKind == Kind::Invalid ? keyTypeName : typeName);
continue;
}
const char *value = Attr(e, "value");
if (value == nullptr && kind != Kind::Nil) {
Warn(status, "%s: KeyValue %s missing value", owner, key);
continue;
}
Push(L, keyKind, key); // [key]
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL) {
// A nil key would raise from SetTable; nothing can be stored.
Warn(status, "%s: KeyValue %s has a nil key", owner, key);
Game::Lua::SetTop(L, top);
continue;
}
Push(L, kind, value); // [key, value]
Game::Lua::SetTable(L, index); // object[key] = value
Game::Lua::SetTop(L, top);
}
}
void ApplyToRegion(void *object, const void *node, void *status) {
if (FindKeyValues(node) == nullptr)
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
if (PushWrapper(L, object))
Apply(L, top + 1, node, status, Game::Lua::ObjectDisplayName(object));
Game::Lua::SetTop(L, top);
}
void PushInfo(void *L, const void *node) {
Game::Lua::NewTable(L); // [list]
const void *keyValues = FindKeyValues(node);
if (keyValues == nullptr)
return;
int n = 0;
for (const void *e = FirstChild(keyValues); e != nullptr; e = NextSibling(e)) {
const char *key = Attr(e, "key");
if (!TagIs(e, "KeyValue") || key == nullptr)
continue;
const char *keyType = Attr(e, "keyType");
const char *type = Attr(e, "type");
const char *value = Attr(e, "value");
Game::Lua::PushNumber(L, static_cast<double>(++n)); // [list, n]
Game::Lua::NewTable(L); // [list, n, entry]
Game::Lua::SetFieldString(L, "key", key);
Game::Lua::SetFieldString(L, "keyType", keyType != nullptr ? keyType : "string");
Game::Lua::SetFieldString(L, "type", type != nullptr ? type : "string");
Game::Lua::SetFieldString(L, "value", value != nullptr ? value : "");
Game::Lua::SetTable(L, -3); // list[n] = entry
}
}
namespace {
const FrameLoad::AutoSubscribe _frameNode{&ApplyToRegion};
} // namespace
} // namespace Xml::KeyValues
+32
View File
@@ -0,0 +1,32 @@
// 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
// `<KeyValues>` on XML elements — see KeyValues.cpp.
namespace Xml::KeyValues {
// Sets each <KeyValues> entry of `node` on the Lua table at stack index
// `index`. `owner` names the element in load warnings.
void Apply(void *L, int index, const void *node, void *status, const char *owner);
// Apply() for a frame, texture or font string: pushes the object's wrapper
// only when `node` has a <KeyValues> child.
void ApplyToRegion(void *object, const void *node, void *status);
// Pushes `node`'s own entries as an array of { key, keyType, type, value }
// (C_XMLUtil.GetTemplateInfo's keyValues), the strings as written.
void PushInfo(void *L, const void *node);
} // namespace Xml::KeyValues
+6
View File
@@ -70,6 +70,9 @@
// for that state (SetTextFontObject / SetHighlightFontObject /
// SetDisabledFontObject) with `_G[style]`.
//
// The texture and font string loaders also apply <KeyValues>
// (Xml::KeyValues, which reads frames itself).
//
// Every loader also loads an <Animations> child (Animation::Xml), before the
// builder finalizes, so OnLoad sees the groups.
//
@@ -82,6 +85,7 @@
#include "Offsets.h"
#include "animation/Xml.h"
#include "texture/Mask.h"
#include "xml/KeyValues.h"
#include <cstdint>
@@ -250,6 +254,7 @@ uintptr_t __fastcall TextureLoad_h(void *self, void *edx, const void *node, void
const uintptr_t r = g_origTextureLoad(self, edx, node, status);
if (self != nullptr && node != nullptr) {
BindParentKey(RegionObject(self), node);
Xml::KeyValues::ApplyToRegion(RegionObject(self), node, status);
Animation::Xml::LoadAnimations(RegionObject(self), node, status);
if (const char *mask = Xml::Attr(node, "mask"); mask != nullptr && *mask != '\0')
Texture::Mask::SetPath(RegionObject(self), mask);
@@ -261,6 +266,7 @@ uintptr_t __fastcall FontStringLoad_h(void *self, void *edx, const void *node, v
const uintptr_t r = g_origFontStringLoad(self, edx, node, status);
if (self != nullptr && node != nullptr) {
BindParentKey(RegionObject(self), node);
Xml::KeyValues::ApplyToRegion(RegionObject(self), node, status);
Animation::Xml::LoadAnimations(RegionObject(self), node, status);
}
return r;
+3 -3
View File
@@ -31,6 +31,7 @@
#include "Game.h"
#include "Offsets.h"
#include "xml/KeyValues.h"
#include <cstdint>
#include <cstdlib>
@@ -179,10 +180,9 @@ int __fastcall Script_GetTemplateInfo(void *L) {
Game::Lua::SetFieldNumber(L, "width", w);
Game::Lua::SetFieldNumber(L, "height", h);
// keyValues: vanilla's XML schema has no <KeyValues> element, so this is
// always an empty table (parity with retail's "empty if none defined").
// keyValues: the template's own <KeyValues> entries, as written.
Game::Lua::PushString(L, "keyValues");
Game::Lua::NewTable(L);
Xml::KeyValues::PushInfo(L, node);
Game::Lua::SetTable(L, -3);
// inherits: the comma-delimited `inherits=` attribute, or nil (field left