Files
benilla-octo/crates/benilla-app/assets/ui/DeathFrame.xml
T
SamandClaude Fable 5 ddc9e1a5a3 being summoned is a dialog, and a soulstone raises you where you fell
- being summoned becomes the dialog it should have been: the summon request was a name-table entry and nothing else, so a warlock's ritual, a meeting stone and a GM's summon all ended with the summoned player seeing nothing — now the three-field question raises CONFIRM_SUMMON and Accept sends the one guid back; the bytes settled three things a reasonable guess gets wrong: the refusal gate is dead OR ghost and lives in the handler (a ghost's wire health is 1, so the health leg alone lets one through — and refusing before the latch means an ill-timed summon can't silently overwrite a live question), the event carries no arguments because the popup re-reads all three getters every frame (the only way the summoner's name can appear after a cache miss), and there is no decline — Cancel, ESC and the clock running out are all silence plus the server's own two-minute timer; exercised end to end by a two-client live probe, the only thing that can, whose final assertion is that you land in the zone the dialog named
- self-resurrect is one private field and one bodyless opcode: PLAYER_SELF_RES_SPELL carries the stored spell and CMSG_SELF_RES spends it; HasSoulstone answers the Spell.dbc NAME of that spell, because that is where Blizzard put the button text — "Use Soulstone", "Reincarnation" and "Twisting Nether" are the spells' own names — and UseSoulstone sends on the death dialog's second button; a --self-res probe runs the whole arc on the live wire and measured the thing that matters: the field arrives BEFORE the health-zero flush, which is what makes the dialog's OnShow read safe
- the decode's fold-in corrected three claims we were resting on: HasSoulstone has a dead gate that runs FIRST, so a released ghost (wire health 1) loses the button at release rather than the dialog merely hiding; a zero field is not the empty answer — the client walks your carried inventory (gear, bags, backpack, keyring; no bank, no buyback) for an item whose on-use spell self-resurrects, labels the button with the ITEM's name and uses the item instead of sending, the two legs exclusive; and an unresolvable spell id labels the button "UNKNOWN" rather than hiding a self-res the server is holding for you

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:16:13 +07:00

311 lines
17 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The death arc's UI (decision 0308 slices 1/3, plus 1746's self-resurrect): our death
dialogs, registered on the shared StaticPopup engine (UiPanels.xml), plus the event driver
that raises and clears them.
The patch-chain FrameXML (mpqx extraction, decision 0246) is the SPECIFICATION for what
each dialog says, offers and answers — StaticPopup.lua l.375-505 (DEATH + RESURRECT×3),
l.1134-1201 (XP_LOSS×2 + RECOVER_CORPSE*), UIParent.lua l.237-262 + l.399-436 (the event
arms). The field names below are the published registry API that addons write into, so
they are the reference's and cannot be renamed; the code that fills them is ours. Six of
the eight dialogs share one disposition and are stamped with it rather than repeating it,
the three resurrect offers and the two spirit-healer confirms are each one shape built
more than once, and the driver is a single event switch. Strings are the reference's own
(GlobalStrings.lua), which is data, not authorship.
The engine-side events feeding this: PLAYER_DEAD/ALIVE/UNGHOST +
RESURRECT_REQUEST + CONFIRM_XP_LOSS fire from `death.rs`'s state machine;
CORPSE_IN_RANGE/OUT_OF_RANGE/IN_INSTANCE join with the slice-2 corpse-run range gate
(registered now — they simply don't fire yet). Deferred with 0308 §8: the SKINNED/
SKINNED_REPOP pair (battleground insignia).
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim; %% escapes as shipped) ─────────────────────────
local DEATH_RELEASE_TIMER = "%d %s until release"
local DEATH_RELEASE_NOTIMER = "You have died. Release to the nearest graveyard?"
local DEATH_RELEASE = "Release Spirit"
-- Vestigial on this build, and kept anyway because it is what the reference registers:
-- HasSoulstone() returns a Spell.dbc NAME whenever it returns anything, and OnShow stamps
-- that over button2 — so the only case this default could render is one that cannot
-- happen (DisplayButton2 shows the button on exactly the same call). It is the pre-1.6
-- wording, from before "Self-resurrection spells show their name on the button" (decision
-- 1746); GlobalStrings still ships it with the comment "Use your soulstone if you have one".
local USE_SOULSTONE = "Reincarnate"
local RECOVER_CORPSE_TEXT = "Resurrect now?"
local RECOVER_CORPSE_TIMER = "%d %s until resurrection"
local RECOVER_CORPSE_INSTANCE_TEXT = "You must enter the instance to recover your corpse"
local RESURRECT_REQUEST_TEXT = "%s wants to resurrect you. You will be afflicted with resurrection sickness."
local RESURRECT_REQUEST_TIMER = "%s wants to resurrect you and will be able to in %d %s. You will be afflicted with resurrection sickness."
local RESURRECT_REQUEST_NO_SICKNESS = "%s wants to resurrect you"
local RESURRECT_REQUEST_NO_SICKNESS_TIMER = "%s wants to resurrect you and will be able to in %d %s"
local CONFIRM_XP_LOSS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory) and you will be afflicted by %s of Resurrection Sickness."
local CONFIRM_XP_LOSS_AGAIN = "Remember, if you find your corpse there is no penalty. Are you sure you want to incur %s of Resurrection Sickness and have all your items take 25%% durability damage?"
local CONFIRM_XP_LOSS_NO_SICKNESS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory)."
local CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS = "Remember, if you find your corpse there is no penalty. Are you sure you want to have all your items take 25% durability damage?"
local ACCEPT = "Accept"
local DECLINE = "Decline"
local CANCEL = "Cancel"
-- ── The corpse-side disposition ──────────────────────────────────────────────────────────
-- Six of these eight dialogs are about a body on the floor, and every one of them wants
-- the same three things from the engine: it must be legal to raise while you are dead
-- (`whileDead` — that is the whole point of the arc), it takes precedence over a running
-- cinematic, and a logout must not quietly close it out from under you. Stamped on here
-- instead of retyped six times; the fields still land on the entry, so an addon reading
-- StaticPopupDialogs["DEATH"].whileDead sees exactly what the reference's does.
local function register_corpse_dialog(which, entry)
entry.whileDead = 1
entry.interruptCinematic = 1
entry.notClosableByLogout = 1
StaticPopupDialogs[which] = entry
end
-- How long a resurrect offer stands once it becomes answerable. It is both the engine's
-- own countdown (`timeout`) and the value OnShow re-seeds past the recovery delay.
local RESURRECT_OFFER_SECONDS = 60
-- ── The release dialog (spec: StaticPopup.lua l.375-414) ─────────────────────────────────
-- OnShow seeds `timeleft` from GetReleaseTimeRemaining(), which the engine renders through
-- its per-tick DEATH text; −1 means this map has no forced release (an instance), so the
-- static wording goes in and nothing counts down. `timeout = 0` because the countdown is
-- the SERVER's to enforce — it force-releases at 6:00 (0308 §1) — and there is deliberately
-- no hideOnEscape: you cannot dismiss dying. Button2 appears only when the server owes us
-- a self-resurrect (PLAYER_SELF_RES_SPELL non-zero); without one a cancel falls through
-- to the release.
register_corpse_dialog("DEATH", {
text = DEATH_RELEASE_TIMER,
button1 = DEATH_RELEASE,
button2 = USE_SOULSTONE,
DisplayButton2 = function()
return HasSoulstone()
end,
OnShow = function()
local name = this:GetName()
this.timeleft = GetReleaseTimeRemaining()
if this.timeleft == -1 then
getglobal(name .. "Text"):SetText(DEATH_RELEASE_NOTIMER)
end
-- The self-res names its own button, out of Spell.dbc: "Use Soulstone" (3026,
-- 20758-20761), "Reincarnation" (21169), "Twisting Nether" (23700).
local soulstone = HasSoulstone()
if soulstone then
getglobal(name .. "Button2"):SetText(soulstone)
end
end,
OnAccept = function()
RepopMe()
end,
OnCancel = function(data, reason)
-- Only a real click on button2 is an answer. "override" and "timeout" are the
-- engine taking the dialog away, and neither is the player choosing anything.
if reason ~= "clicked" then
return
end
-- Button2 is the self-resurrect (decision 1746): CMSG_SELF_RES, and the server
-- casts whatever PLAYER_SELF_RES_SPELL holds. Re-asked rather than remembered
-- from OnShow, exactly as the reference does — the field is the server's and can
-- have been spent since the dialog opened.
if HasSoulstone() then
UseSoulstone()
else
RepopMe()
end
end,
timeout = 0,
cancels = "RECOVER_CORPSE",
})
-- ── At the corpse (spec: StaticPopup.lua l.1182-1202) — slice 2's range gate raises these.
-- Accept is gated behind the recovery delay and, once taken, the dialog STAYS: reclaiming
-- is the server's to confirm, and it does so with the descriptor deltas that stand you up.
register_corpse_dialog("RECOVER_CORPSE", {
StartDelay = GetCorpseRecoveryDelay,
delayText = RECOVER_CORPSE_TIMER,
text = RECOVER_CORPSE_TEXT,
button1 = ACCEPT,
OnAccept = function()
RetrieveCorpse()
return 1
end,
timeout = 0,
})
-- Your body is inside a dungeon you are standing outside of: a notice, not an offer, so
-- it carries no button at all.
register_corpse_dialog("RECOVER_CORPSE_INSTANCE", {
text = RECOVER_CORPSE_INSTANCE_TEXT,
timeout = 0,
})
-- ── Somebody else's offer (spec: StaticPopup.lua l.416-505) ──────────────────────────────
-- Three registry keys, one shape: with sickness, without, and without a recovery timer to
-- wait out. They differ only in the wording and in whether the offer is delay-gated, so
-- they are built rather than written three times.
--
-- Refusing an offer (or letting it lapse) has to undo the `cancels = "DEATH"` that raising
-- it did — otherwise a declined resurrect leaves you dead with nothing on screen to
-- release from.
local function restore_release_dialog()
if UnitIsDead("player") then
StaticPopup_Show("DEATH")
end
end
local function resurrect_offer(offer_text, delay_text)
return {
text = offer_text,
button1 = ACCEPT,
button2 = DECLINE,
OnAccept = function()
AcceptResurrect()
end,
OnCancel = function()
DeclineResurrect()
restore_release_dialog()
end,
-- The countdown. Given a `delay_text` the offer is gated: Accept stays dead and
-- that text counts the corpse-recovery cooldown down; without one (the no-timer
-- variant) Accept is live at once. Either way the offer lapses a minute after it
-- becomes answerable, which is why OnShow re-seeds `timeleft` past the gate
-- instead of leaving `timeout`'s minute to start the moment it appears.
StartDelay = delay_text and GetCorpseRecoveryDelay or nil,
delayText = delay_text,
OnShow = function()
this.timeleft = GetCorpseRecoveryDelay() + RESURRECT_OFFER_SECONDS
end,
timeout = RESURRECT_OFFER_SECONDS,
cancels = "DEATH",
hideOnEscape = 1,
}
end
register_corpse_dialog("RESURRECT",
resurrect_offer(RESURRECT_REQUEST_TEXT, RESURRECT_REQUEST_TIMER))
register_corpse_dialog("RESURRECT_NO_SICKNESS",
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS, RESURRECT_REQUEST_NO_SICKNESS_TIMER))
register_corpse_dialog("RESURRECT_NO_TIMER",
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS))
-- ── The spirit healer's price (spec: StaticPopup.lua l.1134-1180) ────────────────────────
-- Both variants ask twice. The first Accept rewrites the dialog to a blunter second ask
-- and clears `data`; the truthy return keeps it open. The second Accept — now with no data
-- — sends the answer. So `data` is at once the payload and the "which ask is this" flag,
-- which is why the driver below hands the no-sickness variant a bare 1.
--
-- The second ask is a FUNCTION, not a string, because only one of the two has anything to
-- substitute: the no-sickness wording carries a bare "25% durability" that format() would
-- read as a conversion and choke on.
--
-- Neither offer outlives walking away from the healer, so both poll the distance.
local function close_when_out_of_reach(elapsed, dialog)
if not CheckSpiritHealerDist() then
dialog:Hide()
end
end
local function spirit_healer_confirm(first_ask, second_ask)
return {
text = first_ask,
button1 = ACCEPT,
button2 = CANCEL,
OnAccept = function(data)
if not data then
AcceptXPLoss()
return
end
-- `this` is the button the engine clicked; the dialog is its parent.
local dialog = this:GetParent()
getglobal(dialog:GetName() .. "Text"):SetText(second_ask(data))
dialog.data = nil
return 1
end,
OnUpdate = close_when_out_of_reach,
timeout = 0,
exclusive = 1,
whileDead = 1,
showAlert = 1,
hideOnEscape = 1,
}
end
StaticPopupDialogs["XP_LOSS"] = spirit_healer_confirm(CONFIRM_XP_LOSS, function(sickness)
return format(CONFIRM_XP_LOSS_AGAIN, sickness)
end)
StaticPopupDialogs["XP_LOSS_NO_SICKNESS"] = spirit_healer_confirm(
CONFIRM_XP_LOSS_NO_SICKNESS,
function()
return CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS
end)
-- ── The driver (spec: UIParent.lua l.237-262 + l.399-436) ───────────────────────────────
-- One switch over the arc's events. The reference spreads these arms through the whole of
-- UIParent_OnEvent; ours has a frame of its own (below) and nothing else to share it with.
function BenillaDeathDriver_OnEvent()
if event == "PLAYER_DEAD" then
if not StaticPopup_Visible("DEATH") then
CloseAllWindows(1)
if GetReleaseTimeRemaining() > 0 or GetReleaseTimeRemaining() == -1 then
StaticPopup_Show("DEATH")
end
end
elseif event == "PLAYER_ALIVE" then
StaticPopup_Hide("DEATH")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
elseif event == "PLAYER_UNGHOST" then
StaticPopup_Hide("RESURRECT")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
StaticPopup_Hide("RESURRECT_NO_TIMER")
elseif event == "RESURRECT_REQUEST" then
if ResurrectHasSickness() then
StaticPopup_Show("RESURRECT", arg1)
elseif ResurrectHasTimer() then
StaticPopup_Show("RESURRECT_NO_SICKNESS", arg1)
else
StaticPopup_Show("RESURRECT_NO_TIMER", arg1)
end
elseif event == "CONFIRM_XP_LOSS" then
-- If the healer would charge resurrection sickness, its duration goes into the
-- text; if not there is nothing to quote, and the bare 1 handed over instead is
-- only the "this is still the first ask" marker the two-step above reads.
local sickness = GetResSicknessDuration()
local dialog
if sickness then
dialog = StaticPopup_Show("XP_LOSS", sickness)
else
dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
end
if dialog then
dialog.data = sickness or 1
end
-- The confirm covers the healer's gossip window (spec: UIParent.lua l.412).
local gossip = getglobal("GossipFrame")
if gossip and gossip:IsVisible() then
HideUIPanel(gossip)
end
elseif event == "CORPSE_IN_RANGE" then
StaticPopup_Show("RECOVER_CORPSE")
elseif event == "CORPSE_IN_INSTANCE" then
StaticPopup_Show("RECOVER_CORPSE_INSTANCE")
elseif event == "CORPSE_OUT_OF_RANGE" then
StaticPopup_Hide("RECOVER_CORPSE")
StaticPopup_Hide("RECOVER_CORPSE_INSTANCE")
StaticPopup_Hide("XP_LOSS")
end
end
]]></Script>
<Frame name="BenillaDeathDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("PLAYER_DEAD")
self:RegisterEvent("PLAYER_ALIVE")
self:RegisterEvent("PLAYER_UNGHOST")
self:RegisterEvent("RESURRECT_REQUEST")
self:RegisterEvent("CONFIRM_XP_LOSS")
self:RegisterEvent("CORPSE_IN_RANGE")
self:RegisterEvent("CORPSE_IN_INSTANCE")
self:RegisterEvent("CORPSE_OUT_OF_RANGE")
</OnLoad>
<OnEvent>BenillaDeathDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>