hotfix: the cold-breath classifier survives a despawning unit

- entering the world or teleporting could crash: the cold-breath classifier touched a unit the streamed world had just despawned, and the deferred insert panicked on the dead entity — it now tolerates the race

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-11 19:33:33 +07:00
co-authored by Claude Fable 5
parent e7ec7f2c39
commit 017860667b
3 changed files with 130 additions and 4 deletions
+45 -3
View File
@@ -233,10 +233,21 @@
-- page 5, else the paged/bonus formula); we carry the fork as data — a fixed per-button
-- page base set at OnLoad (MultiBars.xml), nil for the bonus-paged main bar.
function BenillaActionButton_Action(button)
-- `index` falls back to the button's ID, which is where the REFERENCE reads it from:
-- `ActionButton_GetPagedID` uses `this:GetID()`, and our `.index` is only that value
-- cached at OnLoad. A button that inherits `ActionBarButtonTemplate` and wires its own
-- OnLoad — zBar.xml:7 does exactly this — never runs ours, so `.index` is nil while
-- `GetID()` is set from the XML `id=`. Before the fallback, our own handler raised
-- `ActionBar.xml:228: arithmetic on local 'index'` on somebody else's button.
--
-- Still nothing? Action 0, which is not a real slot: every reader answers nil for it
-- and the button draws empty. Deliberately not an early `return nil` — the callers are
-- arithmetic all the way down, and a nil would move the raise rather than remove it.
local index = button.index or button:GetID() or 0
if button.base then
return button.base + button.index
return button.base + index
end
return BenillaActionBar_ActionFor(button.index)
return BenillaActionBar_ActionFor(index)
end
-- Hover: SetAction's pure delegation (decision 0276 — a SPELL slot renders the spell
@@ -991,7 +1002,19 @@
ButtonHilight-Square ADD (l.87); CheckedTexture CheckButtonHilight ADD (l.88 — the
IsCurrentAction/IsAutoRepeatAction ring). Only Name (macros) stays OUT.
-->
<CheckButton name="BenillaActionButtonTemplate" virtual="true">
<!-- **The reference's own two-level split, which ours used to conflate into one template.**
`ActionButtonTemplate` (ref ActionButtonTemplate.xml:3) is REGIONS ONLY and carries no
scripts at all; `ActionBarButtonTemplate` (ref ActionBarFrame.xml:4) inherits it and adds
the handler set. Addons rely on both halves being separately inheritable: `zBar.xml:7` is
`<CheckButton name="zBarButtonTemplate" inherits="ActionBarButtonTemplate">` and then
wires its OWN OnLoad, and its per-button code reads
`getglobal(button:GetName().."NormalTexture")` — a derived name it only gets by inheriting
a template that declares `$parentNormalTexture`.
2 corpus addons name `ActionBarButtonTemplate` in an XML `inherits=`; with neither name
published they got a bare frame, no art, no regions, and no error (1203's silent shape).
`BenillaActionButtonTemplate` survives below as a pure alias so every one of our own
`inherits=` sites is untouched. -->
<CheckButton name="ActionButtonTemplate" virtual="true">
<Size><AbsDimension x="36" y="36"/></Size>
<Layers>
<Layer level="BACKGROUND">
@@ -1010,6 +1033,16 @@
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- ref ActionButtonTemplate.xml's `$parentName` — the macro-name line along the
button's bottom edge. Empty by default, so it draws nothing until something
sets it; declared because it is part of the template's published contract and
an addon indexing `<button>Name` would otherwise get nil. -->
<FontString name="$parentName" inherits="GameFontHighlightSmallOutline">
<Size><AbsDimension x="36" y="10"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-ActionButton-Border" hidden="true" alphaMode="ADD">
@@ -1037,6 +1070,10 @@
<PushedTexture file="Interface\Buttons\UI-Quickslot-Depress"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
</CheckButton>
<!-- The bar half: the reference's `ActionBarButtonTemplate`, regions inherited, handlers added. -->
<CheckButton name="ActionBarButtonTemplate" inherits="ActionButtonTemplate" virtual="true">
<Scripts>
<OnClick>BenillaActionButton_OnClick(self)</OnClick>
<OnDragStart>BenillaActionButton_OnDragStart(self)</OnDragStart>
@@ -1047,6 +1084,11 @@
</Scripts>
</CheckButton>
<!-- Pure alias, so every existing `inherits="BenillaActionButtonTemplate"` in this file,
MultiBars.xml, StanceBar.xml and PetActionBar.xml keeps resolving to exactly what it did
before the split. -->
<CheckButton name="BenillaActionButtonTemplate" inherits="ActionBarButtonTemplate" virtual="true"/>
<!-- The bar: 1024x53 anchored BOTTOM of the screen (ref-MainMenuBar.xml l.4-10). -->
<Frame name="MainMenuBar">
<Size><AbsDimension x="1024" y="53"/></Size>
@@ -169,7 +169,9 @@ pub(super) fn classify_breath(
Some(id) if areas.0.is_cold(id) => Breath::Cold,
_ => Breath::None,
};
commands.entity(entity).insert(BreathEnv {
// try_insert: a streamed unit can despawn (teardown, teleport, range-out) between this
// system's parallel run and its sync point — a plain insert panics on the dead entity.
commands.entity(entity).try_insert(BreathEnv {
kind,
stale_at: now + RECLASSIFY_SECS,
});
@@ -1073,3 +1073,85 @@ fn the_reference_action_bar_constants_are_defined() {
"CURRENT_ACTIONBAR_PAGE is mutable page state we do not keep — nil fails loudly, a frozen 1 lies"
);
}
/// **The reference's two-level action-button split, both halves inheritable by name.**
///
/// `ActionButtonTemplate` (ref ActionButtonTemplate.xml:3) is regions only and carries NO scripts;
/// `ActionBarButtonTemplate` (ref ActionBarFrame.xml:4) inherits it and adds the handlers. Ours
/// conflated them under one `Benilla*` name, so an addon inheriting either reference name got a
/// bare frame — no art, no regions, and no error (1203's silent shape).
///
/// `zBar.xml:7` is the corpus shape: it inherits `ActionBarButtonTemplate`, wires its own OnLoad,
/// and then reads `getglobal(button:GetName().."NormalTexture")` — a derived name it only has
/// because the template declares `$parentNormalTexture`. That read is where it died.
///
/// The last assertion is the one that keeps our own bars safe: the alias must still resolve to the
/// full thing, or 48 `inherits=` sites across four files silently lose their handlers.
#[test]
fn both_reference_action_button_templates_are_inheritable() {
let s = UiScript::new().unwrap();
load_action_bar(&s);
// zBar's exact shape: inherit the bar template, supply your own OnLoad.
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<CheckButton name="ZLikeButton" inherits="ActionBarButtonTemplate" id="1">
<Anchors><Anchor point="CENTER"/></Anchors>
</CheckButton>
<CheckButton name="BareLikeButton" inherits="ActionButtonTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</CheckButton>
</Ui>"#,
)
.unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(
report.errors.is_empty(),
"loader errors: {:?}",
report.errors
);
assert!(
!report
.warnings
.iter()
.any(|w| w.contains("unknown template")),
"both names must resolve: {:?}",
report.warnings
);
// The derived name zBar reads, on a button built from each half.
for owner in ["ZLikeButton", "BareLikeButton"] {
assert!(
s.eval::<bool>(&format!("return {owner}NormalTexture ~= nil"))
.unwrap(),
"{owner}NormalTexture — zBar.lua:88's read"
);
assert!(
s.eval::<bool>(&format!(
"return {owner}Icon ~= nil and {owner}Cooldown ~= nil"
))
.unwrap(),
"{owner} must carry the template's regions"
);
}
// The base half carries NO handlers, exactly as the reference's does — an addon inheriting it
// wires its own, and must not silently receive ours.
assert!(
!s.eval::<bool>("return BareLikeButton:GetScript(\"OnClick\") ~= nil")
.unwrap(),
"ActionButtonTemplate is regions only; handlers belong to the bar half"
);
assert!(
s.eval::<bool>("return ZLikeButton:GetScript(\"OnClick\") ~= nil")
.unwrap(),
"ActionBarButtonTemplate carries the handler set"
);
// ...and our own alias still resolves to the full thing.
assert!(
s.eval::<bool>("return ActionButton1NormalTexture ~= nil and ActionButton1:GetScript(\"OnClick\") ~= nil")
.unwrap(),
"BenillaActionButtonTemplate's 48 inherits= sites must be untouched by the split"
);
}