loader: an unnamed instance keeps its own name, so $parent reaches the real parent

`framexml::merge` merged the inherited template's `name` onto the instance, so an unnamed child of a named virtual template took the template's name. ItemRack's `<Frame inherits="ItemRackQueueTemplate"/>` then named its texture after the template instead of the parent frame, and `Rack.OnEvent` indexed the missing `ItemRackInv*Queue` global.

The reference reads `name` and `id` from the instance alone (`PreLoadXML 0x769770`); drop both from the merge base.
This commit is contained in:
chairmanpow
2026-10-02 15:45:38 +01:00
parent ec3349ad96
commit 8b81d08153
2 changed files with 48 additions and 3 deletions
+12 -3
View File
@@ -315,8 +315,9 @@ fn element_from_node(node: roxmltree::Node) -> Element {
/// attributes override or extend the template's (case-insensitively). The reference merges
/// nothing: `LoadXML` re-enters itself on the one object against each template, the most distant
/// first, then the instance (`0x76985c`; `<Frames>` repeat the walk, `0x76a060`), and reads `name`
/// and `id` from the instance alone (`PreLoadXML 0x769770`). Here `name` and `virtual` merge like
/// any attribute. A cycle is skipped with a warning.
/// and `id` from the instance alone (`PreLoadXML 0x769770`). Here every attribute but `name` and
/// `id` merges, `virtual` among them, so an unnamed child of a named template stays unnamed. A
/// cycle is skipped with a warning.
pub fn expand(
element: &Element,
templates: &HashMap<&str, &Element>,
@@ -392,7 +393,15 @@ fn expand_inner(
/// [`expand`]'s merge; `over`'s tag wins, and its body when it has one.
fn merge(base: &Element, over: &Element) -> Element {
let mut attrs = base.attrs.clone();
// `name` and `id` belong to the instance alone (`PreLoadXML 0x769770`): an unnamed child that
// inherits a *named* virtual template stays unnamed, so its regions expand `$parent` against
// the real parent frame rather than the template's name. Every other attribute merges.
let mut attrs: Vec<(String, String)> = base
.attrs
.iter()
.filter(|(k, _)| !k.eq_ignore_ascii_case("name") && !k.eq_ignore_ascii_case("id"))
.cloned()
.collect();
for (k, v) in &over.attrs {
if let Some(slot) = attrs.iter_mut().find(|(ek, _)| ek.eq_ignore_ascii_case(k)) {
slot.1 = v.clone();
+36
View File
@@ -2445,6 +2445,42 @@ mod chunk_name_tests {
"the reference's own no-argument contract must reach the same frame"
);
}
/// An unnamed child that inherits a *named* virtual template stays unnamed: the reference
/// reads `name` from the instance alone (`PreLoadXML 0x769770`), so the child's regions expand
/// `$parent` against the real parent frame. ItemRack ships this shape — an unnamed
/// `<Frame inherits="ItemRackQueueTemplate"/>` under `ItemRackInv0` names its texture
/// `ItemRackInv0Queue`, the global `Rack.OnEvent` looks up (ItemRack.lua:4015).
#[test]
fn an_unnamed_instance_does_not_take_its_template_name() {
let s = UiScript::new().unwrap();
let doc = parse(
r#"<Ui>
<Frame name="ItemRackQueueTemplate" virtual="true">
<Layers><Layer level="OVERLAY">
<Texture name="$parentQueue"/>
</Layer></Layers>
</Frame>
<CheckButton name="ItemRackInvTemplate" virtual="true">
<Frames>
<Frame inherits="ItemRackQueueTemplate"/>
</Frames>
</CheckButton>
<CheckButton name="ItemRackInv0" inherits="ItemRackInvTemplate"/>
</Ui>"#,
);
let report = load(&s, &doc, &no_files);
assert!(report.errors.is_empty(), "{:?}", report.errors);
assert!(
s.eval::<bool>("return ItemRackInv0Queue ~= nil").unwrap(),
"the unnamed child's texture must be named from the real parent, not the template: {:?}",
report.warnings
);
assert!(
s.eval::<bool>("return ItemRackQueueTemplate == nil").unwrap(),
"a virtual template must not leak a global named after it"
);
}
}
#[test]