From 2403edcf18f6cf725491469095b306edb3109a55 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 5 Nov 2026 10:29:35 +0100 Subject: [PATCH] Fix Alchemist Stone procs for healing potion events --- .../2026-11-05-alchemist-stone-crash.md | 38 +++++++++++ src/server/scripts/Spells/spell_generic.cpp | 39 ++++++++--- tests/clientless/test_alchemist_stone_proc.py | 66 +++++++++++++++++++ 3 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 doc/clientless/2026-11-05-alchemist-stone-crash.md create mode 100644 tests/clientless/test_alchemist_stone_proc.py diff --git a/doc/clientless/2026-11-05-alchemist-stone-crash.md b/doc/clientless/2026-11-05-alchemist-stone-crash.md new file mode 100644 index 0000000000..a731f10351 --- /dev/null +++ b/doc/clientless/2026-11-05-alchemist-stone-crash.md @@ -0,0 +1,38 @@ +# Alchemistenstein: Heilungsereignis ohne DamageInfo + +Der gesicherte Core worldserver-20261105-002730.core zeigt SIGSEGV in +DamageInfo::GetSpellInfo(this=nullptr), aufgerufen aus dem CheckProc des +Alchemistensteins in spell_generic.cpp. Akteur: Drakzug; Trankzauber 43185; +HealInfo enthält 3728 Heilung, DamageInfo ist null. Der native Zauberpfad +übergibt bei reinen Heiltreffern absichtlich HealInfo ohne DamageInfo. + +CheckProc verwendet jetzt die vorhandene native Ereignisabfrage +ProcEventInfo::GetSpellInfo und prüft fehlende Metadaten. HandleProc prüft diese +Daten ebenfalls und verwendet für Heiltränke HealInfo::GetHeal. So bleiben +40 Prozent der ursprünglichen Heilung einschließlich Überheilung als +zusätzliche Heilung erhalten (3728 -> 1491 nach bisheriger Rundung). + +Der bisherige Energize-Zweig verwendet weiterhin einen vorhandenen +DamageInfo-Betrag. Fehlende oder leere Beträge erzeugen keinen Bonuszauber. +Eine allgemeine Neuerfassung von Manawiederherstellungsbeträgen in der Engine +ist nicht Teil dieses Crashfixes. Nicht-Trank-Ereignisse erzeugen keinen Bonus. + +Der Regressionstest test_alchemist_stone_proc führt beide Produktionshandler +sowie die native Metadatenabfrage mit ASan und UBSan aus: der konkrete Dumpfall, +HealInfo ohne DamageInfo, ursprüngliche statt effektive Heilung, Rundung, +bisherige Mana-Payloads, fehlende Metadaten und Payloads, Nullbeträge und +Nicht-Trank-Ereignisse. Test und Build mit -j10 bestanden. + +Der Server war bei Buildbeginn bereits beendet; Loaderkonfiguration wurde auf +0 gesetzt, es gab keine laufenden Bots für einen Logout. Vor Installation und +Start wurden Prozess- und tmux-Abwesenheit geprüft. Weitere Ergebnisse in +SESSION-RESUME.md und checkpoints/2026-11-05-alchemist-stone/. + +Das ursprüngliche passende Crashbinary liegt zur weiteren Diagnose unter +checkpoints/2026-11-05-edwin-crash/worldserver-crashed; Core und Backtrace bleiben +im Crashverzeichnis erhalten. Keine Dumps wurden gelöscht. + +Installation und Neustart erfolgreich: PID 4477, frisches Ready und Ports +8085/8087 geprüft. Regulärer Loader aktiv, 118 Bots online, kein ausstehender +sicherer Logout. Ein zusätzlicher Ingame-Heiltranktest wurde nicht durchgeführt; +der konkrete Corefall wird durch den Produktionstest reproduziert. diff --git a/src/server/scripts/Spells/spell_generic.cpp b/src/server/scripts/Spells/spell_generic.cpp index c9b322772f..f7a0bbe081 100644 --- a/src/server/scripts/Spells/spell_generic.cpp +++ b/src/server/scripts/Spells/spell_generic.cpp @@ -199,24 +199,43 @@ class spell_gen_alchemist_stone : public SpellScriptLoader bool CheckProc(ProcEventInfo& eventInfo) { - return eventInfo.GetDamageInfo()->GetSpellInfo()->SpellFamilyName == SPELLFAMILY_POTION; + SpellInfo const* spellInfo = eventInfo.GetSpellInfo(); + return spellInfo && spellInfo->SpellFamilyName == SPELLFAMILY_POTION; } void HandleProc(AuraEffect const* aurEff, ProcEventInfo& eventInfo) { PreventDefaultAction(); - uint32 spellId = 0; - int32 bp = int32(eventInfo.GetDamageInfo()->GetDamage() * 0.4f); - - if (eventInfo.GetDamageInfo()->GetSpellInfo()->HasEffect(SPELL_EFFECT_HEAL)) - spellId = ALCHEMIST_STONE_HEAL; - else if (eventInfo.GetDamageInfo()->GetSpellInfo()->HasEffect(SPELL_EFFECT_ENERGIZE)) - spellId = ALCHEMIST_STONE_MANA; - - if (!spellId) + SpellInfo const* spellInfo = eventInfo.GetSpellInfo(); + if (!spellInfo || spellInfo->SpellFamilyName != SPELLFAMILY_POTION) return; + uint32 spellId = 0; + uint32 amount = 0; + if (spellInfo->HasEffect(SPELL_EFFECT_HEAL)) + { + // Healing hits carry HealInfo, not DamageInfo. Use the + // original heal, retaining the potion bonus with overheal. + if (HealInfo const* healInfo = eventInfo.GetHealInfo()) + { + spellId = ALCHEMIST_STONE_HEAL; + amount = healInfo->GetHeal(); + } + } + else if (spellInfo->HasEffect(SPELL_EFFECT_ENERGIZE)) + { + if (DamageInfo const* damageInfo = eventInfo.GetDamageInfo()) + { + spellId = ALCHEMIST_STONE_MANA; + amount = damageInfo->GetDamage(); + } + } + + if (!spellId || !amount) + return; + + int32 bp = int32(amount * 0.4f); GetTarget()->CastSpell(GetTarget(), spellId, CastSpellExtraArgs(aurEff).AddSpellBP0(bp)); } diff --git a/tests/clientless/test_alchemist_stone_proc.py b/tests/clientless/test_alchemist_stone_proc.py new file mode 100644 index 0000000000..f6acbd1805 --- /dev/null +++ b/tests/clientless/test_alchemist_stone_proc.py @@ -0,0 +1,66 @@ +"""Execute production potion proc handlers and native event metadata fallback.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/scripts/Spells/spell_generic.cpp').read_text();s=s[s.index('class spell_gen_alchemist_stone :'):] +def method(src,start): + a=src.index(start);o=src.index('{',a);depth=1;b=o+1 + while depth: + depth+=(src[b]=='{')-(src[b]=='}');b+=1 + return src[a:b] +unit=(root/'src/server/game/Entities/Unit/Unit.cpp').read_text() +code=r''' +#include +#include +#include +using uint32=uint32_t;using int32=int32_t; +constexpr int SPELLFAMILY_POTION=13,SPELL_EFFECT_HEAL=10,SPELL_EFFECT_ENERGIZE=30; +constexpr unsigned ALCHEMIST_STONE_HEAL=21399,ALCHEMIST_STONE_MANA=21400; +struct SpellInfo{int SpellFamilyName=SPELLFAMILY_POTION,effect=SPELL_EFFECT_HEAL;bool HasEffect(int v)const{return effect==v;}}; +struct DamageInfo{uint32 amount;SpellInfo const*info;uint32 GetDamage()const{return amount;}SpellInfo const*GetSpellInfo()const{return info;}}; +struct HealInfo{uint32 heal,effective;SpellInfo const*info;uint32 GetHeal()const{return heal;}SpellInfo const*GetSpellInfo()const{return info;}}; +struct Spell{SpellInfo const*info;SpellInfo const*GetSpellInfo()const{return info;}}; +struct ProcEventInfo{ + Spell*_spell=nullptr;DamageInfo*_damageInfo=nullptr;HealInfo*_healInfo=nullptr; + SpellInfo const*GetSpellInfo()const; + DamageInfo*GetDamageInfo(){return _damageInfo;}HealInfo*GetHealInfo(){return _healInfo;} +}; +'''+method(unit,'SpellInfo const* ProcEventInfo::GetSpellInfo() const')+r''' +struct AuraEffect{}; +struct CastSpellExtraArgs{AuraEffect const*aura;int32 bp=0;CastSpellExtraArgs(AuraEffect const*a):aura(a){}CastSpellExtraArgs&AddSpellBP0(int32 n){bp=n;return *this;}}; +struct Target{struct Cast{unsigned spell;int32 bp;AuraEffect const*aura;};std::vectorcasts;void CastSpell(Target*t,unsigned id,CastSpellExtraArgs args){assert(t==this);casts.push_back({id,args.bp,args.aura});}}; +struct Script{Target target;unsigned suppressed=0;void PreventDefaultAction(){++suppressed;}Target*GetTarget(){return ⌖} +'''+method(s,'bool CheckProc(')+method(s,'void HandleProc(')+r''' +}; +int main(){ + Script script;AuraEffect aura;SpellInfo healing;HealInfo heal{1000,200,&healing}; + ProcEventInfo event;event._healInfo=&heal; + // Exact crash shape: a healing potion has no DamageInfo. + assert(!event.GetDamageInfo()&&script.CheckProc(event));script.HandleProc(&aura,event); + assert(script.target.casts.size()==1&&script.target.casts.back().spell==21399&&script.target.casts.back().bp==400&&script.target.casts.back().aura==&aura); + assert(script.suppressed==1); // preserve native default-action suppression + // Spell metadata can come from the cast even without damage metadata. + Spell cast{&healing};event._spell=&cast;heal.heal=7;script.HandleProc(&aura,event);assert(script.target.casts.back().bp==2); + // Actual saved crash: Drakzug, potion 43185, raw heal 3728, DamageInfo=null. + heal.heal=3728;script.HandleProc(&aura,event);assert(script.target.casts.back().bp==1491); + heal.heal=0;auto count=script.target.casts.size();script.HandleProc(&aura,event);assert(script.target.casts.size()==count); + // Existing energize damage payload is preserved, including metadata fallback. + SpellInfo mana{SPELLFAMILY_POTION,SPELL_EFFECT_ENERGIZE};DamageInfo damage{500,&mana}; + event={};event._damageInfo=&damage;assert(script.CheckProc(event));script.HandleProc(&aura,event);assert(script.target.casts.back().spell==21400&&script.target.casts.back().bp==200); + count=script.target.casts.size();damage.amount=0;script.HandleProc(&aura,event);assert(script.target.casts.size()==count); + // Missing payloads and missing metadata never dereference null or invent a bonus. + event={};assert(!script.CheckProc(event));script.HandleProc(&aura,event); + cast.info=&mana;event._spell=&cast;assert(script.CheckProc(event));script.HandleProc(&aura,event); + damage={500,nullptr};event={};event._damageInfo=&damage;assert(!script.CheckProc(event));script.HandleProc(&aura,event); + cast.info=&healing;event._spell=&cast;assert(script.CheckProc(event));script.HandleProc(&aura,event); // no HealInfo + assert(script.target.casts.size()==count); + // Generic triggered bonuses and non-potion heals cannot recurse into the stone. + SpellInfo other{0,SPELL_EFFECT_HEAL};heal={1000,1000,&other};event={};event._healInfo=&heal; + assert(!script.CheckProc(event));script.HandleProc(&aura,event);assert(script.target.casts.size()==count); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17','-fsanitize=address,undefined','-fno-omit-frame-pointer','-g',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Alchemist Stone production proc: heal-only crash, metadata fallback, 40% raw heal/rounding, existing mana payload, missing/zero data and non-potion recursion guards PASS (ASan/UBSan)')