diff --git a/sql/updates/hotfixes/master/2026_05_08_00_hotfixes.sql b/sql/updates/hotfixes/master/2026_05_08_00_hotfixes.sql new file mode 100644 index 000000000..34467313e --- /dev/null +++ b/sql/updates/hotfixes/master/2026_05_08_00_hotfixes.sql @@ -0,0 +1,98 @@ +-- +-- 2026-04-29: housing door click — spell 1271876 + exterior_component schema refresh +-- +-- Sniff-decoded from C:\sniff\housing_stuff\{alliance,horde}_housing\dump_12.0.5.67186_2026-04-28_*.pkt: +-- The retail client casts SpellID 1271876 (uint32 at CMSG_CAST_SPELL body offset 0x15) when a +-- player clicks a housing door GO. Bytes 0x15-0x18 are identical across both faction sniffs; +-- only the target PackedGUID at offset 0x3D differs. The cast (not CMSG_GAME_OBJ_REPORT_USE, +-- which is the criteria-tracking follow-up) is what triggers TRANSFER_PENDING + NEW_WORLD on +-- retail. Adding a minimal spell record so server-side cast validation accepts the spell — +-- the actual door action is wired via gameobject_template.data2 (goober.spell) in the world +-- update + the SpellScript in src/server/scripts/Spells/spell_housing.cpp. + +-- Task A: Spell 1271876 "Housing Door Open" +DELETE FROM `spell_name` WHERE `id` = 1271876; +INSERT INTO `spell_name` (`id`, `name`, `VerifiedBuild`) VALUES + (1271876, 'Housing Door Open', 67186); + +DELETE FROM `spell_misc` WHERE `SpellID` = 1271876; +INSERT INTO `spell_misc` ( + `Attributes1`, `Attributes2`, `Attributes3`, `Attributes4`, `Attributes5`, `Attributes6`, `Attributes7`, + `Attributes8`, `Attributes9`, `Attributes10`, `Attributes11`, `Attributes12`, `Attributes13`, `Attributes14`, + `Attributes15`, `Attributes16`, `Attributes17`, + `DifficultyID`, `CastingTimeIndex`, `DurationIndex`, `PvPDurationIndex`, `RangeIndex`, `SchoolMask`, + `Speed`, `LaunchDelay`, `MinDuration`, + `SpellIconFileDataID`, `ActiveIconFileDataID`, `ContentTuningID`, `ShowFutureSpellPlayerConditionID`, + `SpellVisualScript`, `ActiveSpellVisualScript`, + `SpellID`, `VerifiedBuild` +) VALUES + (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 1, 0, 0, 1, 0, + 0, 0, 0, + 0, 0, 0, 0, + 504329, 0, + 1271876, 67186); + +-- Effect 0: SPELL_EFFECT_DUMMY (3), targeting the GameObject (TARGET_GAMEOBJECT_TARGET = 50). +-- The spell needs at least one effect for SpellScript hooks to fire and for SpellInfo +-- validation to pass. The actual door action runs in spell_housing_door_open's +-- HandleDummy override (see src/server/scripts/Spells/spell_housing.cpp), which calls +-- GameObject::Use() on the target so go_housing_door::OnGossipHello executes. +DELETE FROM `spell_effect` WHERE `SpellID` = 1271876; +INSERT INTO `spell_effect` ( + `ID`, `EffectAura`, `DifficultyID`, `EffectIndex`, `Effect`, + `EffectAmplitude`, `EffectAttributes`, `EffectAuraPeriod`, + `EffectBonusCoefficient`, `EffectChainAmplitude`, `EffectChainTargets`, + `EffectItemType`, `EffectMechanic`, `EffectPointsPerResource`, + `EffectPosFacing`, `EffectRealPointsPerLevel`, `EffectTriggerSpell`, + `BonusCoefficientFromAP`, `PvpMultiplier`, `Coefficient`, + `Variance`, `ResourceCoefficient`, `GroupSizeBasePointsCoefficient`, + `EffectBasePoints`, `EffectMiscValue1`, `EffectMiscValue2`, + `EffectRadiusIndex1`, `EffectRadiusIndex2`, + `EffectSpellClassMask1`, `EffectSpellClassMask2`, + `EffectSpellClassMask3`, `EffectSpellClassMask4`, + `ImplicitTarget1`, `ImplicitTarget2`, + `SpellID`, `VerifiedBuild` +) VALUES + (1271876, 0, 0, 0, 3, -- ID, EffectAura, DifficultyID, EffectIndex, Effect=DUMMY + 0, 0, 0, -- EffectAmplitude, EffectAttributes, EffectAuraPeriod + 0, 0, 0, -- EffectBonusCoefficient, EffectChainAmplitude, EffectChainTargets + 0, 0, 0, -- EffectItemType, EffectMechanic, EffectPointsPerResource + 0, 0, 0, -- EffectPosFacing, EffectRealPointsPerLevel, EffectTriggerSpell + 0, 0, 0, -- BonusCoefficientFromAP, PvpMultiplier, Coefficient + 0, 0, 0, -- Variance, ResourceCoefficient, GroupSizeBasePointsCoefficient + 0, 0, 0, -- EffectBasePoints, EffectMiscValue1, EffectMiscValue2 + 0, 0, -- EffectRadiusIndex1, EffectRadiusIndex2 + 0, 0, 0, 0, -- EffectSpellClassMask1..4 + 23, 0, -- ImplicitTarget1=TARGET_GAMEOBJECT_TARGET, ImplicitTarget2=NONE + 1271876, 67186); + + +-- Task D: ExteriorComponent — schema refresh to match 12.0.5 DB2LoadInfo (build 67186 layout 0x53EA0925) +-- Old SQL columns (FileDataID/ConditionID/HookID/Slot/SortOrder/ComponentGroupID/UiTextureKitID/ +-- ExteriorComponentTypeID) were from an older WoW build and don't line up with the 14-field +-- 12.0.5 record (Name + 3 floats + ID + Size + ParentComponentID + ModelFileDataID + Flags + +-- Field_7 + Type + Field_9 + GameObjectID + Field_11 + ItemID + HouseExteriorWmoDataID). +-- HousingMap::SpawnExtCompTree reads `comp->GameObjectID` to spawn the door GO; with the old +-- schema that column doesn't exist and HotfixDatabase loads zeros, so doors never spawn. +DROP TABLE IF EXISTS `exterior_component`; +CREATE TABLE `exterior_component` ( + `Name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `PositionX` float NOT NULL DEFAULT '0', + `PositionY` float NOT NULL DEFAULT '0', + `PositionZ` float NOT NULL DEFAULT '0', + `ID` int unsigned NOT NULL DEFAULT '0', + `Size` tinyint unsigned NOT NULL DEFAULT '0', + `ParentComponentID` int NOT NULL DEFAULT '0', + `ModelFileDataID` int NOT NULL DEFAULT '0', + `Flags` int NOT NULL DEFAULT '0', + `Field_7` tinyint unsigned NOT NULL DEFAULT '0', + `Type` tinyint unsigned NOT NULL DEFAULT '0', + `Field_9` int NOT NULL DEFAULT '0', + `GameObjectID` int NOT NULL DEFAULT '0', + `Field_11` int NOT NULL DEFAULT '0', + `ItemID` int NOT NULL DEFAULT '0', + `HouseExteriorWmoDataID` int unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file diff --git a/sql/updates/world/master/2026_05_08_01_world.sql b/sql/updates/world/master/2026_05_08_01_world.sql new file mode 100644 index 000000000..15a3ef431 --- /dev/null +++ b/sql/updates/world/master/2026_05_08_01_world.sql @@ -0,0 +1,39 @@ +-- +-- 2026-04-29: housing front-door GO templates — set goober.spell (Data10) to 1271876 +-- +-- Sniff-decoded from C:\sniff\housing_stuff\{alliance,horde}_housing\ +-- dump_12.0.5.67186_2026-04-28_*.pkt: when a housing door is clicked the 12.0.5 retail +-- client emits CMSG_CAST_SPELL with SpellID 1,271,876 targeting the door GO. That ID +-- is the new "Housing Door Open" spell (added in this commit's hotfix update). The +-- corresponding goober field on the door GO templates was either zero (575017, +-- 602702) or pointing at the older 12.0.1 spell 1234192 (586576, 602705) — neither +-- matches what the retail client casts, so server-side spell validation rejects the +-- cast (Spell::CheckSpellId or Spell::SendCastResult). Aligning Data10 with the +-- 12.0.5 spell makes the cast accept and run our SpellScript, which calls +-- GameObject::Use() on the target — invoking the existing go_housing_door:: +-- OnGossipHello handler that performs the interior teleport. +-- +-- Entries covered (all GAMEOBJECT_TYPE_GOOBER housing front doors): +-- 575017 Interior Front Door (Data10 was 0) +-- 586576 Front Door (Founder's Point) (Data10 was 1234192) +-- 587318 Razorwind Shores Front Door (Data10 was 1234193 — Razorwind variant) +-- 602702 Front Door (Data10 was 0) +-- 602705 Front Door (Data10 was 1234192) + +UPDATE `gameobject_template` SET `Data10` = 1271876 WHERE `entry` IN ( + 575017, + 586576, + 587318, + 602702, + 602705 +); + + +-- +-- Bind SpellID 1271876 to the spell_housing_door_open SpellScript so the dummy +-- effect actually invokes our handler. SpellScripts registered via +-- RegisterSpellScript() in C++ are matched to spells through this table. +-- +DELETE FROM `spell_script_names` WHERE `spell_id` = 1271876 AND `ScriptName` = 'spell_housing_door_open'; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES + (1271876, 'spell_housing_door_open'); \ No newline at end of file diff --git a/src/server/scripts/Spells/spell_housing.cpp b/src/server/scripts/Spells/spell_housing.cpp index 4e9546996..4ad0aefe6 100644 --- a/src/server/scripts/Spells/spell_housing.cpp +++ b/src/server/scripts/Spells/spell_housing.cpp @@ -23,7 +23,8 @@ enum HousingCornerstoneSpells { - SPELL_TRIGGER_CONVO_UNOWNED_PLOT = 1266097 + SPELL_TRIGGER_CONVO_UNOWNED_PLOT = 1266097, + SPELL_HOUSING_DOOR_OPEN = 1271876 // 12.0.5 retail housing-front-door open spell }; // 1266097 - [DNT] Trigger Convo for Unowned Plot @@ -54,7 +55,69 @@ class spell_housing_trigger_convo_unowned_plot : public SpellScript } }; +// 1271876 - Housing Door Open (12.0.5 retail) +// Cast by client when a player clicks a housing front door GO. Sniff-decoded from +// dump_12.0.5.67186_2026-04-28_*.pkt: bytes 0x15-0x18 of CMSG_CAST_SPELL body = +// 0x00136644 = 1271876, with the door GO as the spell target. Both faction sniffs +// agree on the spell ID (only the target PackedGUID differs). +// +// The spell row in spell_misc / spell_effect we ship for it (sql/updates/hotfixes/ +// master/2026_04_29_00_hotfixes.sql) is a minimal SPELL_EFFECT_DUMMY targeting +// TARGET_GAMEOBJECT_TARGET so server-side validation accepts the cast and the +// dummy hook fires. We intentionally do NOT use SPELL_EFFECT_OPEN_LOCK or +// SPELL_EFFECT_USE_GAMEOBJECT ? those would double-trigger Use() through the spell +// effect machinery in addition to the explicit Use() this script issues. Routing +// through one well-defined path keeps the door teleport semantics owned by the +// existing go_housing_door::OnGossipHello (which already handles edit-mode gating, +// visitor permissions, and the interior?exterior round trip). +class spell_housing_door_open : public SpellScript +{ + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return true; + } + + void HandleDummy(SpellEffIndex /*effIndex*/) const + { + Player* caster = GetCaster()->ToPlayer(); + if (!caster) + return; + + WorldObject* target = GetExplTargetWorldObject(); + if (!target) + target = GetHitGObj(); + if (!target) + { + TC_LOG_DEBUG("housing", "spell_housing_door_open: Spell {} cast by {} but no GO target ? ignored", + GetSpellInfo()->Id, caster->GetGUID().ToString()); + return; + } + + GameObject* doorGo = target->ToGameObject(); + if (!doorGo) + { + TC_LOG_DEBUG("housing", "spell_housing_door_open: Spell {} cast by {} on non-GO target {} ? ignored", + GetSpellInfo()->Id, caster->GetGUID().ToString(), target->GetGUID().ToString()); + return; + } + + TC_LOG_DEBUG("housing", "spell_housing_door_open: Player {} cast {} on door GO {} (entry={}) ? invoking Use()", + caster->GetGUID().ToString(), GetSpellInfo()->Id, doorGo->GetGUID().ToString(), doorGo->GetEntry()); + + // Route to the standard interaction path so go_housing_door::OnGossipHello + // performs the teleport. Use() handles edit-mode gating + animation; the + // gossip script then resolves the destination plot/interior. + doorGo->Use(caster); + } + + void Register() override + { + OnEffectHit += SpellEffectFn(spell_housing_door_open::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); + } +}; + void AddSC_housing_spell_scripts() { RegisterSpellScript(spell_housing_trigger_convo_unowned_plot); + RegisterSpellScript(spell_housing_door_open); }